Skip to content

fix(engine): keep Chrome's sandbox on for local renders where the host supports it - #7625

Merged
jrusso1020 merged 3 commits into
mainfrom
fix/engine-chrome-sandbox
Nov 12, 2026
Merged

jrusso1020 merged 3 commits into
mainfrom
fix/engine-chrome-sandbox

Conversation

@jrusso1020

@jrusso1020 jrusso1020 commented Nov 12, 2026 •

Copy link
Copy Markdown
Collaborator

Local renders now keep Chrome's sandbox on where the OS can provide it (macOS, Windows, and Linux for a non-root user outside a container). Root, container, Lambda and Cloud Run renders keep running without it, as before. If a sandboxed Linux launch still fails, the render warns once and continues without the sandbox. HYPERFRAMES_CHROME_SANDBOX=1|0 overrides the choice.

Claude Opus 5.5

What

buildChromeArgs always passed --no-sandbox --disable-setuid-sandbox. It now adds them only when this host can't sandbox Chrome. Local CLI and Studio users often render projects they did not write, so the sandbox is worth having on their machines.

Why

Hardening for local renders. Turning the sandbox on everywhere would break root, Docker, Lambda and Cloud Run renders, and Linux hosts that restrict unprivileged user namespaces. So the decision is per host, and a launch that fails gets one retry.

Related work

Refs #7623. That issue proposes sandbox-on behind an opt-out env var. This PR takes the host-aware route so existing serverless and container renders keep working. Site-isolation flags are unchanged.

How

chromeSandboxEnabled() in packages/engine/src/services/chromeSandbox.ts:

Host Sandbox
HYPERFRAMES_CHROME_SANDBOX=1 / =0 required (no fallback) / off, whatever the host
macOS, Windows on
Linux, non-root, no container markers on
Linux as root (getuid() === 0) off
AWS_LAMBDA_FUNCTION_NAME or AWS_EXECUTION_ENV off
K_SERVICE, CLOUD_RUN_JOB or KUBERNETES_SERVICE_HOST off
/.dockerenv or /run/.containerenv off
kernel.apparmor_restrict_unprivileged_userns=1 (Ubuntu 23.10+) or kernel.unprivileged_userns_clone=0 off
  • Chrome refuses --no-zygote while the sandbox is on ("Zygote cannot be disabled if sandbox is enabled"). So --no-zygote now ships only with --no-sandbox and --disable-setuid-sandbox, as one set. A test pins this.
  • The two sysctl reads catch Ubuntu 23.10+ before launch. Puppeteer's Chrome has no AppArmor profile and no setuid helper there, so a sandboxed launch can't succeed, and skipping it saves a failed launch per process.
  • Fallback for hosts the checks miss: launchWithChromeSandboxFallback wraps the engine's pooled launch (acquireBrowser: render, Studio thumbnails, audio FX) and the CLI's launchManagedBrowser (validate, layout, snapshot, motion shots, composition frames). On Linux, if a sandboxed launch fails at startup, it retries once without the sandbox and prints one warning. The sandbox stays off for the rest of the process once Chrome names it as the cause, or after a second bare startup exit (one alone may be an unrelated kill). With HYPERFRAMES_CHROME_SANDBOX=1 there is no retry: a refused launch fails. "Fails at startup" means Chrome's sandbox error text, or the TargetCloseError a pipe launch reports when Chrome exits during startup. Timeouts, aborts and other errors are rethrown unchanged.
  • buildChromeArgs accepts sandbox?: boolean for callers and tests that need a fixed answer.
  • Not changed: the GPU-probe and host-ceiling probes, the HDR capture launch, and test fixtures still pass --no-sandbox themselves. The Lambda and Cloud Run images resolve to "off" by env or root, including the Cloud Run Dockerfile's build-time buildChromeArgs call, which runs as root.

Test plan

  • Unit tests: chromeSandbox.test.ts covers each platform, root, Lambda (both vars), Cloud Run service and job, Kubernetes, Docker, Podman, both sysctls and the override. It also pins --no-zygote against the sandbox, and checks the fallback: retry once, warn once, skip the sandbox afterwards, no retry on unrelated errors or on macOS/Windows, and the retry's own error surfaces.
  • sandboxDenied.test.ts (CLI): a closed pipe launch on Linux retries once through launchManagedBrowser with the no-sandbox set. The existing off-macOS case now passes --no-sandbox, so it still pins "no seatbelt relaunch" (unwiring the CLI fallback fails the new case).
  • Mutation probes: flipping the decision fails 4 tests. Dropping --no-zygote from the no-sandbox set fails 1. Removing the retry fails 3. Removing the root check fails 1. Removing the AppArmor sysctl check fails 1.
  • Real render on Linux (Ubuntu 20.04, non-root, Chrome 152 headless shell), 150-frame 1080p composition:
    • default: launched sandboxed (no --no-sandbox, zygote processes present), no warning;
    • user namespaces blocked (nested user namespace with max_user_namespaces=1): Chrome failed with "No usable sandbox!", the render warned once, retried without the sandbox and finished;
    • running as (namespace) root: no sandboxed attempt, no warning.
      All three MP4s are byte-identical. hyperframes snapshot under blocked namespaces took the CLI fallback the same way.
  • Engine and CLI typecheck, oxlint, oxfmt, comment ratchet.
  • Documentation updated (docs/packages/cli.mdx).
  • Comments follow CONTRIBUTING.md "Comments".

