diff --git a/src/helpers/sandbox.ts b/src/helpers/sandbox.ts index 4c970fed..1ef4d596 100644 --- a/src/helpers/sandbox.ts +++ b/src/helpers/sandbox.ts @@ -107,6 +107,24 @@ export function createSandboxedExec(config: SandboxConfig): ExecFileFn { const cb: ExecFileCallback = callback ?? (() => {}); + // Provide isolated per-job TMPDIR, HOME, and XDG directories + const sandboxedEnv = { + ...process.env, + ...options.env, + TMPDIR: resolvedTemp, + TEMP: resolvedTemp, + TMP: resolvedTemp, + HOME: resolvedTemp, + XDG_CONFIG_HOME: `${resolvedTemp}/.config`, + XDG_CACHE_HOME: `${resolvedTemp}/.cache`, + XDG_DATA_HOME: `${resolvedTemp}/.local/share`, + }; + + const sandboxedOptions: ExecFileOptions = { + ...options, + env: sandboxedEnv, + }; + if (!runnerPath) { if (isStrict) { const error = new Error( @@ -122,7 +140,7 @@ export function createSandboxedExec(config: SandboxConfig): ExecFileFn { o: ExecFileOptions, f: ExecFileCallback, ) => ChildProcess - )(cmd, args, options, cb); + )(cmd, args, sandboxedOptions, cb); } // Build landlock-runner arguments @@ -147,24 +165,6 @@ export function createSandboxedExec(config: SandboxConfig): ExecFileFn { runnerArgs.push("--", cmd, ...args); - // Provide isolated per-job TMPDIR, HOME, and XDG directories - const sandboxedEnv = { - ...process.env, - ...options.env, - TMPDIR: resolvedTemp, - TEMP: resolvedTemp, - TMP: resolvedTemp, - HOME: resolvedTemp, - XDG_CONFIG_HOME: `${resolvedTemp}/.config`, - XDG_CACHE_HOME: `${resolvedTemp}/.cache`, - XDG_DATA_HOME: `${resolvedTemp}/.local/share`, - }; - - const sandboxedOptions: ExecFileOptions = { - ...options, - env: sandboxedEnv, - }; - return ( execFileOriginal as ( c: string, diff --git a/tests/helpers/sandbox.test.ts b/tests/helpers/sandbox.test.ts index e00cff70..7b686ebf 100644 --- a/tests/helpers/sandbox.test.ts +++ b/tests/helpers/sandbox.test.ts @@ -55,13 +55,18 @@ test("createSandboxedExec sets TMPDIR and HOME in environment", async () => { tempDir, }); - await new Promise((resolve) => { - sandboxedExec("/bin/sh", ["-c", "echo $TMPDIR"], (_err, stdout) => { - expect(stdout.trim()).toBe(tempDir); - resolve(); + const stdout = await new Promise((resolve, reject) => { + sandboxedExec("/bin/sh", ["-c", 'echo "$TMPDIR $HOME"'], (err, out) => { + if (err) { + reject(err); + } else { + resolve(String(out)); + } }); }); + expect(stdout.trim()).toBe(`${tempDir} ${tempDir}`); + rmSync(testDir, { recursive: true, force: true }); }); @@ -95,12 +100,13 @@ test("createSandboxedExec passes options before callback correctly", async () => }); let called = false; - sandboxedExec("echo", ["hi"], { maxBuffer: 1024 }, () => { - called = true; + await new Promise((resolve) => { + sandboxedExec("echo", ["hi"], { maxBuffer: 1024 }, () => { + called = true; + resolve(); + }); }); - // Give child process time if real exec was spawned - await new Promise((r) => setTimeout(r, 50)); expect(called).toBe(true); rmSync(testDir, { recursive: true, force: true });