Skip to content

worker_threads: validate execArgv at Worker construction - #33363

Closed
robobun wants to merge 10 commits into
mainfrom
farm/7c897052/worker-exec-argv-validation
Closed

robobun wants to merge 10 commits into
mainfrom
farm/7c897052/worker-exec-argv-validation

Conversation

@robobun

@robobun robobun commented Jul 5, 2026 •

Copy link
Copy Markdown
Collaborator

Repro

new Worker(f, { execArgv: ["--definitely-not-a-flag", "--title=x"] });

Node throws ERR_WORKER_INVALID_EXEC_ARGV synchronously. Bun starts the worker and the bogus flags show up verbatim in its process.execArgv.

$ node repro.mjs
ERR_WORKER_INVALID_EXEC_ARGV: Initiated Worker with invalid execArgv flags: --definitely-not-a-flag, --title=x

$ bun repro.mjs
ACCEPTED, worker execArgv=["--definitely-not-a-flag","--title=x"]

Cause

JSWorker.cpp coerced every execArgv entry to a string and stored the array unvalidated. Bun reads only --no-addons back out of it (parse_worker_exec_argv_allow_addons), so everything else is inert but still echoed back through the worker's process.execArgv. Code that relies on the constructor throwing to detect unsupported runtime flags (pools that forward a filtered process.execArgv) silently proceeds: the classic "my --max-old-space-size isn't being applied in workers" report, with no error anywhere.

Fix

Validate execArgv at construction and throw ERR_WORKER_INVALID_EXEC_ARGV, as Node does in Worker::New. Only an explicitly passed execArgv is checked; an inherited one is not, matching Node.

New src/runtime/cli/worker_exec_argv.rs classifies each entry against:

  • Bun's own AUTO_PARAMS table — the same table process.execArgv's re-parser derives its value-consuming set from — so every flag bun <entrypoint> accepts keeps working.
  • A table of Node flags Bun's CLI does not implement (--experimental-vm-modules, --warnings, --tls-min-v1.2, …), taken from Node v26.3.0's PerIsolateOptionsParser. Bun ignores them, but Node allows them in execArgv, so rejecting them would be stricter than Node.
  • A short list of flags that configure the process or the JS engine (--title, --zero-fill-buffers, --use-openssl-ca, --expose-gc, --stack-trace-limit, …). Node rejects these from execArgv too.

Node's parsing rules are reproduced: parsing stops at the first positional; a value-taking flag consumes the next entry only when that entry is not itself a flag; _ is canonicalised to - in a long flag's name; every boolean is registered under one canonical name, with --no-<name> derived from it by stripping no- exactly once (including --no-inspect, which Node registers as a boolean); and parse errors ("--conditions requires an argument") are reported in preference to unrecognised names.

Bun-only flags (--smol, --hot, --port, --no-macros) stay accepted. Node rejects them only because it has no such flag, and users forward them through process.execArgv today.

Verification

A differential of the validator against real Node v26.3.0 over 105 inputs (the repro, engine flags, per-process flags, both boolean spellings, underscore spellings, doubled no- prefixes, negations, missing values, positionals, --, short flags) produces zero differences. The remaining deliberate divergences are flags Bun's CLI has and Node does not (--no-macros, --install, --no-env-file, …), and: ["--stack-trace-limit", "7", "--bogus"] names only --stack-trace-limit where Node names both. Both throw the same error.

$ bun bd test test/js/node/worker_threads/worker_threads.test.ts
 121 pass
 0 fail

With src/ reverted to the merge-base, 19 of those fail.

test/js/web/workers/worker.test.ts, test-no-addons-resolution-condition.js and test-trace-exit.js (all of which pass execArgv to a Worker) still pass.

Also in this PR

  • The ERR_WORKER_INVALID_EXEC_ARGV tests terminate the worker that gets born when the constructor fails to throw, so a regression fails the assertion instead of leaking a thread into every test that follows. The accepting tests wire the worker's error event to reject so a startup failure names itself instead of hanging until the timeout.
  • Eight existing tests in worker_threads.test.ts get a debug-build timeout. Each spawns a subprocess that builds a chain of workers (or takes a full V8-format heap dump) and needs 5.6-11.2s under bun bd, well past the 5s default; they fail identically on clean main. Same treatment as fetch-leak.test.ts. No assertions changed.

Rebase note

#33947 made ErrorCode.rs generated from ErrorCode.ts, so the error code is now a one-line append there; no Rust-side edit remains.