— Rames Jusso

🤖 Generated with Claude Code

…t supports it

Chrome launched with --no-sandbox on every host. Keep the sandbox on
macOS, Windows and non-root Linux outside containers; drop it as root,
in Docker/Podman/Kubernetes, on Lambda and Cloud Run, and where Linux
restricts unprivileged user namespaces. --no-zygote now ships only with
--no-sandbox, since Chrome refuses it while sandboxed. A sandboxed Linux
launch that fails at startup retries once without the sandbox and warns.
HYPERFRAMES_CHROME_SANDBOX=1|0 overrides the choice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mintlify

mintlify Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
hyperframes 🟢 Ready View Preview Nov 12, 2026, 10:57 PM

💡 Tip: Enable Automations to automatically generate PRs for you.

The off-macOS closed-pipe case now passes --no-sandbox, so it still
pins "no seatbelt relaunch" rather than the new Linux sandbox retry,
which gets its own case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 267 (base branch 267), smooth 104 of those

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

Quarantined, measured but not gated (8)

Unstable (1)

  • seqplainkeys-none-px-r0-root-z100: tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo false, teleport true / tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true / tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true

@heygengenesis heygengenesis Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Verdict: APPROVE · 0 blocker · 1 important · 2 nits · be8366b

Host table and wiring check out. I ran chrome-headless-shell 157 here: a non-root sandboxed launch works, including --single-process over a pipe (the CLI noListen shape). Fake root, and user namespaces exhausted through max_user_namespaces (no sysctl row covers that), both fail as TargetCloseError, so the retry catches them. Findings are inline.

Scope

Read in full: chromeSandbox.ts and its tests, the browserManager.ts launchBrowser/buildChromeArgs hunks, and cli/src/browser/launch.ts. I checked every buildChromeArgs caller: each one launches through acquireBrowser or launchManagedBrowser, so all of them get the fallback. HDR capture still hardcodes --no-sandbox (unchanged). CI: every workflow on this head has a successful run; the cancelled runs are superseded duplicates. No prior reviews.

— Genesis

timeout: fingerprint.browserTimeoutMs,
protocolTimeout: fingerprint.protocolTimeoutMs,
}),
).catch(async (error: unknown) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: When the unsandboxed retry fails too, the retry's error lands in this catch, but defaults (a few lines down) is built from the original sandboxed args. diagnoseChromeStartup then re-runs the sandboxed shape. On a host where the sandbox is refused and something else is also broken, the appended diagnosis shows the sandbox failure, not the real one. The same applies to withStartupDiagnosis(..., options, ...) in cli/src/browser/launch.ts. Diagnosing with withoutChromeSandbox(args) after a retry, or after the latch is set, would fix it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Deferred to a follow-up, as the author proposed. It only changes the diagnostic text when both launches fail.

…k on clear evidence only

With the override set to 1, a sandboxed launch Chrome refuses now fails
instead of retrying unsandboxed. A bare startup exit (TargetCloseError)
retries that launch without the sandbox but only latches the sandbox off
after a second one, so a single unrelated early kill no longer disables
it for the rest of a long-lived process. Chrome naming the sandbox in its
error still latches at once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@heygengenesis heygengenesis Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Verdict: APPROVE · 2 resolved, 1 deferred (diagnosis nit), 0 new · be8366b → b6f33eb · CI green on this head.

— Genesis

@jrusso1020
jrusso1020 added this pull request to the merge queue Nov 12, 2026
Merged via the queue into main with commit 400a4e2 Nov 12, 2026
141 of 142 checks passed
@jrusso1020
jrusso1020 deleted the fix/engine-chrome-sandbox branch November 12, 2026 23:31
@miguel-heygen miguel-heygen mentioned this pull request Nov 13, 2026
3 of 5 tasks

This branch was successfully deployed

1 active deployment
staging - docs — b6f33eb0 Deployed Nov 12, 2026 by mintlify[bot]
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.

1 participant