Skip to content

Read --disallow-code-generation-from-strings in command::init for the commands that read their own flags - #46275

Open
robobun wants to merge 3 commits into
mainfrom
robobun/d2b76b78/strict-level-every-command
Open

robobun wants to merge 3 commits into
mainfrom
robobun/d2b76b78/strict-level-every-command

Conversation

@robobun

@robobun robobun commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • With --disallow-code-generation-from-strings=strict in BUN_OPTIONS, bun install --analyze, bun add --analyze and bun create <component> run macros. bun <flag> file.ts refuses them: error: Macros cannot be used with --disallow-code-generation-from-strings=strict.
  • arguments::parse reads the flag for four command tags only (src/runtime/cli/Arguments.rs:1048).

Fix

  • command::init (src/runtime/cli/mod.rs) is a cold wrapper around create_context_data. It reads the flag among the flags of bun: those of BUN_OPTIONS, and those before the command. The last one counts.
  • The package manager and bunx read BUN_OPTIONS only, and keep their error for a typed flag.
  • bun lint and bun fmt use this reader, not their helper.
  • Verified: test/cli/disallow-code-generation-from-strings.test.ts (61 rows, 43 fail on main). Also the lint, fmt and check suites. Self-reviewed: 13 concerns raised, 11 addressed.

Background

  • The flag sets a level for the process. =strict refuses a macro: its entry point is text made at run time.
  • bun build is not read and still runs macros: Compile TypeScript to machine code ahead of time: bun build --compile --aot #45631 makes the flag there the level of the compiled executable. Question for a maintainer: does the flag in BUN_OPTIONS or before build set the level of the build process?
  • Considered a read in each command that bundles: a new command fails open.

Downsides

  • A value other than strict in BUN_OPTIONS stops 22 more commands, before --help for 20.
  • The flag is in v1.4.3. Where BUN_OPTIONS has it, these commands now refuse a macro at =strict, and eval() throws in a macro without a value.
  • Cost: bun <file> and bun run +0 instructions, bun test +28. Release .text +656 B.
Notes

How it was found

A read of the code while #45937 (--no-macros as a switch of the process) was written. No user reported it. Reproduce it with a macro that writes a marker file:

// macro.ts
import { writeFileSync } from "node:fs";
import { join } from "node:path";
export function f() {
  writeFileSync(join(import.meta.dir, "MACRO_RAN"), "");
  return "INLINED_RESULT";
}
// entry.ts
import { f } from "./macro.ts" with { type: "macro" };
console.log(f());

<flag> is --disallow-code-generation-from-strings, <strict> is <flag>=strict.

Command line main 9b4284f this PR
BUN_OPTIONS=<strict> bun install --analyze ./entry.ts the macro runs, exit 0 error: Macros cannot be used with <strict>, exit 1
BUN_OPTIONS=<strict> bun add --analyze ./entry.ts the macro runs refused, exit 1
BUN_OPTIONS=<strict> bun create ./App.tsx, bun <strict> create ./App.tsx, bun create <strict> ./App.tsx, bun create ./App.tsx <strict>, bun -- create ./App.tsx <strict> the macro runs, files are written refused, exit 1, no file written
BUN_OPTIONS=<flag> bun install --analyze ./entry.ts eval() works in the macro eval() throws in the macro
bun <strict> <flag> lint, BUN_OPTIONS=<strict> bun <flag> lint =strict the level of Node.js's flag
BUN_OPTIONS=<flag>=bogus bun install installs error: Invalid value for <flag>: "bogus". Must be "strict", or no value, exit 1
bun <strict> install, bun <flag>=bogus install error: bun install has no <flag> flag. the same
bun build ./entry.ts with the flag in any place the macro runs the macro runs

Where the flags of bun end

  • The tokens that BUN_OPTIONS adds always count. They stand in argv before what was typed.
  • The typed tokens before the command count for the commands that take them there: create, lint, fmt, check, and the commands that arguments::parse parses (exec, upgrade, getcompletes).
  • The package manager commands and bunx refuse a flag they do not have where it is typed (bun install has no ... flag., bunx has no ... flag.). The reader leaves that to them, also for a value that is not strict.
  • The command stands where which() finds it. On main that is after the leading tokens that start with -, or for check, lint and fmt after the values of flags too (place_of_first_word). The reader takes the further of the two. CLI: fixes for bun run, flags, bunfig.toml, .env, --filter, bun init and completions #46007 makes which() use the second walk for every command, and the reader then still reads every flag before the command.
  • -- ends the flags.
  • The reader does not skip the value of a flag that takes one. No flag of these commands takes a list of flags of bun as its value.

bun build

  • arguments::parse gives bun build one table for all of argv, so a token from BUN_OPTIONS or before build is read with the flags of bun build.
  • Compile TypeScript to machine code ahead of time: bun build --compile --aot #45631 (draft) adds the flag to that table as the level of the compiled executable, and exits with --disallow-code-generation-from-strings requires --compile without --compile. With the flag in BUN_OPTIONS, that error stops every plain bun build. This is from a read of its diff. It was not built.
  • So this PR does not read the flag for bun build, in any place. BUN_OPTIONS=<strict> bun build ./entry.ts still runs macros, and a row holds that. Bun.build() in a program that runs with the flag refuses them.
  • If the answer to the question above is yes, the change is one condition in command::init and rows in the test file.

bun create

  • init reads BUN_OPTIONS and the tokens before create.
  • bun create ignores every --flag it does not know, wherever it stands. So a second read covers the tokens after create, up to --, once the kind of template is known. A -- before create ends only the flags of bun.
  • For a template that bunx runs (bun create vite), the tokens after the name of the template go to create-<template>. They are not read.
  • The two reads can give two levels. The higher one holds (bun <strict> create ./App.tsx <flag> refuses macros).

Not covered

  • bun build, see above.
  • Windows: bunx --bun and bun create <template> --bun can start the package in the bunx process. With the flag in BUN_OPTIONS that process now has the level. This is from a read of the code. It was not run: no Windows machine here.
  • The other runtime flags (--no-addons, --no-ffi-cc and the rest of RUNTIME_PARAMS_) are still read for four command tags only. A plugin of bun lint under --no-addons can still call process.dlopen() (checked). Make --no-ffi-cc and --no-addons disable all of bun:ffi #42153 has the bun build case of those two flags. The docs page now says which commands read them.
  • After exec, upgrade and getcompletes the flag is parsed and not read. These commands run no JavaScript.
  • bun init, bun completions and the help do not call init.
  • Make --no-macros a switch of the process #45937 passes the level to a child bun through BUN_OPTIONS. A child that is bun install or bun x reads it only with this change. A child that is bun build does not read it.

Tests

  • The rows are in a new file. test/js/node/disallow-code-generation-from-strings.test.ts and test/cli/lint/js-plugins.test.ts do not pass as whole files under bun bd test on a debug build with the default 5 s limit, with or without this change (13 of 38 rows of the first time out on main: it compiles executables and starts Workers). test/cli has one file for a flag in other cases (console-depth.test.ts, user-agent.test.ts).
  • 61 rows. 43 fail on main 9b4284f:
    • 10 where =strict must refuse a macro (install --analyze, add --analyze, create in six places, two order cases),
    • 2 where the level of Node.js's flag must reach the macro,
    • 1 for a value that is not strict before the name of a template,
    • 3 for the order of the flags in bun lint and bun fmt,
    • 21 for a value that is not strict in BUN_OPTIONS, one per command,
    • 6 for such a value typed before a command.
  • 18 pass on main and pin what must not change: the error of the package manager and of bunx for a typed flag (4), -- in bun create, the arguments of a template, install --analyze with no flag and with no macro, bun build with the flag in BUN_OPTIONS, bun test with the flag on both sides, and the cases of bun lint and bun fmt that main has right (8).
  • bun upgrade and bun update --interactive were checked by hand (BUN_OPTIONS=<flag>=bogus gives the error). They are not rows: on a build without the fix the row would run a real upgrade.
  • The rows of bun lint and bun fmt run one at a time, with one thread. A thread of bun lint starts a VM, and in a debug build that takes seconds beside twenty other rows.
  • With LeakSanitizer on, bun install --analyze that completes exits 134 on main too (test/cli/install/bun-link.test.ts skips that case). The 5 rows that complete such a command do not pin the exit code when isASAN. The file passes in the environment of the ASAN lane, emulated here with the debug build: 61 pass. In that emulation bun build with a macro exits 134 too.
  • On Windows, bun create <component> fails where it writes App.build.ts, after the macros ran (ENOENT, src/runtime/cli/create/SourceFileProjectGenerator.rs:275, seen in CI). This change does not touch that code. On Windows the row for -- asserts only that the macro ran.
  • Also run on a debug build of this branch: test/cli/lint/help.test.ts 361 pass, the BUN_OPTIONS rows of lint.test.ts (3), format.test.ts (2), check.test.ts (1), the flag rows of js-plugins.test.ts (2).

Numbers

Release builds with --lto=off of main 9b4284f and of this branch at 8ed8be0, in one build directory, linux x64. The commit after 8ed8be0 changes comments in src/ and one test row. The stepi, malloc and syscall counts are from the commit before 8ed8be0: init, flags_of_bun and disallow_code_generation_from_strings_among have the same listings in the two builds. Tools: objdump, nm -S, size -A, gdb. strace, perf, valgrind and bloaty are not installed here, and apt has no candidate for them.

  • Startup chain (create_context_data, which, start, arguments::parse, exec_auto_or_run): 5 of 5 functions have the same size and the same instructions, +0 B (204 B and 53 instructions, 2,571 and 621, 1,529 and 349, 25,071 and 5,298, 8,481 and 1,719. objdump listings with addresses masked, then cmp).
  • bun test, bun repl, node, bun build and a compiled executable with flags: +28 instructions, +3 conditional branches and +1 call per process, in the cold command::init (111 B. objdump, and gdb stepi for bun test in an empty directory).
  • A command that reads its own flags, with no flag given: 58 to 84 instructions in the reader and no call (bun pm 58, bun install 61, bun lint 84. gdb stepi).
  • Release binary: .text 77,618,384 to 77,619,040 (+656 B). .rodata, .data and .bss +0. The stripped file has 102,713,144 B in both builds.
  • New symbols: command::init 111 B, arguments::flags_of_bun 553 B, arguments::disallow_code_generation_from_strings_among 365 B. Removed: lint_command::disallow_code_generation_before 534 B.
  • malloc calls (ASAN allocator statistics, debug builds, 3 runs each, main then branch): bun create 11 and 11, bun exec 24 and 24, bun check --help 35 and 35, bun x 12 and 12, bun install --help 83 and 83, bun lint 6,366 and 6,363.
  • With a flag before the command the walk makes one Vec: bun --silent create 13 and 14, bun --cwd=. exec 28 and 29. bun --cwd=. lint 6,403 and 6,401.
  • Syscalls (stops of gdb catch syscall, two for each call, main then branch): bun create 185 and 185, bun exec 187 and 187, bun x 185 and 185, bun install --help 191 and 191, bun build x.ts --outdir o up to its first thread 294 and 294. After the first thread the count of bun build is not stable on one binary.
  • New merge conflicts with Compile TypeScript to machine code ahead of time: bun build --compile --aot #45631, Make --no-macros a switch of the process #45937 and CLI: fixes for bun run, flags, bunfig.toml, .env, --filter, bun init and completions #46007: 0, 0 and 0 (git merge-file on the five edited files. The three PRs have conflicts with main already).
  • create_context_data( has two call sites: init and exec_auto_or_run.
  • A value that is not strict in BUN_OPTIONS now comes before --help for 20 commands: add, audit, dedupe, prune, x, check, lint, fmt, create, install, link, outdated, pm, patch, patch-commit, publish, remove, unlink, update, why. The help stays first for build, exec, upgrade, getcompletes and info.
  • The new test file takes 0.6 s on a release build and 12 to 14 s on a debug build.

Self-review

13 concerns. Done:

  • bun build is out of this change until a maintainer rules (it was in, with a named refusal for bun build --app that is out with it).
  • The place of the command is no longer a copy of the loop of which() alone. It is the further of that loop and place_of_first_word.
  • The package manager and bunx read BUN_OPTIONS only, so a typed flag with a wrong value keeps the error of main.
  • One function gives the flags of bun (arguments::flags_of_bun), for the next switch that needs them.
  • bun create <template> no longer reads the arguments of the template.
  • The docs no longer say that the flag works before bun install.
  • Rebased on bun check: fix errors lost since #45800, and 9 to 37% faster #46001. The comments that put init on the path of bun run are corrected.
  • New rows: --cwd <dir> before lint, fmt and check, a - before the command, bun fmt at both levels, the stop at --, a command that still works at =strict, bun test with the flag on both sides.
  • The count of rows that fail on main is taken again without timeouts.

Not done:

  • The reader does not skip the value of a flag. The case was --compile-exec-argv <flags>, a flag of bun build.
  • No row for the Windows case above. It cannot be run here.

… the commands that read their own flags

With `--disallow-code-generation-from-strings=strict` in `BUN_OPTIONS`, `bun install --analyze`,
`bun add --analyze` and `bun create <component>` ran macros. `arguments::parse` read the flag for
four command tags only, and `bun lint` and `bun fmt` had a helper of their own.

`command::init` is now a cold wrapper around `create_context_data`. For a command that `parse`
does not read the flag for, it reads the flag among the flags of `bun`: those that `BUN_OPTIONS`
adds, and those before the command. The last one counts, as in `parse`. The package manager and
bunx take it from `BUN_OPTIONS` only, and keep their error for a flag that is typed. `bun create`
also takes it after `create`. `bun lint` and `bun fmt` use this reader in place of their helper.
`bun <file>` and `bun run` call `create_context_data` directly. `bun build` is not read.
@coderabbitai

coderabbitai Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 51 seconds.

Check out review usage here.

View limit details

Limit details: You’ve used all 10 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Repository: oven-sh/bun/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 015b3b7f-68ee-4858-9eb3-b85335579ac1
📥 Commits

Reviewing files that changed from the base of the PR and between 9b4284f and f1893d2.

📒 Files selected for processing: 6
  • docs/runtime/code-generation-from-strings.mdx
  • src/runtime/cli/Arguments.rs
  • src/runtime/cli/lint_command.rs
  • src/runtime/cli/mod.rs
  • src/runtime/cli/script_or_command.rs
  • test/cli/disallow-code-generation-from-strings.test.ts
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@robobun

robobun commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 5:05 AM PT - Oct 21st, 2026

❌ @robobun, your commit f1893d2 has some failures in Build #129913 (All Failures)


🧪   To try this PR locally:

bunx bun-pr 46275

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

bun-46275 --bun

@robobun

robobun commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status

Reproduced on main 9b4284f with a macro that writes a marker file:

// macro.ts
import { writeFileSync } from "node:fs";
import { join } from "node:path";
export function f() {
  writeFileSync(join(import.meta.dir, "MACRO_RAN"), "");
  return "INLINED_RESULT";
}

// entry.ts
import { f } from "./macro.ts" with { type: "macro" };
console.log(f());
  • BUN_OPTIONS=--disallow-code-generation-from-strings=strict bun install --analyze ./entry.ts on main: the macro runs, MACRO_RAN exists, exit code 0. On this branch: error: Macros cannot be used with --disallow-code-generation-from-strings=strict, no MACRO_RAN, exit code 1.
  • The same holds for bun add --analyze ./entry.ts and for bun create ./App.tsx with a component that calls the macro.
  • The cases are in test/cli/disallow-code-generation-from-strings.test.ts: 43 of the 61 rows fail on main, and all pass on this branch.

PR: #46275

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

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Beyond the inline findings, I also checked whether flags_of_bun can read a command's own typed argument for exec, upgrade and the other USES_GLOBAL_OPTIONS commands — it slices argv only up to the command found by place_of_first_word, so a value typed after the command is never read. I also checked the lint/fmt change from max-wins (_as_compiled) to last-wins (_among): it matches what arguments::parse does for bun run, and the new test rows pin that order.

Extended reasoning...

The change makes command::init a cold wrapper that applies --disallow-code-generation-from-strings from BUN_OPTIONS and from the flags before the subcommand for commands whose own parser does not read runtime flags, with a second scan of bun create's own arguments; it touches CLI dispatch in src/runtime/cli/mod.rs and Arguments.rs and removes the bespoke lint/fmt helper. The surface is the security-relevant macro/eval gate, and the four inline findings (the -- edge in the create rescan, the pre-existing bun build gap, and the new hard exit on a non-strict value in BUN_OPTIONS for ~20 commands) plus the behavioral change to lint/fmt precedence warrant a human look.

Comment thread src/runtime/cli/mod.rs Resolved
Comment thread src/runtime/cli/mod.rs
Comment on lines +991 to +994
let ctx = create_context_data(cmd, log)?;
// `bun build` reads what stands before it, and what `BUN_OPTIONS` adds, with its own flags.
if !arguments::reads_runtime_flags(cmd) && cmd != Tag::BuildCommand {
arguments::disallow_code_generation_from_strings_among(arguments::flags_of_bun(cmd));

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.

🟡 (optional) Users with a mistyped or future value in BUN_OPTIONS now get exit 1 from bun install, bun upgrade, bun x, bun lint --help and about 20 other commands that ran on the base branch. init at src/runtime/cli/mod.rs:991-994 reads the flag from BUN_OPTIONS for every command that does not read runtime flags, before the command parses its own argv or prints help, and disallow_code_generation_from_strings at Arguments.rs:788-794 exits on any value other than strict or empty. Fix: for commands that run no JavaScript (upgrade, exec, getcompletes, pm, publish, outdated, audit, info, link, the --help paths) ignore or warn on an unknown value instead of exiting, or only validate when the level is about to be used.

Why this was flagged

The trigger is any BUN_OPTIONS containing --disallow-code-generation-from-strings=<x> with <x> not strict or empty, reaching command::init via the dispatch macro at mod.rs:1529. init calls flags_of_bun which always includes the BUN_OPTIONS tokens (Arguments.rs:821, 849), and disallow_code_generation_from_strings_among calls disallow_code_generation_from_strings, which prints the error and Global::exit(1) (Arguments.rs:789-793). On the base branch init was an alias of create_context_data and parse only read the flag for Auto/Run/Test/RunAsNode (Arguments.rs base:1048), so bun install, bun upgrade, bun install --help and the rest ran. The population is every CI or developer shell that exports BUN_OPTIONS globally; a value written for a newer or older bun, or a typo, now blocks bun upgrade itself, the command the user would run to recover. The PR counts this as an accepted downside, but 22 commands that run no JavaScript now fail on a value that affects nothing they do.

Verification: Trigger: BUN_OPTIONS contains --disallow-code-generation-from-strings=<x> with <x> neither empty nor strict. init (mod.rs:985-996) runs disallow_code_generation_from_strings (Arguments.rs:785-796), whose None arm prints the error and Global::exit(1). On the base branch these commands ran: arguments::parse read the flag only for the four runtime tags.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Kept as it is. The reasons:

  • On main the same value in BUN_OPTIONS already stops bun <file>, bun run, bun test and bun repl, with this error. A shell with that value cannot run a program, before and after this change.
  • The reader validates the value where it reads it, at startup, as arguments::parse does. With validation at first use, bun install passes with a mistyped value. bun install --analyze then fails in the same shell.
  • A list of commands that run no JavaScript fails open when it is out of date. bun install runs none, and bun install --analyze bundles. init has one exception, bun build, which waits for a ruling.
  • The error names the flag and the value. BUN_OPTIONS= bun upgrade runs the upgrade.

The cost is real. The PR body has it under Downsides: 22 more commands stop, and for 20 of them the error comes before --help.

Comment thread src/runtime/cli/mod.rs Resolved
The second read of `--disallow-code-generation-from-strings` in
`bun create` started at the first argument. A `--` before `create` ended
that read, so `bun -- create ./App.tsx <flag>=strict` ran macros. The
read now starts after `create`. `command::init` has read what stands
before it.

Tests:
- A row for `bun -- create <component> <flag>=strict`.
- A row that pins `bun build` with the flag in `BUN_OPTIONS`: it still
  runs macros.
- The row for `bun create <component> -- <flag>=strict` asserts only
  that the macro ran. On Windows the command then fails where it writes
  `App.build.ts` (`SourceFileProjectGenerator.rs:275`), which this
  change does not touch.
Comment thread src/runtime/cli/Arguments.rs Resolved
Comment thread src/runtime/cli/Arguments.rs Resolved
Comment thread src/runtime/cli/Arguments.rs Resolved
Comment thread src/runtime/cli/mod.rs Resolved
Comment thread src/runtime/cli/mod.rs Resolved
Comment thread src/runtime/cli/mod.rs Resolved

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

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

The latest push addresses the two points from my earlier review that had a code fix: the second bun create scan now starts at after_create (the index after the create positional, computed from remainder_i + 2 over args[1..]), so a -- from BUN_OPTIONS or before create no longer ends create's own flags, and the new bun build row with =strict in BUN_OPTIONS pins the BuildCommand exclusion in init. Beyond the inline nit, I also re-checked flags_of_bun's slice bounds (1.min(len)..end.min(len)) for an argv of length 1 and for BUN_OPTIONS longer than the typed argv — both clamp without panicking.

Extended reasoning...

The change makes command::init a cold wrapper that applies --disallow-code-generation-from-strings from BUN_OPTIONS and from the tokens before the command for every non-runtime command except bun build, adds a second read inside bun create, drops the lint/fmt helper, updates the docs page, and adds a 311-line CLI test file. It touches a security-relevant runtime gate (macro refusal under =strict) and the behaviour of about twenty commands when BUN_OPTIONS carries a mistyped value, which the author lists as a known downside rather than a bug. The one surviving finding is a test-assertion nit, and the design question about bun build and the exit-on-mistyped-value tradeoff are still open for a maintainer, so a human look remains worthwhile.

Still open from earlier reviews (2):

  • Unresolved: 2 minor or pre-existing.

No code changes in `src/`.

Test: the row for `bun create <component> -- <flag>=strict` asserts
again that the command writes its files, except on Windows, where it
asserts only that the macro ran.

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

Code review found no issues

No high-confidence issues detected in this change.

Still open from earlier reviews (1):

  • Unresolved: 1 minor or pre-existing.

This branch has not been deployed

No deployments
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