Skip to content

process.execArgv: stop at the "-" stdin marker and "--" like a script name - #38577

Open
robobun wants to merge 4 commits into
mainfrom
farm/38881e9f/execargv-stdin-dash
Open

process.execArgv: stop at the "-" stdin marker and "--" like a script name#38577
robobun wants to merge 4 commits into
mainfrom
farm/38881e9f/execargv-stdin-dash

Conversation

@robobun

@robobun robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Problem

  • A script started as bun run - a b (script on stdin) reports process.execArgv as ["-"]; with flags after the marker, bun --no-warnings run - -x --foo reports ["--no-warnings", "-", "-x", "--foo"]. Node prints [] and ["--no-warnings"]: the - is the script-name slot, and everything after it is process.argv. process.argv is already right (["-", "a", "b"]), only execArgv is wrong.
  • Consequence: child_process.fork() from a stdin script is broken. fork() puts process.execArgv in front of the module path, so the child is launched as bun - ./child.js x, reads its (empty) stdin as the script and exits 1 with error: Module not found '<cwd>/[stdin]'.
  • Two more cases in the same loop, found while fixing it: bun --smol -- app.js reports ["--smol", "--"] (node drops the --), and bun -c app.js reports ["-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.js runs app.js).
  • Cause: create_exec_argv in src/runtime/node/node_process.rs re-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

  • The loop now classifies tokens the way the CLI parser does: the token after an option that consumes a value is that value whatever it is spelled like (--conditions - still reports ["--conditions", "-"]); otherwise a bare - or -- ends execArgv exactly like a script name; otherwise a --prefixed token is an exec arg.
  • The value-consuming set is built from params whose takes_value is One or Many; OneOptional ones are left out because the parser never pulls the next token for them.
  • Why this is right: execArgv has one consumer contract, bun <execArgv> <script> <args> must reproduce the current process's options (that is what fork() and new Worker() build). src/clap/streaming.rs treats - 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.
  • The same code path serves process.execArgv inside workers created without an explicit execArgv, and bun running as node (the -- case is covered by a fixture; node - itself is being added separately in cli: read the script from stdin for node - in node emulation #38566). Compiled executables return early and are unaffected. The -- and -c halves 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.
  • Tests: test/js/node/process/process.test.js (process.execArgv fixture table: - in the auto and run forms, --, -- in node mode, -/-- as option values, -c) and test/js/node/child_process/child_process.test.ts (fork() from a parent piped into bun 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 and test/js/node/test/parallel/test-child-process-fork-exec-argv.js still pass.

Background

  • process.execArgv is the list of runtime options between the executable and the script (["--smol"] for bun --smol app.js). Bun does not record it during CLI parsing; create_exec_argv rebuilds 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 makes RunCommand::exec_stdin read the script from stdin and run it as <cwd>/[stdin], with process.argv[1] set to "-" as in node. -- makes the parser take the next token as the script and leave the rest for process.argv.
  • Param kinds (src/clap): One/Many options (--conditions, -e, --title, ...) take the following argv token as their value; OneOptional options (-c/--config, --inspect) only take a value written as --flag=value and never touch the following token.

… 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.
@robobun

robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

Status: reproduced on the released binary (echo 'console.log(process.execArgv)' | bun run - a b prints ["-"]; fork() from such a script exits 1 with Module not found '<cwd>/[stdin]'). The execArgv fixture table in process.test.js (9 of its 14 rows fail on the released binary) and the fork()-from-stdin test in child_process.test.ts pass with this branch's build. Not a duplicate of #34658 / #34654 / #35779: none of them stops at the bare - (details in the comment below the bot's list); #38566 adds node - itself. The first CI build only lacked the macOS 14 arm64 lane, whose jobs expired without an agent; the second push re-ran CI.

@coderabbitai

coderabbitai Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 4 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 36cf024b-d698-4848-a7e8-8bda155719ed

📥 Commits

Reviewing files that changed from the base of the PR and between 523e0da and 99b81db.

📒 Files selected for processing (1)
  • src/runtime/node/node_process.rs

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2590e21f-25fa-43fe-a794-4a6037933b32

📥 Commits

Reviewing files that changed from the base of the PR and between eabb96d and 523e0da.

📒 Files selected for processing (3)
  • src/runtime/node/node_process.rs
  • test/js/node/child_process/child_process.test.ts
  • test/js/node/process/process.test.js

Walkthrough

Changes

Node execArgv parsing

Layer / File(s) Summary
Required-value option parsing
src/runtime/node/node_process.rs
create_exec_argv tracks required-value options and preserves their following tokens, including values beginning with -. Bare - and -- terminate option consumption.
Argument propagation regression coverage
test/js/node/process/process.test.js, test/js/node/child_process/child_process.test.ts
Tests cover stdin scripts, terminators, dash-prefixed values, config options, and fork() argument and exit behavior.

Suggested reviewers: jarred-sumner

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main process.execArgv change for treating stdin and option terminators as script markers.
Description check ✅ Passed The description explains the problem, fix, scope, and verification results, although it does not use the template headings exactly.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 classified Positional (line 319), and OneOptional params never pull the next token while One/Many do — the new CONSUMES_NEXT_ARG predicate 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-end fork() 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 run used to lose its value to the seen_run branch.
  • The Values enum has exactly four variants (None/One/Many/OneOptional), so the matches! covers the intended set with no gaps.
  • NODE_SHORT_ALIASES handling is unchanged and still correct (the -pe → -p alias inherits value-taking from -p).
  • Test structure follows repo conventions (tempDir, drained pipes, single toEqual on the whole fixture map for a useful diff on failure). The stdin-redirect trick (< ${script}) lets the same fixture file serve both index.ts and - positions without duplication.
  • The PR notes the overlap with #34658; whichever lands second is a small rebase of this loop.

@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. process: execArgv fix, getActiveResourcesInfo with sockets/servers/fs, _getActiveHandles/_getActiveRequests (+9 tests, process 85%→94%) #34658 - Rewrites the same create_exec_argv loop in src/runtime/node/node_process.rs with the same awaiting_value refactor, the same Values::One | Values::Many narrowing, and an if arg == b"--" { break; } terminator (missing only the bare - case), and edits the same process.test.js execArgv fixtures.
  2. cli: refuse to run under Node.js permission-model flags #35779 - Despite an unrelated title, its node_process.rs hunk is this PR's optional-value fix verbatim: param.takes_value != Values::Nonematches!(param.takes_value, Values::One | Values::Many) in the same lazy set.
  3. process/worker: env descriptor validation, worker execArgv policy table with per-worker --expose-gc (+3 tests, worker 74%→76%) #34654 - Deletes create_exec_argv's argv-scanning loop entirely in favor of a shared collect_process_exec_argv_tokens() using the identical Values::One | Values::Many classification — same function, structurally incompatible rewrite.

🤖 Generated with Claude Code

@robobun

robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

Checked the three against this branch (fetched each PR head and read the create_exec_argv change):

So the bare - fix, and the fork()-from-stdin breakage it causes, is not in any of them. The overlap is the One | Many predicate (identical in all four branches) and the -- comparison shared with #34658; the end state of that code is the same in each, so whichever lands first leaves the others a mechanical rebase. The fixtures added here (run - a b, --smol run - -x --foo, --smol - foo, run --smol -- - a) would also catch the - case getting lost in the #34654 rewrite.

Comment thread src/runtime/node/node_process.rs Outdated
Comment thread src/runtime/node/node_process.rs Outdated
Comment thread src/runtime/node/node_process.rs Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-set seen_run) and --flag=value (not in the set, so awaiting_value stays false).
  • Confirmed against src/clap/streaming.rs:120-148 that OneOptional never pulls the next token and One/Many pull 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 OneOptional is 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 to Promise.all + Object.fromEntries keeps per-row failure messages keyed by the original command string.

Comment thread src/runtime/node/node_process.rs Outdated
Comment thread src/runtime/node/node_process.rs Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — focused fix to the create_exec_argv re-parse loop with thorough fixture coverage.

What was reviewed:

  • Verified against src/clap/streaming.rs: OneOptional params never pull the next token (line 120-130), One/Many pull it unconditionally (line 138), and bare -/-- are classified as ArgKind::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_value state machine; the ordering (awaiting-value → -/-- break → flag push → run skip) 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_ARG static was hoisted out of the loop body, but as a LazyLock static this is purely cosmetic. The Values::One | Values::Many predicate correctly excludes None and OneOptional (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, all cmd keys are unique (no Object.fromEntries collisions), stdin is piped to every fixture so - cases execute the same script, and the fork test uses tempDir/await using per harness conventions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant