[Bug]: macOS main process aborts with V8 OOM in JSON.stringify while relaying preview CDP events (SIGTRAP)#20395

opened21d agobyMMaxymvs

Before submitting

  • I searched existing issues and did not find a duplicate. #8332 (SIGTRAP during debugger.attach) and #17476 (accessibility bounds check) also end in a CrBrowserMain SIGTRAP, but their native stacks differ. The closest is the SIGSEGV in DevToolsSession::DispatchProtocolNotification posted as a comment on #8332. This crash goes through the same dispatch path but fails differently.
  • I included enough detail to investigate. I don't have a deterministic repro yet.

Area

apps/desktop (preview CDP relay, main process)

Steps to reproduce

The whole desktop app crashed twice in 42 seconds while an agent drove the built-in browser against a local React + Vite dev app. In both cases:

  1. The agent calls preview_open on http://localhost:5174/query. The tab opens hidden (visible: false), and the call succeeds.
  2. The agent calls preview_evaluate with a short DOM query. The second crash's expression also waits 3 seconds first.
  3. The main process aborts 4 to 9 seconds later. That preview_evaluate call never gets a response.

The second crash happened 21 seconds after relaunch, so this is not a slow leak over a long session. A single large message seems to be enough.

Timeline from the traces (local time, -0700):

Crash 1Crash 2
App launched15:07:4315:40:20
Last completed preview callpreview_snapshot (includeImage: true), 15:39:49preview_open, 15:40:28 to 15:40:30
In-flight callpreview_evaluate, sent 15:39:54preview_evaluate, sent 15:40:32
Crash15:39:5815:40:41

Before crash 1, a preview_click on the same page also timed out after 15000 ms. The page was busy during that run.

Expected behavior

A large CDP event from a preview tab should be dropped, truncated, or turned into a typed error. It should not abort the desktop process.

Actual behavior

V8 hits a fatal out-of-memory error inside JSON.stringify in the main process and aborts with EXC_BREAKPOINT (SIGTRAP). The desktop window and the bundled server go down together, and every running agent thread loses its turn.

Impact

Blocks work. The agent can't do browser testing on this page, and each crash takes down every other thread in the app.