Not covered

Node also throws ERR_WORKER_INVALID_EXEC_ARGV for a bad env.NODE_OPTIONS passed to a worker. Bun does not read NODE_OPTIONS at all, so that half of test-worker-execargv-invalid.js is out of scope here; the execArgv cases from that file are covered in worker_threads.test.ts.

@coderabbitai

coderabbitai Bot commented Jul 5, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 29 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: 0b2ad941-8a54-4449-98b8-8abe0f7bee3a

📥 Commits

Reviewing files that changed from the base of the PR and between d3603d4 and 5434286.

📒 Files selected for processing (2)
  • src/jsc/bindings/ErrorCode.ts
  • src/jsc/bindings/webcore/JSWorker.cpp

Walkthrough

Adds a new ERR_WORKER_INVALID_EXEC_ARGV error code, validates worker execArgv in the C++ constructor path through a Rust FFI function, implements flag classification and error reporting in Rust, and expands worker thread tests for invalid and accepted execArgv cases.

Changes

Worker execArgv validation

Layer / File(s) Summary
New error code definition
src/jsc/ErrorCode.rs, src/jsc/bindings/ErrorCode.ts
Adds WORKER_INVALID_EXEC_ARGV, its ERR_ alias, the CODE_STR entry, the updated count, and the TypeScript error mapping entry.
C++ Worker constructor validation call
src/jsc/bindings/webcore/JSWorker.cpp
Declares Bun__Worker__validateExecArgv and calls it from JSWorkerDOMConstructor::construct after building options.execArgv, forwarding exceptions through throwScope.
Rust execArgv validation logic
src/runtime/cli/mod.rs, src/runtime/cli/worker_exec_argv.rs
Adds the worker_exec_argv module, flag lookup/classification tables, invalid-entry and message construction, the exported FFI validator, and unit tests.
Worker execArgv test coverage
test/js/node/worker_threads/worker_threads.test.ts
Adds invalid-type and invalid-flag constructor tests, a regression for repeated no- negation, an accepted execArgv suite, and an explicit timeout for the source-leak test.

Sequence Diagram(s)

sequenceDiagram
  participant JSWorkerDOMConstructor
  participant Bun__Worker__validateExecArgv
  participant ErrorCode
  participant WorkerTests

  JSWorkerDOMConstructor->>Bun__Worker__validateExecArgv: validate execArgv StringImpl pointers
  Bun__Worker__validateExecArgv->>ErrorCode: throw ERR_WORKER_INVALID_EXEC_ARGV on invalid entries
  WorkerTests->>JSWorkerDOMConstructor: construct workers with valid/invalid execArgv
  JSWorkerDOMConstructor-->>WorkerTests: accept or reject constructor
Loading

Compact Metadata

  • Primary language(s): Rust, C++, TypeScript
  • Est. review effort: High
  • Related issues: None specified
  • Related PRs: None specified
  • Suggested labels: area:workers, area:errors, rust, tests
  • Suggested reviewers: None specified
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title clearly summarizes the main change: validating Worker execArgv during construction.
Description check ✅ Passed The description is detailed and covers the PR purpose and verification, though it doesn't use the template's exact section headings.

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

@github-actions github-actions Bot added the claude label Jul 5, 2026
@robobun

robobun commented Jul 5, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 10:25 PM PT - Jul 10th, 2026

❌ @autofix-ci[bot], your commit 5434286 has 1 failures in Build #71786 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 33363

That installs a local version of the PR into your bun-33363 executable, so you can run:

bun-33363 --bun

Comment thread src/runtime/cli/worker_exec_argv.rs Outdated
Comment thread src/runtime/cli/worker_exec_argv.rs
Comment thread src/runtime/cli/worker_exec_argv.rs Outdated
Comment thread src/runtime/cli/worker_exec_argv.rs Outdated
Comment thread src/runtime/cli/worker_exec_argv.rs
Comment thread src/runtime/cli/worker_exec_argv.rs

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

All my earlier nits are addressed (including --no-inspect in 4e83e3e) and I found nothing new this pass, but this introduces a behavioural tightening — previously-accepted execArgv values now throw — backed by ~120 hand-maintained Node flag-name entries and an unsafe C++→Rust FFI boundary, so it's worth a human sign-off on the accept/reject policy and the table contents.

Extended reasoning...

Overview

