Skip to content
37 changes: 34 additions & 3 deletions src/runtime/jsc_hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1457,26 +1457,56 @@
/// reads the single `--no-addons` flag from the result (per the in-tree
/// `// TODO: currently this only checks for --no-addons`), so this body scans
/// the converted argv directly with the same `stop_after_positional_at = 1`
/// short-circuit. Full clap routing can return when `ComptimeClap` grows a
/// borrowed-lifetime variant.
/// short-circuit — consulting `RUN_PARAMS` so a value token following a
/// value-taking flag (`-r x`, `--title x`, …) is consumed rather than treated
/// as the first positional. Full clap routing can return when `ComptimeClap`
/// grows a borrowed-lifetime variant.
///
/// # Safety
/// Each `WTFStringImpl` in `exec_argv` is a live WTF string (the C++
/// `Worker::create` array, kept alive for the worker's lifetime).
unsafe fn parse_worker_exec_argv_allow_addons(
exec_argv: &[bun_core::WTFStringImpl],
) -> Option<bool> {
use crate::cli::arguments::RUN_PARAMS;
use bun_clap::Values;

// Does `bytes` name a `RUN_PARAMS` flag whose value is supplied by the
// *next* token? True only for the bare `--long` / `-s` spellings of a
// `One`/`Many` param; `--long=val`, `-s=val`, `-sval`, chained shorts and
// `OneOptional` params all carry their value (if any) inline and do not
// pull from the iterator in `StreamingClap`.
fn flag_consumes_next_token(bytes: &[u8]) -> bool {
let param = if let Some(long) = bytes.strip_prefix(b"--") {
RUN_PARAMS.iter().find(|p| p.names.matches_long(long))
} else if bytes.len() == 2 && bytes[0] == b'-' {
RUN_PARAMS.iter().find(|p| p.names.short == Some(bytes[1]))
} else {
None
};
matches!(
param.map(|p| p.takes_value),
Some(Values::One | Values::Many)
)
}

Check warning on line 1491 in src/runtime/jsc_hooks.rs

View check run for this annotation

Claude / Claude Code Review

flag_consumes_next_token misses chained shorts ending in a value-taking flag

`flag_consumes_next_token` only matches the exact 2-byte form `-s`, so a chained-short cluster whose *last* character is a value-taking short — e.g. `-br` — returns `false` and the next token is treated as a positional. `StreamingClap::chainging` *does* call `iter.next()` in that case (the `arg.len() <= next_index` branch at `src/clap/streaming.rs:254`), so for `['-br', './preload.js', '--no-addons']` the reference parser consumes `./preload.js` as `-r`'s value and sees `--no-addons`, while this
Comment thread
robobun marked this conversation as resolved.
Outdated

let mut no_addons = false;
let mut prev_wants_value = false;
for &arg in exec_argv {
if arg.is_null() {
continue;
}
// SAFETY: per fn contract — `arg` is a live `WTFStringImpl*`.
let owned = unsafe { &*arg }.to_owned_slice_z();
let bytes = owned.as_bytes();
// `stop_after_positional_at = 1` — first non-flag token ends parsing.
// `stop_after_positional_at = 1` — first positional ends parsing. A
// non-`-` token that the previous flag consumes as its value is not a
// positional.
if bytes.first() != Some(&b'-') {
if core::mem::take(&mut prev_wants_value) {
continue;
}
break;

Check warning on line 1509 in src/runtime/jsc_hooks.rs

View check run for this annotation

Claude / Claude Code Review

prev_wants_value not consumed when value token starts with '-'

The `prev_wants_value` consume-and-continue is nested inside `if bytes.first() != Some(&b'-')`, so a value token that itself starts with `-` is never consumed as the previous flag's value — `StreamingClap` pulls the next token via raw `iter.next()` regardless of content. For `['-r', '--', '--no-addons']` this scanner hits the `b"--"` → `break` instead of consuming `'--'` as `-r`'s value, leaving addons enabled (the same false-negative class this PR fixes); hoist the `core::mem::take(&mut prev_wa
Comment thread
robobun marked this conversation as resolved.
Outdated
}
if bytes == b"--" {
break;
Expand All @@ -1484,6 +1514,7 @@
if bytes == b"--no-addons" {
no_addons = true;
}
prev_wants_value = flag_consumes_next_token(bytes);
}
// Override `allow_addons` unconditionally on successful parse.
Some(!no_addons)
Expand Down
26 changes: 26 additions & 0 deletions test/js/node/worker_threads/worker_threads.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,32 @@ describe("execArgv option", async () => {
await run('["--no-warnings"]', '["--no-warnings"]\n');
});
// TODO(@190n) get our handling of non-string array elements in line with Node's

describe("--no-addons is honored after a value-taking flag", () => {
// The value token for `-r` / `--title` / `--port` / `-e` must not be
// treated as the first positional when parsing the worker's execArgv.
const body = `try { process.dlopen({ exports: {} }, "/nonexistent.node"); } catch (e) { require("node:worker_threads").parentPort.postMessage(e.code); }`;
it.each([
[["--no-addons"]],
[["-r", "./preload.js", "--no-addons"]],
[["--require", "./preload.js", "--no-addons"]],
[["--title", "foo", "--no-addons"]],
[["--port", "3000", "--no-addons"]],
[["-e", "void 0", "--no-addons"]],
[["--no-addons", "-r", "./preload.js"]],
])("%j", async execArgv => {
const worker = new Worker(body, { eval: true, execArgv });
try {
const code = await new Promise((resolve, reject) => {
worker.on("message", resolve);
worker.on("error", reject);
});
expect(code).toBe("ERR_DLOPEN_DISABLED");
} finally {
await worker.terminate();
}
});
});
});

test("eval does not leak source code", async () => {
Expand Down
Loading