Version or commit

  • T3 Code Nightly 0.0.46-nightly.20261015.3171
  • Electron Framework 44.4.5, UUID 4C4C44ED-5555-3144-A1CB-427AA45E6F33 (same build as #17476)

Environment

macOS 27.0.1 (26A434), Apple silicon (Mac16,7). The app was in the foreground.

Logs or stack traces

I symbolicated both reports with the official electron-v44.4.5-darwin-arm64-symbols.zip. Both crashes have the same stack. Crash 2, thread 0 (CrBrowserMain):

text
10 partition_alloc::internal::OnNoMemoryInternal(unsigned long) +0x18
22 partition_alloc::internal::OnNoMemory(unsigned long)
33 partition_alloc::internal::OnErrnoNoMem()
45 v8::Utils::ReportOOMFailure(...)
56 v8::internal::V8::FatalProcessOutOfMemory(...)
67 v8::internal::Zone::Expand(unsigned long)
78 v8::internal::Zone::ExpandAndAllocate(unsigned long)
89 v8::internal::OutBuffer<unsigned short>::Extend(unsigned long)
910 v8::internal::FastJsonStringifier<unsigned short>::SerializeObject(...)
1011 v8::internal::FastJsonStringifier<unsigned short>::ResumeFrom<unsigned char>(...)
1112 v8::internal::JsonStringify(...)
1213 v8::internal::Builtin_JsonStringify(...)
1314-19 <JIT JavaScript frames>
1420 v8::internal::Execution::Call(...)
1521 v8::Function::Call(...)
1622 node::InternalMakeCallback(...)
1724 gin_helper::internal::CallMethodWithArgs(...)
1825 gin_helper::EmitEvent<...>
1926 gin_helper::EventEmitterMixin<electron::api::Debugger>::Emit<...>
2027 electron::api::Debugger::DispatchProtocolMessage(...)
2128 content::DevToolsSession::DispatchProtocolResponseOrNotification(...)
2229 content::DevToolsSession::DispatchProtocolNotification(...)
2330 blink::mojom::DevToolsSessionHostStubDispatch::Accept(...)
24...

So a CDP notification from the preview tab reaches a webContents.debugger "message" listener, and that listener calls JSON.stringify on a value large enough to exhaust V8's zone. The OOM is a fatal V8 error, so JS can't catch it.

The bundled apps/desktop/dist-electron/main.cjs registers two debugger.on("message", ...) listeners. The control-session listener in PreviewManager.ensureControlSession doesn't stringify. The CDP relay listener (tab.onMessage → relay.event → send) does, for every event it forwards:

js
1const send = (message) => write(JSON.stringify(message));

It sends every non-download event once per attached page session, with no size check. That makes the relay the most likely caller. The JIT frames aren't symbolicated, so I can't prove it from the stack alone.

I don't know which CDP event is oversized. The page is a large React app served by the Vite dev server.

Possible fixes, from narrowest to broadest:

  • Check the size of params before calling JSON.stringify in the relay, and drop or replace oversized events with a typed notice.
  • Have the relay forward only the domains the server-side Playwright actually uses.
  • Move the serialization out of the main process, so a fatal error kills a utility process instead of the whole app.

I can share both .ips reports and the matching desktop.trace.ndjson / server.trace.ndjson windows.

Note

Grok responding on behalf of Julius.

This is a bug on current main: the desktop CDP relay serializes every preview event in the Electron main process with no size limit, so one oversized event can take down the whole app.

What main does (apps/desktop/src/preview/CdpRelay.ts, unchanged since nightly 3171 apart from comment cleanup in #19559):

  • DesktopBrowserHost.attach registers a debugger.on("message") listener that passes every event to relay.event.
  • relay.event calls send = (message) => write(JSON.stringify(message)) once per attached page session, for every event, with no size check.
  • The relay enables no CDP domains itself. It forwards everything the server-side Playwright enables (Page, Runtime, Network and so on), so large Runtime/Network payloads from a big Vite dev page all go through this path.

The stack shows JSON.stringify called from JS inside Debugger::Emit, so this is our listener, not an Electron bug. It's also a different failure from #8332 (SIGTRAP in debugger.attach) and #9067 (use-after-free, fixed by #9068).

Which event is oversized is still a guess. If you can share the desktop.trace.ndjson window right before either crash, that would help pin it down.

Synced from GitHub

Thanks for confirming. I pulled the desktop.trace.ndjson windows, but they don't identify the event:

  • The trace has no spans for the CDP relay or individual debugger events, so none of the relayed traffic shows up. The only preview spans are PreviewManager.syncWebContentsState, registerWebview, publishFavicon and emitAgentPointer. All succeed in under 1 ms.
  • Nothing is slow or failing before either crash. The last spans are routine IPC and update polling, 3 to 4 seconds before each abort. That fits a single event-listener call that never returns, rather than load building up over time.
  • server.trace.ndjson has already rotated past Oct 15, so I can't send the Playwright side for these two crashes.

Preview-related spans, local time (-0700). IPC and polling spans are omitted:

Crash 1 (abort at 15:39:58)
text
115:39:11.860 PreviewManager.registerWebview 0.3 ms Success
215:39:12.830 PreviewManager.publishFavicon 0.2 ms Success
315:39:16.936 PreviewManager.emitAgentPointer 0.2 ms Success
415:39:17.097 PreviewManager.emitAgentPointer 0.2 ms Success
515:39:44.757 PreviewManager.syncWebContentsState x15 <0.3 ms Success (through 15:39:44.947)
615:39:48.340 desktop.updates.checkForUpdates 146.3 ms Success
715:39:54.673 last span in the file before the abort (IPC)
Crash 2 (relaunch at 15:40:20, abort at 15:40:41)
text
115:40:28.828 PreviewManager.getBrowserSession 1.6 ms Success
215:40:29.698 PreviewManager.registerWebview 0.2 ms Success
315:40:30.205 PreviewManager.publishFavicon 0.3 ms Success
415:40:30.312 PreviewManager.syncWebContentsState x9 <0.3 ms Success (through 15:40:30.789)
515:40:35.940 desktop.updates.checkForUpdates 773.7 ms Success
615:40:37.938 last span in the file before the abort (IPC)

To find the event, the relay would need to record something before calling JSON.stringify, such as the CDP method name and a rough size of params. Logging after the call doesn't help because the abort happens inside it. If a nightly adds that logging, I can run the same preview_open + preview_evaluate sequence against the page again and report the result. It reproduced within about 20 seconds of launch.

Synced from GitHub

Still happening on a newer build: 3 main-process crashes in about 5 minutes on Nightly 0.1.1-nightly.20261020.3411 (d36b7da2), 20 October 2026. The machine and Electron binary are the same as in this report.

Environment

  • T3 Code Nightly 0.1.1-nightly.20261020.3411, desktop app. This Mac hosts the server, and the user connects from another device, so each crash also takes down the bundled server and every remote thread.
  • Electron Framework 44.4.5, UUID 4C4C44ED-5555-3144-A1CB-427AA45E6F33. Same binary as this issue and #17476.
  • macOS 27.0.1 (26A434), Apple silicon, 32 GB RAM. No memory pressure: swap 0, 68% free, renderer working sets about 300–400 MB.
  • Workload: 5 Claude and Codex threads running long builds in parallel, with agents driving preview tabs through the browser MCP tools (mcp: tab IDs).
  • The user had no crashes on 0.1.1-nightly.20261019.3360. The app updated to 3411 between 14:21 and 14:42 local time, and the first crash came at 15:07. Nothing between 3360 and 3411 touches CdpRelay.ts, so the update may not be the cause. The user's heavy browser use today could also explain the timing.

Crashes

All three .ips reports show EXC_BREAKPOINT (SIGTRAP) on thread 0 CrBrowserMain, brk #0 at the PC, and the same leading native frames at the same offsets. In crashes 2 and 3, six JIT frames sit above v8::Function::Call. That is the same shape as the stack in this issue. I have not symbolicated them.

Timeline (UTC). Only the preview automation calls are shown:

Crash 1Crash 2Crash 3
Session start12:40:0413:07:4113:10:24
Preview tab created / webview registered13:06:4713:08:3113:11:45
Preview calls that timed out beforehandsnapshot 13:04:16, snapshot 13:05:50, evaluate 13:06:20snapshot 13:08:42, snapshot 13:09:05snapshot 13:12:11
Last desktop trace span13:06:47.8813:10:03.5013:12:19.87
Crash (.ips capture time)13:07:0813:10:0813:12:26

The pattern held in all three: the agent opens a tab, snapshot/evaluate time out (PreviewAutomationTimeoutError, 15000 ms), the desktop trace goes quiet for 5–20 s, and the main process aborts. The relaunched session ran agent threads but opened no preview tab, and it has not crashed so far.

No Crashpad minidumps were written, and the unified log has no V8 fatal message. The .ips files are the only native evidence. I can share them.

Related: #16567, which covers the snapshot timeouts that come before each crash.


Filed via t3 triage by Claude Code (Claude Opus 5.5) on behalf of @kestereverts, with their approval.

Synced from GitHub

Still happening on the stable 0.1.5 desktop app (Electron 44.10.0): 3 main-process crashes in 13 minutes on 29 October 2026, each one taking down the bundled server and every running thread. The third crash came 42 seconds after relaunch.

Environment

  • T3 Code (Beta) 0.1.5, desktop app with local server (server also reports 0.1.5).
  • Electron Framework 44.10.0, UUID 4C4C4476-5555-3144-A14E-2916C939B105.
  • macOS 27.0.1 (26A434), Apple silicon (Mac17,14), 36 GB RAM.
  • Workload: several Claude and Codex threads. One Claude thread was driving the built-in browser against a local Vite dev app (http://127.0.0.1:5530/work-orders/<id>, a page that renders to <canvas>) with preview_open, preview_snapshot, preview_act and preview_evaluate.

Crashes

All three .ips reports have the same stack, which I symbolicated with the official electron-v44.10.0-darwin-arm64-dsym.tar.xz (dSYM UUID matches the report). It is the same path as the original report, except that this build dies allocating the result string rather than in Zone::Expand:

text
1Thread 0 CrBrowserMain, EXC_BREAKPOINT (SIGTRAP)
2 0 partition_alloc::internal::OnNoMemoryInternal(unsigned long) oom.cc:85
3 2 partition_alloc::internal::OnNoMemory(unsigned long) oom.cc:100
4 3 partition_alloc::internal::OnErrnoNoMem() oom.cc:106
5 4 (anonymous namespace)::AllowWasmCodeGenerationCallback(...) node_bindings.cc:214 (nearest symbol; the OOM callback)
6 5 v8::Utils::ReportOOMFailure(...) api.cc:330
7 6 v8::internal::V8::FatalProcessOutOfMemory(...) api.cc:289
8 8 v8::internal::FactoryBase<Factory>::NewRawOneByteString(...)
9 9 v8::internal::JsonStringify(...)
1010 v8::internal::Builtin_JsonStringify(...) builtins-json.cc:34
1111-16 <JS frames>
1217 v8::internal::Execution::Call(...)
1318 v8::Function::Call(...) api.cc:5699
1419-20 node::InternalMakeCallback(...)
1521 gin_helper::internal::CallMethodWithArgs(...)
1622 gin_helper::EmitEvent(...)
1723 gin_helper::EventEmitterMixin<electron::api::Debugger>::Emit(...)
1824 electron::api::Debugger::DispatchProtocolMessage(...)
1925 content::DevToolsSession::DispatchProtocolResponseOrNotification(...)
2026 content::DevToolsSession::DispatchProtocolNotification(...)
2127 blink::mojom::DevToolsSessionHostStubDispatch::Accept(...)

So it is again our debugger.on("message") listener calling JSON.stringify on a relayed CDP notification. apps/desktop/src/preview/CdpRelay.ts line 54 (write(JSON.stringify(message))) is unchanged between v0.1.5 and current main (52f4ec6f).

Timeline (local time, -0500)

Crash 1Crash 2Crash 3
App launched09:13:0214:47:4515:00:03
Preview activity before the abortpreview_open of the canvas page 14:44:22preview_open 14:57:19, then preview_snapshot at 14:58:40, 14:58:51, 14:59:14preview_open 15:00:22, preview_evaluate 15:00:29 and 15:00:32 (the second one polls document.querySelectorAll('canvas'))
Last main-process span14:47:5x (routine IPC)14:59:00 (routine IPC, then 29 s of silence)15:00:34 desktop.updates.handleCheckingForUpdate, then 10 s of silence
Crash report14:47:5314:59:2915:00:45

Before each abort the 3-second IPC polling simply stops, so the main thread was busy for 10 to 30 seconds (serializing, then dying) rather than failing fast.

Preview renderer memory (renderer-history.ndjson)

The guest pages were large. Working-set samples for the preview webContents right before each crash:

SessionPreview webContentsWorking set
Crash 1 (main pid 96442)wc 122,667 MB
Crash 1 (main pid 96442)wc 131,244 MB
Crash 3 (main pid 16945)wc 22,134 MB, reached within ~20 s of the tab being created

The main-window renderer stayed between 400 and 660 MB.

Knock-on: the Mac then kernel-panicked

About 15 minutes after crash 3 the machine hit a watchdog timeout: no checkins from watchdogd in 90 seconds panic with 905 free pages (~14 MB), 29 swap files and "LOW swap space". That panic was system-wide memory exhaustion (Safari WebContent, a VM and Chrome dominated the 60 GB resident), and T3's own four processes were about 1 GB at the time, so I am not attributing the panic to this bug. I mention it because the multi-GB preview renderers and the uncapped agent children (#24048) made T3 a contributor, and because it explains why some server-side traces for the crash window are gone.

What would help

Same ask as the original report: cap or drop oversized events in the relay (or stringify incrementally and bail above a byte limit) so one event cannot abort the whole desktop process. If a desktop.trace.ndjson span around relay.event recorded the method and payload size, the next report could name the offending event.


Diagnosis produced by Claude Fable 5.1 (Claude Code) via t3 triage on behalf of the reporter.

Note

🤖 Claude Opus 5.5 responding on behalf of Theo

Closing: this is covered by #23461, which landed. Reopen if an important part is still missing.

Synced from GitHub

Note

🤖 Claude Opus 5.5 responding on behalf of Pasi

#23461 does not cover every crash on this path. On 0.1.7-nightly.20261104.4151, which includes that fix, the app still aborted in JSON.stringify on Page.screencastFrame events, twice, each about 60 seconds into an agent preview recording. These are ordinary frames rather than one oversized message, so the size limit doesn't apply.

Filed as #30735 with the symbolicated stack, timeline and likely cause.