fix(browse): enable Chromium sandbox on headed launchPersistentContext

Mirrors v1.40.0.1 from main lineage (PR #1617). Cherry-picked onto
gbrowser-anti-detection so the GBrowser submodule can consume the fix
without waiting for main to merge.

Playwright auto-adds --no-sandbox whenever chromiumSandbox !== true
(playwright-core/lib/server/chromium/chromium.js:291-292). The headless
chromium.launch() site set the option; the two headed sites
(launchHeaded() and handoff()) did not. Every headed launch on macOS
and Linux showed Chromium's yellow "unsupported command-line flag:
--no-sandbox" infobar.

shouldEnableChromiumSandbox() centralizes the Win32 / CI / CONTAINER /
root heuristic that previously lived only in the headless path's
explicit --no-sandbox push at :225. All three launch sites now use the
helper, and six unit tests pin the policy across darwin, linux, win32,
CI, CONTAINER, and root.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-05-19 20:39:21 -07:00
parent 1d03d5a2e9
commit ec7d88e424
2 changed files with 111 additions and 3 deletions
+35 -2
View File
@@ -40,6 +40,29 @@ export function isCustomChromium(): boolean {
return p.includes('GBrowser') || p.includes('gbrowser');
}
/**
* Decide whether Playwright should request Chromium's sandbox.
*
* Returns false on Windows (Bun→Node→Chromium chain breaks the sandbox,
* GitHub #276) and on Linux under root / CI / container (sandbox needs
* unprivileged user namespaces, which are missing for root and typically
* disabled in containers).
*
* When false, Playwright auto-adds --no-sandbox to the launch args — the
* desired behavior in those environments. When true, Playwright does NOT
* add --no-sandbox, which keeps Chromium's "unsupported command-line flag"
* yellow infobar from appearing on every headed launch.
*
* The headless launch path also pushes an explicit '--no-sandbox' into args
* when CI/CONTAINER/root is set; that push is now defensively redundant
* (Playwright will add it anyway when this returns false) and harmless.
*/
export function shouldEnableChromiumSandbox(): boolean {
if (process.platform === 'win32') return false;
const isRoot = typeof process.getuid === 'function' && process.getuid() === 0;
return !(process.env.CI || process.env.CONTAINER || isRoot);
}
export type { RefEntry };
// Re-export TabSession for consumers
@@ -240,8 +263,10 @@ export class BrowserManager {
headless: useHeadless,
// On Windows, Chromium's sandbox fails when the server is spawned through
// the Bun→Node process chain (GitHub #276). Disable it — local daemon
// browsing user-specified URLs has marginal sandbox benefit.
chromiumSandbox: process.platform !== 'win32',
// browsing user-specified URLs has marginal sandbox benefit. Also disabled
// on Linux root/CI/container, where the sandbox requires unprivileged user
// namespaces that aren't available.
chromiumSandbox: shouldEnableChromiumSandbox(),
...(launchArgs.length > 0 ? { args: launchArgs } : {}),
...(this.proxyConfig ? { proxy: this.proxyConfig } : {}),
});
@@ -435,6 +460,10 @@ export class BrowserManager {
const { STEALTH_IGNORE_DEFAULT_ARGS } = await import('./stealth');
this.context = await chromium.launchPersistentContext(userDataDir, {
headless: false,
// Match the sandbox policy used by launch() above. Without this,
// Playwright auto-adds --no-sandbox on every headed launch and the user
// sees Chromium's "unsupported command-line flag" yellow infobar.
chromiumSandbox: shouldEnableChromiumSandbox(),
args: launchArgs,
viewport: null, // Use browser's default viewport (real window size)
userAgent: this.customUserAgent || customUA,
@@ -1324,6 +1353,10 @@ export class BrowserManager {
const { STEALTH_IGNORE_DEFAULT_ARGS } = await import('./stealth');
newContext = await chromium.launchPersistentContext(userDataDir, {
headless: false,
// Match the sandbox policy used by launchHeaded() / launch(). The
// handoff path is the headless→headed re-launch and shares the same
// anti-detection posture, including no spurious --no-sandbox infobar.
chromiumSandbox: shouldEnableChromiumSandbox(),
args: launchArgs,
viewport: null,
...(this.proxyConfig ? { proxy: this.proxyConfig } : {}),
+76 -1
View File
@@ -1,4 +1,4 @@
import { describe, it, expect } from 'bun:test';
import { afterEach, beforeEach, describe, it, expect } from 'bun:test';
// ─── BrowserManager basic unit tests ─────────────────────────────
@@ -15,3 +15,78 @@ describe('BrowserManager defaults', () => {
expect(bm.getRefMap()).toEqual([]);
});
});
// ─── shouldEnableChromiumSandbox ─────────────────────────────────
//
// Pinning this is what prevents the "--no-sandbox" yellow infobar from
// regressing on headed launches. Playwright auto-adds --no-sandbox when
// chromiumSandbox !== true (playwright-core chromium.js:291-292), so all
// three launch sites in browser-manager.ts must pass the policy this
// helper computes.
describe('shouldEnableChromiumSandbox', () => {
const origPlatform = process.platform;
const origCI = process.env.CI;
const origContainer = process.env.CONTAINER;
const origGetuid = process.getuid;
beforeEach(() => {
delete process.env.CI;
delete process.env.CONTAINER;
});
afterEach(() => {
Object.defineProperty(process, 'platform', { value: origPlatform });
if (origCI === undefined) delete process.env.CI; else process.env.CI = origCI;
if (origContainer === undefined) delete process.env.CONTAINER; else process.env.CONTAINER = origContainer;
process.getuid = origGetuid;
});
function setPlatform(p: NodeJS.Platform) {
Object.defineProperty(process, 'platform', { value: p });
}
it('darwin, no CI/CONTAINER/root → true', async () => {
setPlatform('darwin');
process.getuid = (() => 501) as typeof process.getuid;
const { shouldEnableChromiumSandbox } = await import('../src/browser-manager');
expect(shouldEnableChromiumSandbox()).toBe(true);
});
it('linux, no CI/CONTAINER/root → true', async () => {
setPlatform('linux');
process.getuid = (() => 1000) as typeof process.getuid;
const { shouldEnableChromiumSandbox } = await import('../src/browser-manager');
expect(shouldEnableChromiumSandbox()).toBe(true);
});
it('win32 → false (sandbox fails in Bun→Node→Chromium chain)', async () => {
setPlatform('win32');
process.getuid = (() => 1000) as typeof process.getuid;
const { shouldEnableChromiumSandbox } = await import('../src/browser-manager');
expect(shouldEnableChromiumSandbox()).toBe(false);
});
it('linux + CI=1 → false', async () => {
setPlatform('linux');
process.env.CI = '1';
process.getuid = (() => 1000) as typeof process.getuid;
const { shouldEnableChromiumSandbox } = await import('../src/browser-manager');
expect(shouldEnableChromiumSandbox()).toBe(false);
});
it('linux + CONTAINER=1 → false', async () => {
setPlatform('linux');
process.env.CONTAINER = '1';
process.getuid = (() => 1000) as typeof process.getuid;
const { shouldEnableChromiumSandbox } = await import('../src/browser-manager');
expect(shouldEnableChromiumSandbox()).toBe(false);
});
it('linux + root (uid 0) → false', async () => {
setPlatform('linux');
process.getuid = (() => 0) as typeof process.getuid;
const { shouldEnableChromiumSandbox } = await import('../src/browser-manager');
expect(shouldEnableChromiumSandbox()).toBe(false);
});
});