diff --git a/packages/sandbox/src/violation.ts b/packages/sandbox/src/violation.ts index 9f4a724..d01909e 100644 --- a/packages/sandbox/src/violation.ts +++ b/packages/sandbox/src/violation.ts @@ -42,7 +42,7 @@ const SPAWN_EXEC_PATH = /spawn(?:Sync)?\s+(\/\S+)\s+(?:EPERM|EACCES)/; // "/bin/sh: : : Permission denied" (lineno optional across shells). Two // linear tests (detect + extract), mirroring the SH_EXEC split, to stay ReDoS-safe. const LINUX_EXEC_PERM = /[Pp]ermission denied/; -const LINUX_EXEC_PATH = /(?:^|[/\s])(?:sh|bash|dash|zsh): (?:\d+: )?(\/[^:\n]+):/; +const LINUX_EXEC_PATH = /(?:^|[/\s])(?:sh|bash|dash|zsh): (?:(?:line )?\d+: )?(\/[^:\n]+):/; function firstMatchingLine(stderr: string, re: RegExp): string | null { for (const line of stderr.split(/\r?\n/)) if (re.test(line)) return line.trim(); diff --git a/packages/sandbox/test/violation.test.ts b/packages/sandbox/test/violation.test.ts index 4158ba6..938400a 100644 --- a/packages/sandbox/test/violation.test.ts +++ b/packages/sandbox/test/violation.test.ts @@ -265,6 +265,16 @@ const LL_DS = { const failLL = (stderr: string) => ({ exitCode: 126, stdout: "", stderr }); describe("classifyViolation — Linux Landlock floor mode (Phase 2)", () => { + test("Bash line-number denials identify masked processes and keep allowed exec failures ambient", () => { + for (const denySet of [LINUX_DS, LL_DS]) { + const denied = classifyViolation(failLL("/bin/sh: line 1: /usr/bin/curl: Permission denied"), denySet); + assert.equal(denied?.kind, "process"); + assert.equal(denied?.confidence, "confirmed"); + assert.equal(denied?.target, "/usr/bin/curl"); + assert.equal(classifyViolation(failLL("/bin/sh: line 1: /usr/bin/make: Permission denied"), denySet), null); + } + }); + test("a floor-OUTSIDE exec denial (dropped /tmp binary) is confirmed exec-floor-deny", () => { const v = classifyViolation(failLL("/bin/sh: 1: /tmp/spikestash/payload: Permission denied"), LL_DS); assert.equal(v?.kind, "process");