Adds ERR_WORKER_INVALID_EXEC_ARGV validation at new Worker(f, { execArgv }) construction to match Node.js. The bulk of the change is a new 605-line src/runtime/cli/worker_exec_argv.rs containing a flag classifier (lookup_long/lookup_exact/invalid_entries) plus four hand-maintained tables (PER_PROCESS_LONG, NODE_ONLY_BOOLEAN ~80 entries, NODE_ONLY_WITH_VALUE ~40 entries, NEGATABLE_WITH_OPTIONAL_VALUE) derived from Node v26.3.0's node_options.cc. JSWorker.cpp gains an extern "C" call into Bun__Worker__validateExecArgv via a reinterpret_cast<WTF::StringImpl**> over the local Vector<String>. ErrorCode.ts/ErrorCode.rs get the new error code appended. ~120 lines of new tests in worker_threads.test.ts plus Rust unit tests, and an unrelated debug-build timeout bump on the neighbouring "eval does not leak source code" test.

Security risks

None identified. Input is same-process JS (not a trust boundary), the classifier only reads static tables and byte slices, and the earlier stack-overflow-on-pathological-input I flagged was fixed (single non-recursive no- strip, covered by a 200k-repeat regression test). The FFI unsafe block dereferences WTF::StringImpl* from a caller-owned stack Vector<String> that outlives the call; the pattern mirrors Worker::create() and is guarded by a static_assert.

Level of scrutiny

Medium-high. This is a behavioural tightening: execArgv values Bun previously accepted silently now throw synchronously, so real user code (worker pools forwarding filtered process.execArgv, packages passing --max-old-space-size) can start failing. The accept/reject policy encodes several judgment calls a maintainer should ratify — accepting Bun-only flags Node rejects (--smol, --no-macros), the --use-system-ca per-environment carve-out, treating --inspect as boolean-negatable via a special-case list — and the NODE_ONLY_* tables are a new hand-maintained mirror of upstream Node source that will drift. The five rounds of edge-case fixes on this PR (positive boolean spellings, _→- canonicalisation, recursive no-, Bun-only negative flags, OneOptional negation) show the correctness surface is subtle.

Other factors

Test coverage is thorough (Rust unit tests including sortedness checks, JS it.each tables for both accept and reject paths, the differential against Node described in the PR body). All six of my prior inline findings were addressed with commits and pinned by tests; the last one (--no-inspect) is fixed in 4e83e3e via NEGATABLE_WITH_OPTIONAL_VALUE though the thread isn't yet marked resolved. No CODEOWNERS hit that I can see. The unrelated timeout bump on "eval does not leak source code" is reasonable and explained in the PR body.

@robobun

robobun commented Jul 5, 2026 •

Copy link
Copy Markdown
Collaborator Author

All six inline findings are addressed and their threads are resolved (the snapshot above predates the last resolve). Summarising the judgment calls so they are easy to ratify or overrule, since they are the part that needs a human:

Decision Why Overruling it means
Bun-only flags stay accepted (--smol, --hot, --no-macros) Users forward them through process.execArgv today; Node rejects them only because it has no such flag Breaking code that forwards Bun's own flags
Node flags Bun ignores stay accepted (--experimental-vm-modules, the --test-* family) Node accepts them in execArgv, and this PR never wants to be stricter than Node Rejecting flags that work under Node
--use-system-ca accepted, --use-openssl-ca / --use-bundled-ca rejected Node registers the first in EnvironmentOptionsParser, the other two in PerProcessOptionsParser (node_options.cc:1100 vs :1432) Diverging from Node on one of the three
--no-inspect accepted, --no-config not Node registers --inspect as a plain boolean with an implied --inspect-port; Bun's --config really names a value Inventing --no-config, a flag neither runtime has

On table drift: a flag a later Node adds and NODE_ONLY_* misses is rejected, not silently ignored, so the failure is loud and one line to fix. That is also how Node behaves with a flag from a newer Node. Noted in the table's doc comment in 9e1793c.

The behavioural tightening is the point of the PR rather than a side effect — execArgv entries Bun cannot apply were being echoed back through the worker's process.execArgv as if they had taken effect. Everything Bun previously accepted and acted on still works; what now throws is the set Bun was ignoring, narrowed to what Node also rejects.

Not covered, for the record: Node also validates a worker's env.NODE_OPTIONS. Bun does not read NODE_OPTIONS at all, so that is out of scope here.

Rebased onto 55ba6e4 (main moved 115 commits since I branched). Two things touched this diff:

