From 73836d9c8f6f7d2c5b34006fe073927f2268f01c Mon Sep 17 00:00:00 2001 From: Colin McDonnell Date: Tue, 24 Feb 2026 14:39:39 +0000 Subject: [PATCH] harden proc isolation and filter cross-fork check suite PRs shell sandbox: double-umount + remount /proc to prevent exfiltration when agent peels off --mount-proc overlay. webhooks: filter check_suite.pull_requests to same-repo PRs only (excludes cross-fork sync PRs) and skip merged/closed PRs. Co-authored-by: Cursor --- entry | 6 +++--- mcp/shell.ts | 22 +++++++++++----------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/entry b/entry index 753f504..4079762 100755 --- a/entry +++ b/entry @@ -143955,13 +143955,14 @@ function detectSandboxMethod() { log.info("PID namespace isolation not available - falling back to env filtering only"); return "none"; } +var PROC_CLEANUP = "umount /proc 2>/dev/null; umount /proc 2>/dev/null; mount -t proc proc /proc 2>/dev/null;"; function spawnShell(params) { const spawnOpts = { env: params.env, cwd: params.cwd, stdio: params.stdio, detached: true }; const sandboxMethod = detectSandboxMethod(); if (sandboxMethod === "unshare") { return spawn2( "unshare", - ["--pid", "--fork", "--mount-proc", "bash", "-c", params.command], + ["--pid", "--fork", "--mount-proc", "bash", "-c", `${PROC_CLEANUP} ${params.command}`], spawnOpts ); } @@ -143983,10 +143984,9 @@ function spawnShell(params) { "--mount-proc", "bash", "-c", - params.command + `${PROC_CLEANUP} ${params.command}` ], { ...spawnOpts, env: {} } - // empty env since we pass via sudo env ); } return spawn2("bash", ["-c", params.command], spawnOpts); diff --git a/mcp/shell.ts b/mcp/shell.ts index abfbff0..7fbb4c8 100644 --- a/mcp/shell.ts +++ b/mcp/shell.ts @@ -82,27 +82,27 @@ function detectSandboxMethod(): SandboxMethod { return "none"; } +// strip inherited proc mount that sits underneath --mount-proc's overlay. +// --mount-proc mounts fresh proc on top, but `umount /proc` peels it off and exposes the +// host's proc with all host PIDs — allowing /proc//environ exfiltration. +// double-umount removes both layers, then a clean mount gives only sandbox PIDs. +// on unprivileged systems where umount fails, --mount-proc still provides isolation +// (the agent also can't umount in that case). +const PROC_CLEANUP = "umount /proc 2>/dev/null; umount /proc 2>/dev/null; mount -t proc proc /proc 2>/dev/null;"; + function spawnShell(params: SpawnParams): ChildProcess { const spawnOpts = { env: params.env, cwd: params.cwd, stdio: params.stdio, detached: true }; const sandboxMethod = detectSandboxMethod(); if (sandboxMethod === "unshare") { - // use PID namespace isolation to prevent reading /proc/$PPID/environ - // this creates a new PID namespace where: - // 1. the subprocess becomes PID 1 in its namespace - // 2. parent PIDs are not visible (PPID = 0) - // 3. fresh /proc is mounted showing only sandbox PIDs - // combined with resolveEnv("restricted"), this prevents all /proc-based secret theft return spawn( "unshare", - ["--pid", "--fork", "--mount-proc", "bash", "-c", params.command], + ["--pid", "--fork", "--mount-proc", "bash", "-c", `${PROC_CLEANUP} ${params.command}`], spawnOpts ); } if (sandboxMethod === "sudo-unshare") { - // on GHA runners, unprivileged namespaces are blocked but sudo works - // pass filtered env via sudo env command since sudo clears environment const envArgs: string[] = []; for (const [k, v] of Object.entries(params.env)) { if (v !== undefined) { @@ -120,9 +120,9 @@ function spawnShell(params: SpawnParams): ChildProcess { "--mount-proc", "bash", "-c", - params.command, + `${PROC_CLEANUP} ${params.command}`, ], - { ...spawnOpts, env: {} } // empty env since we pass via sudo env + { ...spawnOpts, env: {} } ); }