Skip to content

Keep tabs alive when WebMCP discovery runs on scratch-document pages - #431

Merged
rgarcia merged 2 commits into
mainfrom
hypeship/webmcp-scratch-document-bind
Oct 1, 2026
Merged

rgarcia merged 2 commits into
mainfrom
hypeship/webmcp-scratch-document-bind

Conversation

@rgarcia

@rgarcia rgarcia commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Summary

GET /webmcp/tools can kill the tab (Terminating renderer for bad IPC message, reason 346) on pages that have already read modelContext on a second document in the same frame. Some sandboxing libraries do this at startup when they copy the properties of a document.implementation.createHTMLDocument() document.

The polyfill bridge read window.document.modelContext in every web frame on every listing, before it checked whether the page had a polyfill. On those pages that read was the second bind, and Chromium killed the renderer. The listing still returned 200 {"tools":[]}.

Background

  1. Two processes. Page JS runs in a renderer process (Blink). Chrome's trusted browser process sits above it, and the two talk over Mojo IPC. The browser process treats every renderer as possibly compromised: when a renderer sends IPC that breaks the rules, the browser kills it (bad_message.cc). That's the "Aw, Snap".
  2. Frame vs document. A frame is the top level of a tab or an iframe. The browser process tracks each one as a RenderFrameHost. A document is the DOM loaded in a frame. JS can create extra detached documents with document.implementation.createHTMLDocument(), and those still count against the frame whose script created them.
  3. WebMCP host. document.modelContext is the registry where a page registers tools for agents. The tools have to be visible outside the page, for example through the CDP WebMCP domain, so the registry lives in the browser process. Its browser-side half (ModelContextHost) is stored once per frame.
  4. Lazy bind. Blink creates a document's ModelContext the first time JS reads .modelContext on that document, and creating it sends a bind request to the browser. Later reads of the same document return the same object, so they don't bind again.

The conflict: Blink binds once per document, but the browser accepts one bind per frame. When two documents in one frame read modelContext, the browser kills the renderer.

The exact bind rule hasn't been checked in Chromium source. The behavior is confirmed by repro:

  • Reading the real document's modelContext repeatedly is fine, including from an extension's isolated world.
  • Reading it on a scratch document, then on the real document, kills the renderer.
  • Reading navigator.modelContext never binds.

document.modelContext only exists here because the image launches Chromium with --enable-features=WebMCPTesting,DevToolsWebMCPSupport (server/cmd/wrapper/chromium.go).

sequenceDiagram
    participant Page as sandbox iframe JS (renderer)
    participant Bridge as polyfill_bridge.js (renderer)
    participant Host as browser process: frame's ModelContextHost
    Page->>Page: createHTMLDocument().modelContext
    Page->>Host: bind #1 (scratch document)
    Host-->>Page: ok
    Note over Bridge: client calls GET /webmcp/tools
    Bridge->>Bridge: sync() → nativeContext() → window.document.modelContext
    Bridge->>Host: bind #2 (frame's real document)
    Host-->>Page: duplicate bind → renderer killed (reason 346)
Loading

syncPolyfillTools (server/lib/webmcpclient/polyfill.go) runs the bridge in every web frame, including sandbox iframes. Before this change, sync() called nativeContext() first, and that function reads window.document.modelContext.

Changes

  • Bridge fix (polyfill_bridge.js): sync() checks navigator.modelContext for a polyfill first, and reads document.modelContext only when one exists. polyfill() no longer compares against the native context, since isNative already excludes the native registry.
  • Repro test (TestPlaywrightExecuteAPI/WebMCPScratchDocument): loads a page that reads modelContext on a scratch document, once at top level and once in a srcdoc frame. It lists tools once, then asserts that no new reason-346 kill appears in the Chromium log and that the page kept its JS state. It runs as the last subtest on the shared container, because a killed foreground tab also stalls /playwright/execute.

Not covered

  • A page with a real navigator.modelContext polyfill in the same frame as a scratch-document read can still be killed: the bridge has to read document.modelContext there to register the polyfill's tools.
  • Any other code that reads document.modelContext on these pages triggers the same kill. That includes the page itself and extension content scripts (reproduced with an unpacked extension). This PR only stops the bridge from being that reader. A full fix needs Chromium to accept one bind per document, or the image to stop exposing document.modelContext to every page by default.

Testing

Ran locally against onkernel/chromium-headless:e77bac1:

  • Unpatched image: both WebMCPScratchDocument cases fail with reason 346 after a 200 listing.
  • Patched API binary: all TestPlaywrightExecuteAPI subtests pass, including the existing WebMCPPolyfill.
  • go test ./lib/webmcpclient/ passes.

CI is green on this branch. Only the headless image was tested locally.

The polyfill bridge read document.modelContext in every frame on every
tool listing. On a frame that had already read modelContext on another
document, that read was a second WebMCP host bind and Chromium killed
the renderer. Read it only when the page has a navigator.modelContext
polyfill to bridge, and add an e2e test that lists tools on such pages
and asserts the renderer survives.
@rgarcia
rgarcia force-pushed the hypeship/webmcp-scratch-document-bind branch from fc5a970 to f8de255 Compare October 1, 2026 18:59

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit f8de255. Configure here.

Comment thread server/e2e/e2e_webmcp_scratch_document_test.go Outdated
@rgarcia
rgarcia merged commit 4e46e66 into main Oct 1, 2026
12 checks passed
@rgarcia
rgarcia deleted the hypeship/webmcp-scratch-document-bind branch October 1, 2026 19:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants