Skip to content
Open
54 changes: 51 additions & 3 deletions src/runtime/jsc_hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1457,33 +1457,81 @@
/// 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;

fn find_short(c: u8) -> Option<&'static bun_clap::Param<bun_clap::Help>> {
RUN_PARAMS.iter().find(|p| p.names.short == Some(c))
}

// Does `bytes` name a `RUN_PARAMS` flag whose value is supplied by the
// *next* token? `--long=val` and `OneOptional` params never pull from the
// iterator. A bare `--long` / `-s` pulls when its `takes_value` is
// `One`/`Many`. A chained-short cluster (`-br`) pulls when every prefix
// char is a `None`/`OneOptional` short and the last char is a
// `One`/`Many` short with nothing after it — `StreamingClap::chainging`
// calls `iter.next()` in exactly that case.
fn flag_consumes_next_token(bytes: &[u8]) -> bool {
if let Some(long) = bytes.strip_prefix(b"--") {
let param = RUN_PARAMS.iter().find(|p| p.names.matches_long(long));
return matches!(
param.map(|p| p.takes_value),
Some(Values::One | Values::Many)
);
}
if let Some(shorts) = bytes.strip_prefix(b"-") {
for (i, &c) in shorts.iter().enumerate() {
let Some(param) = find_short(c) else {
return false;
};
match param.takes_value {
Values::None | Values::OneOptional => continue,
// `One`/`Many`: pulls from the iterator only when this is
// the last char; otherwise the rest of the cluster (or
// the text after `=`) is the inline value.
Values::One | Values::Many => return i + 1 == shorts.len(),
}

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

View check run for this annotation

Claude / Claude Code Review

OneOptional short mid-cluster: scanner keeps chaining where StreamingClap stops

The chained-short walk treats `Values::OneOptional` like `Values::None` (`continue`), but `StreamingClap::chainging` stops chaining whenever `takes_value != None` (streaming.rs:231) and returns `{value: None}` for `OneOptional` (streaming.rs:245-251), silently dropping the rest of the cluster. For `['-cr', '--no-addons']` (`-c` is `--config <PATH>?`, the only `OneOptional` short in `RUN_PARAMS`) the reference parser drops the `'r'` and sees `--no-addons`, while this scanner walks to `'r'`, retur
Comment thread
robobun marked this conversation as resolved.
Outdated
}
}
false
}

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.
// The previous `One`/`Many` flag consumes this token as its value via
// raw `iter.next()` — regardless of a leading `-` or a literal `--`.
if core::mem::take(&mut prev_wants_value) {
continue;
}
// `stop_after_positional_at = 1` — first positional ends parsing.
if bytes.first() != Some(&b'-') {
break;
}

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

View check run for this annotation

Claude / Claude Code Review

Bare '-' token not treated as positional, diverging from StreamingClap

A bare `'-'` token isn't treated as a positional here, but `StreamingClap::parse_next_arg` (streaming.rs:308) classifies both `b"--"` and `b"-"` as `ArgKind::Positional`, so under `stop_after_positional_at = 1` the reference parser stops at `'-'` while this scanner continues past it — for `['-', '--no-addons']` the two diverge (this one disables addons, StreamingClap leaves them enabled). It errs in the safe direction and the input is implausible, but the rewritten comment 'first positional ends
Comment thread
robobun marked this conversation as resolved.
Outdated
if bytes == b"--" {
break;
}
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
37 changes: 36 additions & 1 deletion test/js/node/no-addons.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { spawnSync } from "bun";
import { expect, test } from "bun:test";
import { describe, expect, test } from "bun:test";
import { bunExe, bunEnv as env } from "harness";
import { Worker } from "node:worker_threads";

test("--no-addons throws an error on process.dlopen", () => {
const { stdout, stderr, exitCode } = spawnSync({
Expand All @@ -15,3 +16,37 @@ test("--no-addons throws an error on process.dlopen", () => {
expect(out).toBeEmpty();
expect(err).toContain("\nerror: Cannot load native addon because loading addons is disabled.");
});

describe("worker execArgv --no-addons parsing matches RunCommand clap", () => {
// A value token following a value-taking flag (`-r x`, `--title x`, ...)
// must not be treated as the first positional when scanning execArgv, and
// the value is consumed regardless of its own content (even `--` or a
// `-`-prefixed string).
const body = `try { process.dlopen({ exports: {} }, "/nonexistent.node"); } catch (e) { require("node:worker_threads").parentPort.postMessage(e.code); }`;
test.each([
[["--no-addons"], "ERR_DLOPEN_DISABLED"],
[["-r", "./preload.js", "--no-addons"], "ERR_DLOPEN_DISABLED"],
[["--require", "./preload.js", "--no-addons"], "ERR_DLOPEN_DISABLED"],
[["--title", "foo", "--no-addons"], "ERR_DLOPEN_DISABLED"],
[["--port", "3000", "--no-addons"], "ERR_DLOPEN_DISABLED"],
[["-e", "void 0", "--no-addons"], "ERR_DLOPEN_DISABLED"],
[["--no-addons", "-r", "./preload.js"], "ERR_DLOPEN_DISABLED"],
// chained shorts ending in a value-taking short pull the next token
[["-br", "./preload.js", "--no-addons"], "ERR_DLOPEN_DISABLED"],
// the value is consumed via raw iter.next(), even if it is `--`
[["-r", "--", "--no-addons"], "ERR_DLOPEN_DISABLED"],
// here `--no-addons` is `-r`'s value, not a flag; addons stay enabled
[["-r", "--no-addons"], "ERR_DLOPEN_FAILED"],
])("%j", async (execArgv, expected) => {
const worker = new Worker(body, { eval: true, execArgv });
try {
const code = await new Promise((resolve, reject) => {
worker.on("message", resolve);
worker.on("error", reject);
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
expect(code).toBe(expected);
} finally {
await worker.terminate();
}
});
});
Loading