process.execArgv: stop at the "-" stdin marker and "--" like a script name - #38577
process.execArgv: stop at the "-" stdin marker and "--" like a script name#38577robobun wants to merge 4 commits into
Conversation
… name create_exec_argv re-parses argv and treated every token starting with "-" as an exec arg, so a script started as `bun run - a b` reported execArgv ["-"] (plus every later dash-prefixed user arg), and child_process.fork() from such a script launched the child with "-" in front of the module path, running stdin instead of the module. The CLI parses a bare "-" as the script positional (stdin) and "--" as "the next token is the script", so both now end execArgv, unless they are the value of an option that consumes the next token (`--conditions -`), which still counts as an exec arg. The value-consuming set now only contains options that actually take the following token (One/Many); optional-value options such as -c and --inspect never do, so `bun -c app.js` no longer reports app.js in execArgv.
|
Status: reproduced on the released binary ( |
|
Warning Review limit reached
Next review available in: 4 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (1)
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (3)
WalkthroughChangesNode execArgv parsing
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
There was a problem hiding this comment.
LGTM — focused fix that makes create_exec_argv's re-parse mirror the CLI parser's actual token rules.
What was reviewed:
- Verified against
src/clap/streaming.rs: bare-/--are classifiedPositional(line 319), andOneOptionalparams never pull the next token whileOne/Manydo — the newCONSUMES_NEXT_ARGpredicate matches. - Checked the
awaiting_value-first ordering doesn't regress the existing fixtures (-e bruh, value-shaped tokens after a value-taking flag) and incidentally fixes-e run. - Test additions cover the variant matrix (
run -, auto-,--, node-mode--,-/--as option values,-c) plus the end-to-endfork()failure that motivated it.
Extended reasoning...
Overview
This PR fixes process.execArgv for three related cases in the argv re-parse loop in src/runtime/node/node_process.rs::create_exec_argv: a bare - (stdin script marker) and -- were being pushed as exec args instead of terminating the list like a script name, and OneOptional params (-c/--config, --inspect) were incorrectly treated as consuming the next token. Two test files gain coverage: a 10-row expansion of the process.execArgv fixture table, and a new fork() regression test that exercises the user-visible symptom (fork() from a bun run - parent launching the child with - in front of the module path).
Security risks
None. This is pure argv-token classification for a Node-compat property; no external input, no privilege boundary, no allocation on untrusted sizes.
Level of scrutiny
Medium. process.execArgv feeds child_process.fork() and new Worker({execArgv}) defaults, so a wrong value silently breaks child launches — but the change is contained to one ~40-line loop and makes it more faithful to the CLI parser it is emulating. I cross-checked each claim against src/clap/streaming.rs: parse_next_arg classifies both - and -- as ArgKind::Positional; the long/short arms consume iter.next() only for One/Many, never OneOptional. The new awaiting_value state machine mirrors that exactly, and moving the value-consume check before the --prefix check is required for --conditions - / --conditions -- to round-trip (which the CLI parser accepts unconditionally via iter.next()).
Other factors
- The three pre-existing fixtures still produce the same result under the new loop (traced by hand); the new ordering also fixes an unlisted edge case where
-e runused to lose its value to theseen_runbranch. - The
Valuesenum has exactly four variants (None/One/Many/OneOptional), so thematches!covers the intended set with no gaps. NODE_SHORT_ALIASEShandling is unchanged and still correct (the-pe → -palias inherits value-taking from-p).- Test structure follows repo conventions (
tempDir, drained pipes, singletoEqualon the whole fixture map for a useful diff on failure). The stdin-redirect trick (< ${script}) lets the same fixture file serve bothindex.tsand-positions without duplication. - The PR notes the overlap with #34658; whichever lands second is a small rebase of this loop.
|
This PR may be a duplicate of:
🤖 Generated with Claude Code |
|
Checked the three against this branch (fetched each PR head and read the
So the bare |
There was a problem hiding this comment.
I reviewed this PR and didn't find any bugs — the awaiting_value state machine correctly mirrors src/clap/streaming.rs (verified that One/Many params unconditionally consume the next token while OneOptional returns early), and the fixture table covers the -/--/-c/--conditions run cases. A human look would still help for two reasons: comment-cop has three unaddressed inline flags on the new multi-line comments, and there are three overlapping open PRs (#34658, #34654, #35779) touching the same loop where a maintainer should pick the merge order.
What was reviewed
- Traced every new fixture row through the rewritten loop; all match, including
--conditions run(previously mis-setseen_run) and--flag=value(not in the set, soawaiting_valuestays false). - Confirmed against
src/clap/streaming.rs:120-148thatOneOptionalnever pulls the next token andOne/Manypull it unconditionally regardless of spelling. - Checked the fork()-from-stdin test follows harness conventions (
tempDir, drains all pipes, asserts combined object before exitCode).
Extended reasoning...
Overview
The PR rewrites the argv re-parsing loop in create_exec_argv (src/runtime/node/node_process.rs) so process.execArgv stops at a bare - (stdin script) or -- the same way it stops at a script name, and narrows the "consumes next token" set from takes_value != None to One | Many so OneOptional params (-c, --inspect) no longer swallow the following token. State tracking moves from prev: Option<&[u8]> to awaiting_value: bool, which also fixes --conditions run (previously the run branch fired before the value-consuming check). Tests add 11 new fixture rows to the execArgv table and an end-to-end fork() from a stdin-piped parent.
Security risks
None. This is read-only reconstruction of a string array from process argv; no unsafe blocks touched, no allocation ownership changes, no user-controlled parsing beyond what the CLI already accepted.
Level of scrutiny
Medium — Node-compat behavior change on a rarely-read property, but with a concrete downstream consumer (child_process.fork()). The loop is ~25 lines of straightforward token classification; I verified it against the actual clap streaming parser semantics rather than trusting the description. The fixture table is exhaustive for the cases the description lists.
Other factors
- comment-cop: three inline bot flags on the new multi-line comments (lines ~322, ~343, ~371). The comments explain non-obvious cross-module semantics (why
OneOptionalis excluded, why-/--terminate) rather than justifying a workaround, so they may be false positives against the "one line" rule — but they're unaddressed and the repo's REVIEW.md does say "One line." - Overlapping PRs: #34658, #34654, and #35779 all touch this loop with partially-duplicate fixes. The author has already analyzed the overlap in a follow-up comment (none of them handles bare
-), but a maintainer should decide merge order. - Tests: hermetic, assert exact values, drain all pipes concurrently, use
tempDir. The fixture-table refactor toPromise.all+Object.fromEntrieskeeps per-row failure messages keyed by the original command string.
There was a problem hiding this comment.
LGTM — focused fix to the create_exec_argv re-parse loop with thorough fixture coverage.
What was reviewed:
- Verified against
src/clap/streaming.rs:OneOptionalparams never pull the next token (line 120-130),One/Manypull it unconditionally (line 138), and bare-/--are classified asArgKind::Positional(line 319) — the new loop mirrors the real parser exactly. - Traced every fixture row (including the three pre-existing ones) through the new
awaiting_valuestate machine; the ordering (awaiting-value →-/--break → flag push →runskip) is correct for--conditions -,--conditions run, and-c script. - The fork()-from-stdin test drains stdout/stderr/exited concurrently and asserts output before exit code; ordering is deterministic since child inherits the parent's stdout fd.
Extended reasoning...
Overview
This PR fixes process.execArgv in src/runtime/node/node_process.rs (create_exec_argv) to stop treating bare - (stdin script marker) and -- (positional terminator) as exec args, and to stop treating OneOptional params (-c, --inspect) as consuming the next token. The loop is refactored from a trailing prev-lookup to a leading awaiting_value flag, which also fixes the case where an option's value is spelled -, --, or run. Two test files gain coverage: 11 new rows in the process.execArgv fixture table and one fork()-from-stdin integration test.
Security risks
None. The change only affects what process.execArgv reports (a read-only informational array). No parsing of untrusted input, no auth/crypto/permissions, no memory-unsafe code.
Level of scrutiny
Moderate. This is Node-compat behavior with a downstream consumer contract (fork() and new Worker() prepend execArgv to the child's argv), so getting it wrong breaks child-process spawning. I verified the three semantic claims the PR relies on directly against src/clap/streaming.rs: (1) parse_next_arg returns ArgKind::Positional for both - and --; (2) the OneOptional branch returns without calling iter.next(); (3) the One/Many branch calls iter.next() unconditionally regardless of what the next token looks like. All three match the new re-parse loop's rules exactly, so bun <execArgv> <script> <argv> round-trips.
Other factors
- The
CONSUMES_NEXT_ARGstatic was hoisted out of the loop body, but as aLazyLockstatic this is purely cosmetic. TheValues::One | Values::Manypredicate correctly excludesNoneandOneOptional(the enum has exactly four variants). - Not CODEOWNER-covered. All comment-cop bot feedback is resolved (comments were shortened in 99b81db). No outstanding human reviewer comments.
- Three overlapping open PRs (#34658, #34654, #35779) touch the same loop; the author has documented the overlap and none of them handles the bare
-case. That's a merge-order coordination question for the maintainer, not a correctness concern for this change. - Test quality: fixtures now run in parallel via
Promise.all, allcmdkeys are unique (noObject.fromEntriescollisions), stdin is piped to every fixture so-cases execute the same script, and the fork test usestempDir/await usingper harness conventions.
Problem
bun run - a b(script on stdin) reportsprocess.execArgvas["-"]; with flags after the marker,bun --no-warnings run - -x --fooreports["--no-warnings", "-", "-x", "--foo"]. Node prints[]and["--no-warnings"]: the-is the script-name slot, and everything after it isprocess.argv.process.argvis already right (["-", "a", "b"]), onlyexecArgvis wrong.child_process.fork()from a stdin script is broken.fork()putsprocess.execArgvin front of the module path, so the child is launched asbun - ./child.js x, reads its (empty) stdin as the script and exits 1 witherror: Module not found '<cwd>/[stdin]'.bun --smol -- app.jsreports["--smol", "--"](node drops the--), andbun -c app.jsreports["-c", "<path>/app.js"]because the set of "options that consume the next token" also contained the optional-value options (-c/--config,--inspect*), which never consume it (bun -c app.jsrunsapp.js).create_exec_argvinsrc/runtime/node/node_process.rsre-parses argv and pushed every token starting with-as an exec arg before reaching its "this is the script name, stop" branch. A bare-and--start with-but are not flags.Fix
--conditions -still reports["--conditions", "-"]); otherwise a bare-or--ends execArgv exactly like a script name; otherwise a--prefixed token is an exec arg.takes_valueisOneorMany;OneOptionalones are left out because the parser never pulls the next token for them.bun <execArgv> <script> <args>must reproduce the current process's options (that is whatfork()andnew Worker()build).src/clap/streaming.rstreats-and--as positionals, so they belong to the script slot, and a value-consuming option takes the next token unconditionally, so that token has to stay in execArgv. The result matches node for every case node accepts (node --no-warnings - a b,node --no-warnings -- app.js,node -e code -- a); node rejects-/--as option values outright, bun's CLI accepts them, so reporting them is the round-trippable behavior.process.execArgvinside workers created without an explicitexecArgv, and bun running asnode(the--case is covered by a fixture;node -itself is being added separately in cli: read the script from stdin fornode -in node emulation #38566). Compiled executables return early and are unaffected. The--and-chalves overlap with the execArgv part of process: execArgv fix, getActiveResourcesInfo with sockets/servers/fs, _getActiveHandles/_getActiveRequests (+9 tests, process 85%→94%) #34658; whichever lands second is a small rebase of this loop.test/js/node/process/process.test.js(process.execArgvfixture table:-in the auto andrunforms,--,--in node mode,-/--as option values,-c) andtest/js/node/child_process/child_process.test.ts(fork()from a parent piped intobun run -). Both fail on the released binary (8 of the 13 fixtures differ / child exit 1) and pass with the debug build;test/cli/run/run-eval.test.ts,test/cli/run/as-node.test.ts,test/js/node/util/parse_args/default-args.test.mjs,test/bundler/compile-process-execargv.test.ts, the worker argv/execArgv tests andtest/js/node/test/parallel/test-child-process-fork-exec-argv.jsstill pass.Background
process.execArgvis the list of runtime options between the executable and the script (["--smol"]forbun --smol app.js). Bun does not record it during CLI parsing;create_exec_argvrebuilds it lazily by walking the raw argv again, so it has to mirror the CLI parser's token rules by hand.bun run -/bun -: a bare-in the script position makesRunCommand::exec_stdinread the script from stdin and run it as<cwd>/[stdin], withprocess.argv[1]set to"-"as in node.--makes the parser take the next token as the script and leave the rest forprocess.argv.src/clap):One/Manyoptions (--conditions,-e,--title, ...) take the following argv token as their value;OneOptionaloptions (-c/--config,--inspect) only take a value written as--flag=valueand never touch the following token.