@mochi.js/core
Advanced tools
+1
-1
| { | ||
| "name": "@mochi.js/core", | ||
| "version": "0.9.4", | ||
| "version": "0.9.5", | ||
| "description": "The library for faithful browser automation. Bun-native; relational fingerprint matrix, biomechanical input, stock Chromium-for-Testing.", | ||
@@ -5,0 +5,0 @@ "license": "MIT", |
@@ -383,2 +383,17 @@ /** | ||
| }); | ||
| it("emits the AppArmor user-namespace hint for the canonical 'No usable sandbox' message (issue #52)", () => { | ||
| // The exact stderr emitted by Chromium on Kubuntu 25.10 — captured | ||
| // verbatim in the issue report. | ||
| const tail = | ||
| "[759176:759176:0511/132845.681682:FATAL:content/browser/zygote_host/" + | ||
| "zygote_host_impl_linux.cc:128] No usable sandbox! If you are running on " + | ||
| "Ubuntu 23.10+ or another Linux distro that has disabled unprivileged user " + | ||
| "namespaces with AppArmor, see ..."; | ||
| const hint = diagnoseEarlyExitTail(tail); | ||
| expect(hint).toContain("user-namespace sandbox cannot initialize"); | ||
| expect(hint).toContain("apparmor_restrict_unprivileged_userns"); | ||
| expect(hint).toContain("Install an AppArmor profile"); | ||
| expect(hint).toContain("--no-sandbox"); | ||
| }); | ||
| }); |
+104
-20
@@ -10,3 +10,3 @@ /** | ||
| import { mkdtemp, rm } from "node:fs/promises"; | ||
| import { mkdtemp, readFile, rm } from "node:fs/promises"; | ||
| import { tmpdir } from "node:os"; | ||
@@ -255,30 +255,61 @@ import { join } from "node:path"; | ||
| // Linux + uid 0 (root) + no `--no-sandbox` anywhere → Chromium will refuse | ||
| // to start with the user-namespace sandbox. We auto-inject `--no-sandbox` | ||
| // (with a one-line warning naming the fingerprint trade-off) instead of | ||
| // letting `spawnChromium` crash with `EPIPE`. Users who explicitly want | ||
| // the sandbox under root can either run as a non-root user, `chmod 4755` | ||
| // the chrome-sandbox SUID helper, or pass their own `--no-sandbox` (which | ||
| // we'd see in args and skip this branch). | ||
| // Auto-fallback: --no-sandbox when Chromium's user-namespace sandbox | ||
| // CANNOT initialize. Two host configurations trigger this on Linux: | ||
| // | ||
| // 1. Running as root (uid 0). Chromium refuses to start under root with | ||
| // the user-namespace sandbox enabled. Hits CI containers, Docker | ||
| // defaults, etc. | ||
| // 2. Unprivileged user namespaces blocked by the kernel/AppArmor. This | ||
| // is the default on Ubuntu 23.10+ and Kubuntu 25.10+ (per the | ||
| // `apparmor_restrict_unprivileged_userns` knob) — Chromium emits | ||
| // `FATAL: No usable sandbox!` and exits. Issue #52. ANY non-root | ||
| // user on those distros hits this without intervention. | ||
| // | ||
| // In either case Chromium dies in the first few hundred ms with no CDP | ||
| // pipe ever opened. We inject `--no-sandbox` (with a one-line warning | ||
| // naming the fingerprint trade-off) instead of letting `spawnChromium` | ||
| // crash with `EPIPE`. Users who explicitly want the sandbox can: | ||
| // - run as a non-root user on a distro that allows unprivileged userns, | ||
| // - `chmod 4755` the `chrome-sandbox` SUID helper next to the CfT binary, | ||
| // - on Ubuntu 23.10+, install an AppArmor profile for the Chromium binary | ||
| // (see https://chromium.googlesource.com/chromium/src/+/main/docs/security/apparmor-userns-restrictions.md), | ||
| // - or pass their own `--no-sandbox` in `args` (which we'd see and skip | ||
| // this branch). | ||
| // | ||
| // We DO NOT add `--no-sandbox` to DEFAULT_CHROMIUM_FLAGS (PLAN.md §8.6 | ||
| // explicitly omits it as a fingerprint leak). This is a runtime fallback, | ||
| // not a default — only fires under the specific environment that would | ||
| // otherwise crash. The fingerprint-leak risk is documented in | ||
| // docs/quickstart.md "Linux gotcha — Chromium and root". | ||
| // otherwise crash. | ||
| if ( | ||
| process.platform === "linux" && | ||
| process.getuid?.() === 0 && | ||
| !args.some((a) => a === "--no-sandbox" || a.startsWith("--no-sandbox=")) && | ||
| !cfg.allowRootWithSandbox | ||
| ) { | ||
| console.warn( | ||
| "[mochi] Detected root + Linux + missing --no-sandbox. " + | ||
| "Auto-adding --no-sandbox so Chromium can launch. " + | ||
| "This is a fingerprint leak per PLAN.md §8.6 — run as non-root or " + | ||
| "use the chrome-sandbox SUID helper for stealth-critical workloads. " + | ||
| "See docs/quickstart.md 'Linux gotcha — Chromium and root'. " + | ||
| "Pass `allowRootWithSandbox: true` to mochi.launch() to opt out of this fallback.", | ||
| ); | ||
| args.push("--no-sandbox"); | ||
| const isRoot = process.getuid?.() === 0; | ||
| const usernsBlocked = !isRoot && (await apparmorRestrictsUserns()); | ||
| if (isRoot) { | ||
| console.warn( | ||
| "[mochi] Detected root + Linux + missing --no-sandbox. " + | ||
| "Auto-adding --no-sandbox so Chromium can launch. " + | ||
| "This is a fingerprint leak per PLAN.md §8.6 — run as non-root or " + | ||
| "use the chrome-sandbox SUID helper for stealth-critical workloads. " + | ||
| "See docs/getting-started/linux-server.md 'Linux gotcha — Chromium and root'. " + | ||
| "Pass `allowRootWithSandbox: true` to mochi.launch() to opt out of this fallback.", | ||
| ); | ||
| args.push("--no-sandbox"); | ||
| } else if (usernsBlocked) { | ||
| console.warn( | ||
| "[mochi] Detected AppArmor unprivileged user-namespace restriction " + | ||
| "(/proc/sys/kernel/apparmor_restrict_unprivileged_userns=1). " + | ||
| "Chromium's user-namespace sandbox cannot initialize on this host " + | ||
| "(Ubuntu 23.10+ / Kubuntu 25.10+ default). " + | ||
| "Auto-adding --no-sandbox so Chromium can launch. " + | ||
| "This is a fingerprint leak per PLAN.md §8.6 — for stealth-critical " + | ||
| "workloads, install an AppArmor profile for the Chromium binary " + | ||
| "(https://chromium.googlesource.com/chromium/src/+/main/docs/security/apparmor-userns-restrictions.md) " + | ||
| "or run on a distro without the restriction. " + | ||
| "Pass `allowRootWithSandbox: true` to mochi.launch() to opt out of this fallback.", | ||
| ); | ||
| args.push("--no-sandbox"); | ||
| } | ||
| } | ||
@@ -507,2 +538,31 @@ | ||
| */ | ||
| /** | ||
| * Returns `true` when the host is Ubuntu 23.10+ / Kubuntu 25.10+ (or any | ||
| * Linux with the AppArmor knob enabled), where unprivileged user | ||
| * namespaces are blocked and Chromium's sandbox cannot initialize for | ||
| * non-root processes. | ||
| * | ||
| * Probes `/proc/sys/kernel/apparmor_restrict_unprivileged_userns` — | ||
| * value `1` means blocked. Missing file (older kernels, non-AppArmor | ||
| * distros) returns `false` so we don't false-positive into auto- | ||
| * `--no-sandbox` on systems that don't need it. Issue #52. | ||
| * | ||
| * Side-effect-free, sub-millisecond on Linux. Returns `false` on every | ||
| * non-Linux host. | ||
| * | ||
| * @internal | ||
| */ | ||
| export async function apparmorRestrictsUserns(): Promise<boolean> { | ||
| if (process.platform !== "linux") return false; | ||
| try { | ||
| const buf = await readFile("/proc/sys/kernel/apparmor_restrict_unprivileged_userns", "utf8"); | ||
| return buf.trim() === "1"; | ||
| } catch { | ||
| // ENOENT, EACCES, or anything else — assume not blocked. The | ||
| // post-spawn `diagnoseEarlyExitTail` still catches the "No usable | ||
| // sandbox" stderr pattern as the safety net. | ||
| return false; | ||
| } | ||
| } | ||
| export function diagnoseEarlyExitTail(tail: string): string { | ||
@@ -519,2 +579,26 @@ if (/running.*root.*without.*--no-sandbox|--no-sandbox.*required/i.test(tail)) { | ||
| } | ||
| // Ubuntu 23.10+ / Kubuntu 25.10+ — AppArmor blocks unprivileged user | ||
| // namespaces. Chromium emits "FATAL: ... No usable sandbox!" with a | ||
| // pointer to its own AppArmor docs. If the proactive AppArmor probe in | ||
| // spawnChromium() missed (e.g., AppArmor on without the | ||
| // `apparmor_restrict_unprivileged_userns` sysctl set), surface the | ||
| // remediation here. Issue #52. | ||
| if (/No usable sandbox|unprivileged.+user namespace|apparmor/i.test(tail)) { | ||
| return ( | ||
| "\n\nChromium's user-namespace sandbox cannot initialize on this host.\n" + | ||
| "This is the Ubuntu 23.10+ / Kubuntu 25.10+ default — AppArmor blocks\n" + | ||
| "unprivileged user namespaces. Check the knob with:\n\n" + | ||
| " cat /proc/sys/kernel/apparmor_restrict_unprivileged_userns # 1 = blocked\n\n" + | ||
| "Fixes (preferred → workaround):\n" + | ||
| " 1. Install an AppArmor profile for the Chromium binary —\n" + | ||
| " https://chromium.googlesource.com/chromium/src/+/main/docs/security/apparmor-userns-restrictions.md\n" + | ||
| " 2. Temporarily lift the restriction (system-wide; weakens host security):\n" + | ||
| " sudo sysctl kernel.apparmor_restrict_unprivileged_userns=0\n" + | ||
| " 3. Pass args: ['--no-sandbox'] to mochi.launch() — fingerprint leak\n" + | ||
| " (PLAN §8.6), acceptable for local dev / CI, not stealth-critical prod.\n" + | ||
| " mochi auto-applies this fallback when it detects the AppArmor\n" + | ||
| " restriction; if you saw this error, the detection missed —\n" + | ||
| " please file an issue with `cat /proc/sys/kernel/apparmor_restrict_unprivileged_userns`." | ||
| ); | ||
| } | ||
| const libMatch = /error while loading shared libraries:\s+([^\s:]+)/i.exec(tail); | ||
@@ -521,0 +605,0 @@ if (libMatch !== null) { |
+1
-1
| /** Single source of truth for the @mochi.js/core package version string. */ | ||
| export const VERSION = "0.9.3" as const; | ||
| export const VERSION = "0.9.4" as const; |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
561348
0.98%13672
0.71%