From 196d78ff6dd3a48bbc8a541b331f0c017dfc00d1 Mon Sep 17 00:00:00 2001 From: Chen Yu Date: Thu, 6 Aug 2026 11:06:33 +0800 Subject: [PATCH 1/3] feat(engine): add strict browser sandbox mode --- packages/engine/README.md | 12 +++++ packages/engine/src/index.ts | 9 ++++ .../src/services/browserManager.test.ts | 23 ++++++++ .../engine/src/services/browserManager.ts | 31 ++++++++--- .../src/services/browserSandbox.test.ts | 53 ++++++++++++++++++ .../engine/src/services/browserSandbox.ts | 54 +++++++++++++++++++ packages/engine/src/services/hdrCapture.ts | 13 +++-- 7 files changed, 186 insertions(+), 9 deletions(-) create mode 100644 packages/engine/src/services/browserSandbox.test.ts create mode 100644 packages/engine/src/services/browserSandbox.ts diff --git a/packages/engine/README.md b/packages/engine/README.md index 1adf934d89..1e0a8cd841 100644 --- a/packages/engine/README.md +++ b/packages/engine/README.md @@ -66,6 +66,18 @@ await browserLease.release(); Most users should use `@hyperframes/producer` or the `hyperframes` CLI instead of calling the engine directly. +## Strict Chromium sandbox mode + +HyperFrames keeps its existing browser launch behavior by default for backwards compatibility. A trusted Linux renderer can opt into a fail-closed Chromium process sandbox and site isolation profile with: + +```bash +PRODUCER_BROWSER_SANDBOX_MODE=strict hyperframes render ./composition +``` + +Strict mode refuses to launch on Linux unless HyperFrames can verify that it is running as a non-root user. It also omits `--no-sandbox`, `--disable-setuid-sandbox`, and `--no-zygote`, enables `--site-per-process`, and prevents the normal render engine from disabling site isolation. The host or container must provide a working Chromium sandbox; HyperFrames does not fall back to an unsafe launch when strict mode fails. + +This setting governs browsers launched by the render engine. Auxiliary CLI commands that launch their own browser are outside this contract. + ## Documentation Full documentation: [hyperframes.heygen.com/packages/engine](https://hyperframes.heygen.com/packages/engine) diff --git a/packages/engine/src/index.ts b/packages/engine/src/index.ts index e9c3481438..8b6d9665f3 100644 --- a/packages/engine/src/index.ts +++ b/packages/engine/src/index.ts @@ -89,6 +89,15 @@ export { type CaptureMode, type AcquiredBrowser, } from "./services/browserManager.js"; +export { + BROWSER_SANDBOX_MODE_ENV, + assertBrowserSandboxRuntime, + buildDisabledBrowserFeaturesArg, + getBrowserSandboxLaunchArgs, + getBrowserSandboxProcessArgs, + resolveBrowserSandboxMode, + type BrowserSandboxMode, +} from "./services/browserSandbox.js"; export { augmentProtocolTimeoutError, isProtocolTimeoutError, diff --git a/packages/engine/src/services/browserManager.test.ts b/packages/engine/src/services/browserManager.test.ts index db6e7e4686..0e46b05357 100644 --- a/packages/engine/src/services/browserManager.test.ts +++ b/packages/engine/src/services/browserManager.test.ts @@ -121,6 +121,15 @@ describe("BeginFrame capability probe", () => { describe("buildChromeArgs browser GPU mode", () => { const base = { width: 1920, height: 1080 }; + const originalBrowserSandboxMode = process.env.PRODUCER_BROWSER_SANDBOX_MODE; + + afterEach(() => { + if (originalBrowserSandboxMode === undefined) { + delete process.env.PRODUCER_BROWSER_SANDBOX_MODE; + } else { + process.env.PRODUCER_BROWSER_SANDBOX_MODE = originalBrowserSandboxMode; + } + }); it("uses SwiftShader software GL by default for reproducible local renders", () => { const args = buildChromeArgs(base); @@ -187,6 +196,20 @@ describe("buildChromeArgs browser GPU mode", () => { expect(args).toContain("--use-angle=swiftshader"); expect(args).not.toContain("--use-angle=metal"); }); + + it("uses the Chromium process sandbox and site isolation in strict mode", () => { + process.env.PRODUCER_BROWSER_SANDBOX_MODE = "strict"; + + const args = buildChromeArgs({ ...base, platform: "linux" }); + + expect(args).toContain("--site-per-process"); + expect(args).not.toContain("--no-sandbox"); + expect(args).not.toContain("--disable-setuid-sandbox"); + expect(args).not.toContain("--no-zygote"); + const disabledFeatures = args.find((arg) => arg.startsWith("--disable-features=")); + expect(disabledFeatures).not.toContain("IsolateOrigins"); + expect(disabledFeatures).not.toContain("site-per-process"); + }); }); describe("browser launch capture-mode contract", () => { diff --git a/packages/engine/src/services/browserManager.ts b/packages/engine/src/services/browserManager.ts index 762626eb3c..6a394bc41b 100644 --- a/packages/engine/src/services/browserManager.ts +++ b/packages/engine/src/services/browserManager.ts @@ -11,6 +11,13 @@ import { existsSync, readdirSync } from "fs"; import { join } from "path"; import { homedir } from "os"; import { DEFAULT_CONFIG, type EngineConfig } from "../config.js"; +import { + assertBrowserSandboxRuntime, + buildDisabledBrowserFeaturesArg, + getBrowserSandboxLaunchArgs, + getBrowserSandboxProcessArgs, + resolveBrowserSandboxMode, +} from "./browserSandbox.js"; import { getSystemTotalMb, LOW_MEMORY_TOTAL_MB_THRESHOLD } from "./systemMemory.js"; import { BrowserLeasePool, @@ -449,9 +456,10 @@ async function getPuppeteerOrNull(): Promise { } function getHardwareGpuProbeArgs(platform: NodeJS.Platform): string[] { + const browserSandboxMode = resolveBrowserSandboxMode(); + assertBrowserSandboxRuntime(browserSandboxMode); return [ - "--no-sandbox", - "--disable-setuid-sandbox", + ...getBrowserSandboxLaunchArgs(browserSandboxMode), "--disable-dev-shm-usage", "--enable-webgl", "--ignore-gpu-blocklist", @@ -761,6 +769,8 @@ export function buildChromeArgs( options: BuildChromeArgsOptions, config?: Partial>, ): string[] { + const browserSandboxMode = resolveBrowserSandboxMode(); + assertBrowserSandboxRuntime(browserSandboxMode); const platform = options.platform ?? process.platform; const gpuDisabled = config?.disableGpu ?? DEFAULT_CONFIG.disableGpu; const browserGpuMode = gpuDisabled @@ -771,8 +781,7 @@ export function buildChromeArgs( // appear in Puppeteer's defaults, Playwright, Remotion, and Chrome's own // headless-shell guidance. const chromeArgs = [ - "--no-sandbox", - "--disable-setuid-sandbox", + ...getBrowserSandboxLaunchArgs(browserSandboxMode), "--disable-dev-shm-usage", CANVAS_DRAW_ELEMENT_FEATURE_FLAG, "--enable-webgl", @@ -800,13 +809,23 @@ export function buildChromeArgs( "--disable-domain-reliability", "--disable-print-preview", "--no-pings", - "--no-zygote", + ...getBrowserSandboxProcessArgs(browserSandboxMode), // Memory — scale GPU budget to available system RAM `--force-gpu-mem-available-mb=${getGpuMemBudgetMb()}`, "--disk-cache-size=268435456", ...getLowMemoryFlags(), // Disable features that add overhead - "--disable-features=AudioServiceOutOfProcess,IsolateOrigins,site-per-process,Translate,BackForwardCache,IntensiveWakeUpThrottling", + buildDisabledBrowserFeaturesArg( + [ + "AudioServiceOutOfProcess", + "IsolateOrigins", + "site-per-process", + "Translate", + "BackForwardCache", + "IntensiveWakeUpThrottling", + ], + browserSandboxMode, + ), // Allow AudioContext to start without a user gesture in headless Chrome. // Without this flag, any code path that constructs an AudioContext // (including GSAP tweening an