Repository navigation
Closed
Labels
bugSomething isn't workingSomething isn't working
Description
Test I ran
node child_process.ts
slice run child_process.ts --allow-exec=/bin/sh,/bin/cat,echo,exit,/nonexistent/bin/xyzchild_process.ts
import * as cp from "node:child_process";
const show = async (k: string, f: () => unknown) => { let v; try { v = await Promise.race([Promise.resolve().then(f), new Promise((r) => setTimeout(() => r("NEVER SETTLED (2000 ms)"), 2000))]); } catch (e: any) { v = "THROWS " + e.name + ": " + String(e.message).slice(0, 100); } console.log(k.padEnd(52) + JSON.stringify(v)); };
(async () => {
await show("cp.execSync('echo $((1+2))')", () => cp.execSync("echo $((1+2))").toString());
await show("cp.execSync('echo a && echo b')", () => cp.execSync("echo a && echo b").toString());
await show("cp.execSync('echo hi | tr a-z A-Z')", () => cp.execSync("echo hi | tr a-z A-Z").toString());
await show("cp.execSync('exit 4') error.status", () => { try { cp.execSync("exit 4", { stdio: "pipe" }); return "no throw"; } catch (e: any) { return "status=" + e.status; } });
await show("cp.execFileSync('/bin/sh',['-c','exit 4']) status", () => { try { cp.execFileSync("/bin/sh", ["-c", "exit 4"], { stdio: "pipe" }); return "no throw"; } catch (e: any) { return "status=" + e.status; } });
await show("cp.spawn('/bin/cat') stdin -> stdout", () => new Promise((res, rej) => { const c = cp.spawn("/bin/cat"); let o = ""; c.stdout.on("data", (d) => (o += d)); c.on("error", rej); c.on("close", (code) => res([code, o])); c.stdin.write("to-"); c.stdin.end("cat"); }));
await show("cp.spawn(missing binary) -> 'error' code", () => new Promise((res) => { const c = cp.spawn("/nonexistent/bin/xyz"); c.on("error", (e: any) => res("error code=" + e.code)); c.on("close", (code) => res("close code=" + code + " (no 'error' event)")); }));
await show("cp.spawnSync(missing binary).error.code", () => { const r = cp.spawnSync("/nonexistent/bin/xyz"); return r.error ? (r.error as any).code : "no .error; status=" + r.status; });
})();Result
Node v24.22.2:
cp.execSync('echo $((1+2))') "3\n"
cp.execSync('echo a && echo b') "a\nb\n"
cp.execSync('echo hi | tr a-z A-Z') "HI\n"
cp.execSync('exit 4') error.status "status=4"
cp.execFileSync('/bin/sh',['-c','exit 4']) status "status=4"
cp.spawn('/bin/cat') stdin -> stdout [0,"to-cat"]
cp.spawn(missing binary) -> 'error' code "error code=ENOENT"
cp.spawnSync(missing binary).error.code "ENOENT"
Slice v0.10.7:
cp.execSync('echo $((1+2))') "$((1+2))\n"
cp.execSync('echo a && echo b') "a && echo b\n"
cp.execSync('echo hi | tr a-z A-Z') "hi | tr a-z A-Z\n"
cp.execSync('exit 4') error.status "status=undefined"
cp.execFileSync('/bin/sh',['-c','exit 4']) status "status=4"
cp.spawn('/bin/cat') stdin -> stdout [0,""]
cp.spawn(missing binary) -> 'error' code "error code=undefined"
cp.spawnSync(missing binary).error.code "THROWS Error: Error: spawn /nonexistent/bin/xyz ENOENT (No such file or directory (os error 2))"
What is wrong
exec/execSyncdo not go through a shell. The command string is split on whitespace and the first word is executed with the rest as literal arguments, soecho a && echo bprintsa && echo b, pipes and$(( ))are passed through as text, andexit 4is not a shell builtin. In Node these two functions are defined as "spawn a shell and run the command in it". If refusing a shell is a deliberateExecCapdecision (a shell would defeat the binary allow-list), the call should throw on shell syntax or the behaviour should be documented; returning different output silently is the hazardous part.spawn(): data written tochild.stdinnever reaches the child (/bin/catexits 0 with empty output).spawnSync(..., { input })works.- Error shape:
execSyncfailure has no.status;spawn()of a missing binary emits'error'without.code(ENOENT);spawnSync()of a missing binary throws instead of returning{ error }.
execFileSync, spawnSync (status, stdout, stderr, input), spawn stdout/stderr/exit code, the execFile callback, and the env and cwd options all matched Node in the same run.
Environment
- Slice
v0.10.7, commitea7c70c, built withcargo +1.96.0 build --release --locked --bin slice - Reference: Node.js
v24.22.2(same script, same machine) - Ubuntu 26.04, Linux 7.0, x86_64
- The same output is produced by the
v0.10.5tag, so this is not a regression in 0.10.6/0.10.7.
Metadata
Metadata
Assignees
Labels
bugSomething isn't workingSomething isn't working
Activity
siktec-lab commentedon Nov 11, 2026
Addressed in #66 (
v0.10.10):exec,execSync, andspawn/spawnSyncwith{ shell: true }now execute commands through/bin/sh -c(supporting&&,||,;, pipelines|, and propagating non-zero exit codes viaerr.status) while enforcing Ring-1 OCapExecCapchecks.spawn()now buffers and pipescp.stdin.write()/cp.stdin.end()binary and UTF-8 payloads to the child process stdin.spawnSyncreturns{ status: null, error }witherror.code = ENOENTwhen the target binary does not exist, andspawnasynchronously emits'error'witherr.code = ENOENT.