Skip to content

fix(viewer): keep rich parts visible under Chrome 149 field trials#101

Open
kiluazen wants to merge 1 commit into
modem-dev:mainfrom
kiluazen:fix/sandboxed-part-resize-retry-backoff
Open

fix(viewer): keep rich parts visible under Chrome 149 field trials#101
kiluazen wants to merge 1 commit into
modem-dev:mainfrom
kiluazen:fix/sandboxed-part-resize-retry-backoff

Conversation

@kiluazen

@kiluazen kiluazen commented Jun 22, 2026

Copy link
Copy Markdown

Problem

Rich parts (markdown, diff, terminal, mermaid, code, and comments) can render blank or clipped on every reload in affected Chrome 149 field-trial variants. The failure is non-deterministic per frame: the same surface may appear on one refresh and disappear on the next.

Root-cause evidence

Rich parts use sandboxed srcdoc iframes. On an affected Chrome profile, opaque-origin frames defer layout: scrollHeight, offsetHeight, and getBoundingClientRect() return zero when the bridge measures them. Off-screen frames may never produce a later resize, so the viewer leaves them collapsed.

The same 500px document produces different measurements solely from the sandbox mode:

sandbox immediate on load +200ms
allow-scripts (opaque origin) 0 0 500
allow-scripts allow-same-origin 500 500 500

This isolates the browser trigger to opaque-origin srcdoc layout. The exact upstream Chrome experiment is still unidentified; disabling field trials on the same Chrome binary removes the failure. The single delayed reparse from #85 is not deterministic because it starts another layout race.

Fix

  • Render rich/comment srcdoc frames with sandbox="allow-scripts allow-same-origin", avoiding the affected opaque-origin layout path.
  • Generate a fresh 128-bit nonce for every rich document.
  • Replace script-src 'unsafe-inline' with script-src 'nonce-<value>'; only the trusted resize/interaction bridge receives the nonce.
  • Keep rendered part bodies non-executable. Injected scripts and inline event handlers do not have the nonce and are blocked.
  • Move the new code part's copy behavior into the trusted bridge, so code surfaces remain functional without body scripts or inline handlers.
  • Remove fix(viewer): retry srcdoc parse when Chrome field trial breaks iframe layout #85's reparse retry.
  • Leave agent-authored HTML parts at /s/:id unchanged; they remain opaque-origin and intentionally support scripts.
  • Update the existing patch changeset to describe the final behavior.

Security boundary

This deliberately changes rich-frame containment from an opaque origin to a nonce-only script policy because the opaque-origin srcdoc path is the browser trigger. CSP is therefore load-bearing for rich frames.

Behavioral browser coverage now feeds unsanitized <script> and onerror markup directly into a rich document and verifies that:

  • neither payload can mutate the same-origin parent;
  • the trusted nonce-bearing bridge still executes and reports resize;
  • the sandbox attribute remains intact.

The policy still has no connect-src, CDN script source, 'unsafe-inline', or board-origin script source.

Validation

  • npm test — 192/192 pass
  • npm run typecheck — pass (Node, Workers, viewer)
  • npm run lint — pass
  • npm run format:check — pass
  • Relevant Chromium E2E — 24/24 pass
  • Relevant WebKit E2E — 24/24 pass
  • Manual verification on the affected Chrome 149 profile — rich parts remain visible across reloads

Supersedes this PR's earlier retry/backoff implementation.

@kiluazen

Copy link
Copy Markdown
Author

While digging into this on an affected machine (Chrome/149.0.0.0), I isolated why the layout is broken — it's specifically the opaque origin, and the timing is deterministic per sandbox mode. Same content (a known 500px div), two sandboxes, measuring document.body.scrollHeight from the in-frame bridge:

sandbox immediate on load +200ms
allow-scripts (opaque) 0 0 500
allow-scripts allow-same-origin 500 500 500

So the field trial defers layout only for opaque-origin srcdoc iframes; same-origin frames lay out synchronously. That explains why a fixed-time bridge (and the single re-parse retry) miss it intermittently — layout can land after the last timer, and re-parsing just opens a new race window.

This backoff-retry PR is still a strict improvement to the existing workaround, but flagging the root finding in case it informs a more decisive fix (e.g. a non-opaque but still-isolated rendering path, or gating on the trial). Obviously allow-same-origin itself would undo the #65 isolation, so it's not a real fix here — just the diagnostic that pinpoints the trigger.

@kiluazen kiluazen force-pushed the fix/sandboxed-part-resize-retry-backoff branch from 056c8e6 to a5c8d8c Compare June 22, 2026 13:21
@kiluazen kiluazen changed the title fix(viewer): retry sandboxed-part srcdoc reparse on a backoff fix(viewer): render rich parts same-origin with a nonce'd CSP (Chrome 149 blank parts) Jun 22, 2026
@kiluazen kiluazen force-pushed the fix/sandboxed-part-resize-retry-backoff branch from a5c8d8c to 4fcf3d5 Compare June 22, 2026 16:27
@kiluazen kiluazen changed the title fix(viewer): render rich parts same-origin with a nonce'd CSP (Chrome 149 blank parts) fix(viewer): keep rich parts visible under Chrome 149 field trials Jun 22, 2026
Chrome 149 field-trial variants can defer layout in opaque-origin srcdoc frames, leaving rich surfaces blank or clipped even after the existing reparse retry.

Render rich srcdoc frames same-origin to avoid the affected layout path and replace unsafe-inline with a fresh per-document CSP nonce. Only the trusted resize and interaction bridge receives the nonce; rendered part bodies remain non-executable. Move code-part copy behavior into that bridge so code surfaces remain functional under the stricter policy.

Add browser coverage proving injected scripts and handlers stay blocked while the bridge and code-copy path work.
@kiluazen kiluazen force-pushed the fix/sandboxed-part-resize-retry-backoff branch from 4fcf3d5 to 1366d21 Compare June 22, 2026 16:29
@kiluazen kiluazen marked this pull request as ready for review June 22, 2026 16:30
@benvinegar

benvinegar commented Jun 24, 2026

Copy link
Copy Markdown
Member

@kiluazen Thanks for opening this, taking a look.

First reaction: activating allow-same-origin seems like it would break the security model, but maybe I'm missing something.

@benvinegar

Copy link
Copy Markdown
Member

Opened #125 as an alternative that keeps the opaque origin. It loads rich frames from a blob: URL instead of srcdoc — a different load path with the same sandbox="allow-scripts" opaque origin — so it avoids the same-origin trade and leaves the "sandboxed without allow-same-origin… never weaken this" invariant untouched.

The key open question for both PRs: is the bug specific to opaque-origin srcdoc (then blob: fixes it) or to opaque origin in general (then blob: also fails and the same-origin approach here is the justified last resort)? The evidence so far varies only allow-same-origin, which doesn't distinguish the two. Could you reload-test #125 on the affected Chrome 149 profile? If rich parts stay visible there, we get the fix without changing containment.

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