Post-rebase verification: bun bd test worker_threads.test.ts is 121 pass / 0 fail across three runs; with src/ reverted to main, 19 fail. test/js/web/workers/worker.test.ts and test-no-addons-resolution-condition.js still pass. Format, clippy, clang-format and prettier are clean.

CI status (5434286, build #71786): worker_threads.test.ts passes 121/0 on every lane that ran it, including darwin 26 aarch64 (2.85s). 226+ test-bun lanes pass. Format, cargo clippy, Lint JavaScript green.

The only red is test/cli/run/no-orphans.test.ts on darwin 26 aarch64 — bun run --no-orphans (perl): fast-exit intermediate times out at 30s across all four retries. This diff touches neither --no-orphans, spawn, nor process reaping; reported separately so a dedicated session owns that test.

Ready for a maintainer.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/js/node/worker_threads/worker_threads.test.ts`:
- Around line 384-394: The Worker test currently waits only for the "message"
event in the execArgv case, so startup regressions can hang until timeout
instead of failing fast. Update the test around the Worker creation and
once(worker, "message") await to also listen for the worker "error" event and
reject immediately on failure, so the assertion fails with the startup error
rather than waiting for a message that never arrives.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8a0df085-7d68-4938-8bf1-1da84066e13b

📥 Commits

Reviewing files that changed from the base of the PR and between fb50cce and 9e1793c.

📒 Files selected for processing (6)
  • src/jsc/ErrorCode.rs
  • src/jsc/bindings/ErrorCode.ts
  • src/jsc/bindings/webcore/JSWorker.cpp
  • src/runtime/cli/mod.rs
  • src/runtime/cli/worker_exec_argv.rs
  • test/js/node/worker_threads/worker_threads.test.ts

Comment thread test/js/node/worker_threads/worker_threads.test.ts
Comment thread test/js/node/worker_threads/worker_threads.test.ts

@coderabbitai coderabbitai 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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
test/js/node/worker_threads/worker_threads.test.ts (1)

353-357: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Use Buffer.alloc(...).toString() instead of .repeat() for the large string.

"no-".repeat(200_000) builds a ~600KB string via repeated string ops, which the repo's own testing guideline calls out as slow in debug JSC builds. This string is only used as an array element (not passed to a byte-encoding API), so no exception applies here.

As per coding guidelines: "To create repetitive strings in tests, use Buffer.alloc(count, fill).toString() instead of "string".repeat(count) for better performance in debug builds."

♻️ Proposed fix
-      await expectRejected(["--" + "no-".repeat(200_000)], {
+      await expectRejected(["--" + Buffer.alloc(200_000 * "no-".length, "no-").toString()], {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/js/node/worker_threads/worker_threads.test.ts` around lines 353 - 357,
The test in worker_threads.test.ts builds a very large repeated string with the
negation argument, which should be changed to the repo-preferred Buffer-based
approach for faster debug JSC performance. Update the repeated value used in
expectRejected in the worker_threads test to use Buffer.alloc(...).toString()
instead of the current repeat-based construction, keeping the same string
content and preserving the existing assertion behavior.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@test/js/node/worker_threads/worker_threads.test.ts`:
- Around line 353-357: The test in worker_threads.test.ts builds a very large
repeated string with the negation argument, which should be changed to the
repo-preferred Buffer-based approach for faster debug JSC performance. Update
the repeated value used in expectRejected in the worker_threads test to use
Buffer.alloc(...).toString() instead of the current repeat-based construction,
keeping the same string content and preserving the existing assertion behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 08b045fe-ab0a-4ca0-826f-4a4a47a43efd

📥 Commits

Reviewing files that changed from the base of the PR and between 9e1793c and d3603d4.

📒 Files selected for processing (1)
  • test/js/node/worker_threads/worker_threads.test.ts

@robobun
robobun force-pushed the farm/7c897052/worker-exec-argv-validation branch from d3603d4 to f7a05a4 Compare July 11, 2026 02:23
robobun added 9 commits July 11, 2026 02:23
new Worker(file, { execArgv }) accepted any array of strings and echoed it
back through the worker's process.execArgv, even though Bun reads only
--no-addons out of it. A typo, or a flag that can never apply to a thread,
passed silently.

Parse execArgv the way Node does and throw ERR_WORKER_INVALID_EXEC_ARGV for
unrecognised names, per-process flags and engine flags. Flags Bun's CLI
accepts stay accepted, and so do the Node flags Bun ignores, so nothing that
Node allows in execArgv is rejected.
clippy's disallowed-methods bans String::from_utf8_lossy; format the joined
names through bstr::BStr instead.
…n, and single `no-` strip

Three divergences from Node, all found by review:

- Node registers one canonical name per boolean option and derives the other
  spelling, so `--warnings` and `--no-warnings` both name the same option.
  Our tables carry the documented spelling, so look up the negation too.
- Node canonicalises `_` to `-` in a long option's name before looking it up.
- Node strips `no-` exactly once. Recursing per prefix also overflowed the
  stack on `--` + "no-".repeat(200000), crashing the process instead of
  reporting the flag.

The ERR_WORKER_INVALID_EXEC_ARGV tests now terminate the worker that is born
when the constructor fails to throw, so a regression fails the assertion
instead of leaking a thread into every later test. The heavy eval-source leak
test gets a debug-build timeout: its fixture hands 600 MiB of source to six
workers, which takes ~30s under a debug build and always blew the 5s default.
…e-only flags

The reverse `no-` lookup went through AUTO_PARAMS, so Bun flags that only have
a negative name (`--no-macros`, `--no-clear-screen`, `--no-orphans`,
`--no-exit-on-error`) made up positive spellings that neither Bun nor Node
knows. Spell NODE_ONLY_BOOLEAN with the name Node registers each option under
instead, which is the positive one for a default-true option, so the existing
`no-` strip derives the other spelling and no reverse lookup is needed.
It shares Bun's CA-store group with --use-bundled-ca and --use-openssl-ca, but
Node registers it in EnvironmentOptionsParser rather than
PerProcessOptionsParser, so it is the one of the three Node accepts in a
worker's execArgv. Pin all three in the tests so the asymmetry is not mistaken
for an oversight.
Bun declares --inspect, --inspect-brk and --inspect-wait as <STR>?, so the
no- strip classified --no-inspect as negating a non-boolean and threw. Node
registers all three as plain booleans whose address is a separate implied
--inspect-port, and accepts --no-inspect.

Only those three, not every <STR>? flag: Bun's --config really does name a
value, and Node rejects --no-config just as it rejects --no-inspect-port.
Awaiting only "message" meant an accepted flag that broke worker startup hung
until the test timed out. Reject on "error" so the failure names itself.
…nd heap-dump tests

Seven tests from #31216 spawn a subprocess that builds a chain of workers (or
take a full V8-format heap snapshot) and need 5.6-11.2s under `bun bd`, well
past the 5s default. They fail the same way on clean main. Same treatment as
the `eval does not leak source code` timeout earlier in this PR; no
assertions changed.
@robobun
robobun force-pushed the farm/7c897052/worker-exec-argv-validation branch from f7a05a4 to b1b4e2f Compare July 11, 2026 02:35

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

Additional findings (outside current diff — PR may have been updated during review):

  • 🟡 test/js/node/worker_threads/worker_threads.test.ts:1-8 — The const subprocessTimeout = ... declaration (and its comment) sits between the harness import and the node:events/node:fs/worker_threads imports, breaking the file's contiguous-imports-at-top convention. Purely cosmetic (ES imports hoist, so isDebug is defined regardless) — moving the const below the last import restores it.

    Extended reasoning...

    What this is

    The diff changes the top of worker_threads.test.ts from a single contiguous block of imports to:

    import { bunEnv, bunExe, isDebug, tmpdirSync } from "harness";
    
    // A debug build starts a worker VM about an order of magnitude more slowly, so
    // the tests that spawn a subprocess that then builds a chain of workers need
    // headroom past the 5s default to finish under `bun bd`.
    const subprocessTimeout = isDebug ? 60_000 : 20_000;
    import { once } from "node:events";
    import fs from "node:fs";
    import { join, relative, resolve } from "node:path";
    import { Readable } from "node:stream";
    import wt, { ... } from "worker_threads";

    Before this PR, all import statements were contiguous at the top of the file with no code interleaved. The new const (with its 3-line comment) is wedged between the first import and the remaining five.

    Why it's flagged

    Root CLAUDE.md's "Code style & idioms reviewers enforce" section says: "Match the exact file's local conventions: the namespace aliases the neighboring lines actually use, import placement, canonical parameter names, ...". This file's local convention was contiguous imports at the top, and the new line is a fresh addition in this PR (not pre-existing), so it's fair to point at.

    Why it doesn't matter behaviorally

    ES module import declarations are hoisted: all module bindings are established before any top-level statement executes. So even though const subprocessTimeout = isDebug ? ... textually precedes import { once } from "node:events", at runtime isDebug is already bound (from the hoisted harness import) when the initializer runs, and the later imports are already resolved too. There is zero behavioral difference between the current placement and the conventional one.

    Step-by-step

    1. Module linking phase: all six import declarations are hoisted and their bindings (bunEnv, bunExe, isDebug, once, fs, ...) established.
    2. Module evaluation phase: top-level statements execute in source order. The first non-import statement is const subprocessTimeout = isDebug ? 60_000 : 20_000;.
    3. isDebug is a live binding from step 1 → evaluates fine → subprocessTimeout is assigned.
    4. All subsequent uses of subprocessTimeout (as the third argument to test(...)/it(...) at lines ~334, ~638, ~1495, etc.) read the same value they would if the const were below the last import.

    Identical observable behavior either way.

    Impact & fix

    Purely cosmetic. The placement has a defensible rationale — proximity to the isDebug import it depends on — but it reads oddly (a blank line, a comment, a const, then five more imports with no separating blank line) and diverges from both the file's prior state and the repo-wide norm.

    Fix: move the comment and const subprocessTimeout = ... to immediately after the from "worker_threads" import block, so all imports stay contiguous:

    import { bunEnv, bunExe, isDebug, tmpdirSync } from "harness";
    import { once } from "node:events";
    ...
    import wt, { ... } from "worker_threads";
    
    // A debug build starts a worker VM about an order of magnitude more slowly, ...
    const subprocessTimeout = isDebug ? 60_000 : 20_000;

    Non-blocking; nothing breaks if merged as-is.

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

No new issues found — the six earlier inline findings are all addressed and resolved. Deferring to a human because this is a ~600-line new validator that tightens user-visible behaviour (previously-accepted execArgv entries now throw), and the author's own summary explicitly lists design judgment calls for a maintainer to ratify.

Checked: the FFI boundary in JSWorker.cpp (StringImpl* lifetime, RETURN_IF_EXCEPTION placement, null-impl guard); lookup_canonical_long's single-strip no- handling and _→- canonicalisation against Node's parser rules; that the sorted tables' binary_search invariant is unit-tested; and that the timeout-only edits to eight existing tests leave their assertions unchanged.

Extended reasoning...

Overview

Adds Node-compatible ERR_WORKER_INVALID_EXEC_ARGV validation to new Worker(_, { execArgv }). A new 606-line src/runtime/cli/worker_exec_argv.rs classifies each flag against Bun's own AUTO_PARAMS table plus three hand-maintained tables (~150 entries) mirroring Node v26.3.0's PerIsolateOptionsParser. JSWorker.cpp calls into it over FFI before moving the strings into WorkerOptions. One-line ErrorCode.ts append; ~130 new test lines plus debug-build timeout headroom on eight existing subprocess/heap-dump tests.

Security risks

None material. Input is same-process JS constructing a Worker (not a trust boundary). The one previously-identified DoS shape — unbounded no- recursion overflowing the stack — was fixed and has both a Rust unit test and a JS regression test. The reinterpret_cast<WTF::StringImpl**> matches the existing pattern in Worker::create and is guarded by a static_assert; the Rust side null-checks each impl and owns its transcoded copies.

Level of scrutiny

High. This is a behavioural tightening: execArgv values Bun silently accepted now throw synchronously at construction. That is the point (Node parity), but it can break code that today forwards flags Bun ignores. The classification logic is subtle — no- derivation direction, _ canonicalisation, Values::OneOptional negation, per-process vs per-environment split — and took six review rounds to converge. The three static tables mirror an external source of truth (Node's option registry) and will drift; the author documented that drift fails loud, but the maintenance burden is real.

Other factors

The author's own summary comment explicitly tables four design decisions "so they are easy to ratify or overrule, since they are the part that needs a human" — accepting Bun-only flags, tolerating Node-only flags Bun ignores, the --use-system-ca split, and the --no-inspect/--no-config asymmetry. That is a direct request for maintainer sign-off on policy, which puts this outside auto-approval scope regardless of implementation quality. Test coverage is strong (Rust unit tests + it.each matrices on both accept and reject sides, differential-tested against live Node), and all prior inline findings are resolved with pinning tests.

@robobun

robobun commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator Author

Closing as part of a cleanup of stale pull requests. This PR has had no new commits since 2026-07-11, it conflicts with main, and its last CI run failed. This is not a judgment on the fix itself. If the problem still reproduces on a current build, reopen this PR after a rebase or open a new one against main.

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