Skip to content

Hardening: fix crashes across the runtime, bundler, dev server, shell and CLI - #46021

Merged
Jarred-Sumner merged 32 commits into
mainfrom
claude/hardening
Oct 20, 2026
Merged

Jarred-Sumner merged 32 commits into
mainfrom
claude/hardening

Conversation

@Jarred-Sumner

@Jarred-Sumner Jarred-Sumner commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator

What does this PR do?

Hardening. This collects the open pull requests that fix a crash in Bun into one change, and fixes about 20 more crashes that had no pull request.

"Crash" here means a segfault, a Rust panic, an abort, a failed assertion, memory corruption that a sanitizer reports, a native stack overflow, or an exception that ends the process and that the program can't catch. With this PR those cases throw a normal error, print a normal CLI error and exit 1, or just work.

306 pull requests are included: 227 whole, and 79 cut down to the crash fix where the rest was a refactor, a feature or a behavior change. Each one was checked against current main first. 45 more turned out to be fixed already, and 17 were duplicates. The full list is at the bottom.

Crashes you could hit in normal use

Most of the rest needs unusual input: strings near the 2 GiB limit, buffers that another thread or a getter resizes or detaches in the middle of a call, worker.terminate() at a bad moment, running out of memory or file descriptors, very deep nesting, or hostile arguments to bun:ffi, node:vm and bun:sqlite.

Behavior changes

Things that didn't crash before and behave differently now:

File formats

Performance

Measured in instructions per operation on release builds of this branch and its base, with an A/A run for the noise floor (0.0 to 0.2 %).

No change: Bun.serve, node:http, WebSocket echo and publish, bun build of 3,807 modules, bun run with 2,000 imports, readdir / Glob / fs.cp over 23,689 entries, startup and RSS. The binary grows by 176 KB (0.17 %).

Faster Instructions
WebSocket echo of a 1 MB non-ASCII text message −72 %
Bun.inspect of a large non-ASCII string −39 %
Bun.markdown.html on large non-ASCII input −33 %
$`cmd` with 300 environment variables −6 %
fs.readSync −5 %
TextEncoder.encodeInto, short ASCII strings −4 %
async crypto.pbkdf2, 16-byte inputs −17 %
Slower Instructions Why
cc() calls +5 to +51 per call Argument count and type checks, and a check for close() (#37989, #38057, #43802, #44921). dlopen() calls don't change
process.env.X = v 38 → 593 per write Writes aren't cached. Reads don't change (#38871)
async crypto.pbkdf2, inputs of 1 KB or more +4 to +9 % Password and salt are copied (#40554)
bun build with an object --define used thousands of times +4.5 % The object is copied per use (#45969)

Not included

How did you verify your code works?

  • Tests. Every fix has a test that fails on main (a release or an ASAN build) and passes on this branch, except where that isn't practical: code that only runs on Windows or macOS, inputs that need several GB of memory, and a few rare races. Those were checked by hand or by reading.
  • Stored repro scripts. 230 crash repros ran on release and ASAN builds of this branch and of main, side by side. Release: 88 no longer crash. ASAN: 106 no longer crash. None crashes that didn't before. What still crashes needs WebKit, is SQLite's own bug, or isn't a crash. The ASAN number is from a later commit than the release number.
  • Dev server. Because one fix makes requests wait, 2,171 generated programs ran random interleavings of edits, build errors, failing plugins, reloads, stops and disconnects with about 120,000 requests. No request hung, was answered twice or got a stale body. main hung in 82 of 1,161 programs. This also found the four dev server bugs listed above.
  • Path helpers. 336,000 boundary cases with buffers of exactly the computed size, and 12,003 random calls through 30 JS APIs on an ASAN build, compared with main. No sanitizer report, and the results match wherever main doesn't panic.
  • What worked on 1.4.3 still works. Reviewers compared behavior with 1.4.3 and took out what changed it: for example an output directory layout change for 9 or more entry points, a server.reload() change, and a changed error message for import("file:///50%deposit.ts").
  • Not run locally: the Windows, macOS and FreeBSD code paths. CI compiles and tests them.

Included pull requests

Bundler, transpiler, CSS, macros (33)

bun build --compile and standalone executables (3)

Resolver, module loading, plugins, paths (25)

Dev server, bun bake, routers, --watch and --hot (28)

Bun.serve, sockets, HTTP, WebSocket, HTTP/2, QUIC, DNS (28)

Streams, Blob, S3, files, node:fs (28)

Node-API, V8 API, bun:ffi, process.dlopen (15)

node:vm, workers, process, timers (19)

Shell, spawn, CLI, bun test, bun install (31)

Web APIs, Buffer, encoding, URL, crypto, zlib, SQLite, Redis and the rest (96)

Already fixed on main, or duplicates of a pull request above

Already fixed: #27323 #30401 #31011 #31206 #31566 #31626 #31695 #31702 #31945 #32773 #34154 #34697 #34744 #34964 #35655 #35816 #36020 #36181 #36296 #36342 #36581 #36602 #36807 #36818 #36857 #37172 #37235 #37239 #37267 #37420 #37731 #37914 #38113 #38119 #38509 #38514 #38540 #38975 #39410 #40749 #41163 #41171 #42108 #43085 #43230

Duplicates: #45654 #29986 #33378 #36895 #37070 #37639 #38014 #39021 #41266 #42250 #42951 #43238 #44259 #45335 #45565 #45726 #45965

Fixes #5019
Fixes #6106
Fixes #10690
Fixes #10886
Fixes #13566
Fixes #14745
Fixes #15551
Fixes #20454
Fixes #21310
Fixes #29145
Fixes #30454
Fixes #30613
Fixes #30766
Fixes #32103
Fixes #32142
Fixes #32183
Fixes #32212
Fixes #39060
Fixes #42296
Fixes #42931
Fixes #43671
Fixes #44201
Fixes #44735
Fixes #44783
Fixes #45198
Fixes #45327
Fixes #45491
Fixes #45721

@robobun

robobun commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator
Updated 4:05 PM PT - Oct 20th, 2026

@Jarred-Sumner, your commit f48d297 is building: #129662

Comment thread src/runtime/napi/uv_posix.rs Resolved
Comment thread src/install/PackageInstaller.rs Resolved
Comment thread src/jsc/VirtualMachine.rs Resolved
Comment thread src/runtime/napi/uv_posix.rs Resolved
Comment thread src/runtime/shell/ParsedShellScript.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.

Beyond the inline findings, I also checked the Windows us_loop_free rework in packages/bun-usockets/src/eventing/libuv.c (uv_walk + bounded uv_loop_close retry) — every loop Bun creates passes a null hint to us_create_loop, so usockets owns the uv_loop it walks, the wait is deadline-bounded, and a loop that never closes is leaked rather than freed, so it is not a crash or misuse of a foreign loop.

Extended reasoning...

The us_loop_free change only runs on loops where is_default is 0, which is every loop in src/ since all us_create_loop call sites pass a null hint; the retry loop breaks at a 10s deadline and skips freeing the uv_loop when uv_loop_close still reports busy.

Findings marked 🟡 are optional suggestions and need no follow-up push.

Additional findings (outside the current diff — GitHub can't attach inline comments there):

  • 🔴 src/jsc/bindings/sqlite/JSSQLStatement.cpp — A bun:sqlite user whose connection has a commit/update hook or user function can hit a use-after-free of a finalized statement, which the base never did. jsSQLStatementFunctionIterateEnd copies castedThis->stmt into a local at JSSQLStatement.cpp:2272 and only then runs CHECK_NOT_BUSY at :2275. That macro calls finalizeOrphans (:2710), which sqlite3_finalize()s GC-orphaned statements and so runs user hook JS. If that JS calls stmt.finalize() or db.close(true), the local stmt is freed, and :2278 passes it to sqlite3_reset. Fix: re-read castedThis->stmt after any CHECK_NOT_BUSY / finalizeOrphans and bail when it is null, at every site that captures stmt or db before the macro.

    Why this was flagged

    Trigger: a connection with a hook, user function or authorizer (Bun::sqliteCommitsWhenFinalized) has a GC-orphaned statement in VersionSqlite3::orphans (JSSQLStatement.cpp:193); user code then calls the iterator's return()/end, reaching jsSQLStatementFunctionIterateEnd. The local stmt is read at :2272 before CHECK_NOT_BUSY at :2275. CHECK_NOT_BUSY (:160-161) calls finalizeOrphans (:2710), whose sqlite3_finalize commits and runs the user's commit hook JS, and reportUncaughtExceptionAtEventLoop (:2718) can also run JS. If that JS calls stmt.finalize() on the same statement or db.close(true), castedThis->stmt is nulled and the sqlite3_stmt freed, but the stale local is still used at :2278 (sqlite3_reset(stmt), sqlite3_db_handle(stmt)): heap use-after-free. On the base branch orphans did not exist; finalization happened in the GC finalizer so no user JS ran between the null check and the use. Remedy: order CHECK_NOT_BUSY before any read of castedThis->stmt and re-check stmt for null afterwards.

    Verification: In src/jsc/bindings/sqlite/JSSQLStatement.cpp, jsSQLStatementFunctionIterateEnd copies auto* stmt = castedThis->stmt; at :2272 and only then runs CHECK_NOT_BUSY at :2275. That macro (:160-161) now calls finalizeOrphans. :2278 calls runCount(stmt) and sqlite3_reset(stmt) on the stale local. In the base file CHECK_NOT_BUSY (base :155-159) is a pure inCallback check.

  • 🟣 src/runtime/node/node_fs.rs:2881 — pre-existing: async fs.readv/fs.writev on a view of a non-shared WebAssembly.Memory still reads or writes freed memory after grow(), while this PR fixes only fs.read. FdVectorIo::from_js at node_fs.rs:2873-2881 pins the backing stores, but a pin does not keep a Wasm block alive (array_buffer.rs:757 comment), so the iovecs captured there point at a freed block once memory.grow() runs before the pool thread's readv/preadv (node_fs.rs:6276) or uv_fs_read (node_fs.rs:869). Fix: give the vector path the same protection fs.read got at node_fs.rs:4159-4163, so every async fd I/O argument (read, readv, write, writev) copies or uses scratch for buffers that can_be_freed_while_pinned and writes back after completion.

    Why this was flagged

    A script calls fs.readv(fd, [new Uint8Array(mem.buffer)], cb) where mem is a non-shared WebAssembly.Memory, then calls mem.grow(1) before the callback runs. FdVectorIo::from_js (node_fs.rs:2873-2881) collects iovec pointers via Bun__JSArray__collectBufferSpans (bindings.cpp:7065-7071), which only pin()s the ArrayBuffer. The diff's own doc on ArrayBuffer::can_be_freed_while_pinned (array_buffer.rs:757-760) states grow() detaches a non-shared Wasm memory's buffer regardless of the pin count and frees the old block. The pool thread then runs Syscall::preadv/readv (node_fs.rs:6276, 6288) into that freed block: either EFAULT surfaces as a spurious I/O error, or, if the allocator reused the pages, the kernel writes file bytes over live memory. This PR added write_to_scratch_if_wasm/write_back for args::Read only (node_fs.rs:4159-4163, 1131-1134); the base branch had the same exposure, so readv/writev remain as on base. The pin at node_fs.rs:2878-2881 looks like the safeguard but, per array_buffer.rs:757, does not cover Wasm memory.

    Verification: node_fs.rs:2873-2881 FdVectorIo::from_js calls VectorArrayBuffer::from_js; nothing copies or scratches Wasm-backed views. array_buffer.rs:184-187: grow() detaches a non-shared WebAssembly.Memory's buffer regardless of the pin count. The raw iovecs are consumed off the JS thread at node_fs.rs:6276/6288. No equivalent of the fs.read fix (node_fs.rs:4159-4163) exists on the vector path.

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

Still open from earlier reviews (5):

  • 🔴 src/runtime/napi/uv_posix.rs:370 — A native addon thread that keeps calling uv_async_send faster than its callback returns can pin the JS thread inside on…
  • Also unresolved: 4 minor or pre-existing.

If you have decided not to act on one of these findings, resolve its thread (a reply alone leaves it open) and the next review stops counting it. To review this commit again now, use Re-run on its "Claude Code Review" check.

Comment thread src/jsc/bindings/ZigGlobalObject.cpp Resolved
Comment thread src/runtime/ffi/ffi_body.rs Resolved
Comment thread src/runtime/ffi/ffi_body.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.

Findings marked 🟡 are optional suggestions and need no follow-up push.

Comment thread test/js/bun/ffi/cc.test.ts Resolved
robobun added a commit to robobun/soup that referenced this pull request Nov 1, 2026
A variable in double quotes that is empty or not set expanded to no
argument. In POSIX, and in bash and dash, it is one empty argument, and
scripts are written for that. Without it these ran another command and
reported no error:

    [ -n "$CI" ] || bun run build      # ran `[ -n ]`, which is true
    [ -f "$CONFIG" ] && echo found     # ran `[ -f ]`, which is true
    git commit -m "$MESSAGE" --amend   # -m took --amend as the message
    export "$NAME"                     # ran `export`, which prints all
    cd "$DIR" && rm -rf build          # ran `cd`, which goes home

"$(command)" did the same when the command printed nothing, and so did
"$1". oven-sh#27231 kept the literal "" and '', and
oven-sh#46021 kept a quoted substitution that printed only NUL
bytes. This keeps the rest:

- The lexer ends a double-quoted region that held nothing but variables
  with a DoubleQuotedText token of length zero, which the parser already
  reads as a quoted empty. A region with text or a substitution in it
  gets no token.
- A quoted substitution marks its word as quoted, whatever it printed.
- A word that expands to "" where the command name goes fails with
  `bun: command not found: ` and exit code 1. Before, the command was
  skipped with exit code 0.

What changes for a script that was not broken, against bun 1.4.3:

- `"$UNSET" args` ran `args`. It fails now, as in bash and dash.
  `"" args`, and `${""} args` from JavaScript, ran nothing and exited 0.
  They fail too.
- A command gets the empty operand. `cd "$UNSET"` stays where it is (it
  went to the home directory), `ls "$UNSET"` fails (it listed the cwd),
  `cat "$UNSET"` fails (it read stdin) and `exit "$UNSET"` fails (it
  exited 0).
- `export "$UNSET"` fails with `export: not a valid variable name: ""`
  (it printed every exported variable). export skipped an empty argument
  before, so `export ""` fails now too, and so does `export =value`,
  which put a variable with no name into the environment. The other
  arguments are still exported.
- `~"$UNSET"` is one empty argument (it was none). It is not the home
  directory, which only the literal `~""` is. For the same reason
  `~""$UNSET` is an empty argument now. It was the home directory.
- `{"$A","$A"}` with an empty A is one empty argument (it was none).

Not changed: $VAR without quotes is no argument, "${VAR}" is not
expanded, and a brace expansion still drops an empty alternative at its
front, so `{"$A",b}` is `b`.

This needs the commit before it, which makes the file builtins fail on
an empty operand. Without that commit `touch "$UNSET"` touches the cwd,
and on Windows `rm -rf "$UNSET"` would empty it.

The substitution and the command name follow df0dbd6 and daa5626
of the branch farm/e0c3d2d4/shell-brace-empty-argv in oven-sh/bun, which
is not merged.

Neither commit was run on Windows. The Windows target passes cargo
check.
robobun added a commit to robobun/soup that referenced this pull request Nov 2, 2026
A variable in double quotes that is empty or not set expanded to no
argument. In POSIX, and in bash and dash, it is one empty argument, and
scripts are written for that. Without it these ran another command and
reported no error:

    [ -n "$CI" ] || bun run build      # ran `[ -n ]`, which is true
    [ -f "$CONFIG" ] && echo found     # ran `[ -f ]`, which is true
    git commit -m "$MESSAGE" --amend   # -m took --amend as the message
    export "$NAME"                     # ran `export`, which prints all
    cd "$DIR" && rm -rf build          # ran `cd`, which goes home

"$(command)" did the same when the command printed nothing, and so did
"$1". oven-sh#27231 kept the literal "" and '', and
oven-sh#46021 kept a quoted substitution that printed only NUL
bytes. This keeps the rest:

- The lexer ends a double-quoted region that held nothing but variables
  with a DoubleQuotedText token of length zero, which the parser already
  reads as a quoted empty. A region with text or a substitution in it
  gets no token.
- A quoted substitution marks its word as quoted, whatever it printed.
- A word that expands to "" where the command name goes fails with
  `bun: command not found: ` and exit code 1. Before, the command was
  skipped with exit code 0.

What changes for a script that was not broken, against bun 1.4.3:

- `"$UNSET" args` ran `args`. It fails now, as in bash and dash.
  `"" args`, and `${""} args` from JavaScript, ran nothing and exited 0.
  They fail too.
- A command gets the empty operand. `cd "$UNSET"` stays where it is (it
  went to the home directory), `ls "$UNSET"` fails (it listed the cwd),
  `cat "$UNSET"` fails (it read stdin) and `exit "$UNSET"` fails (it
  exited 0).
- `export "$UNSET"` fails with `export: not a valid variable name: ""`
  (it printed every exported variable). export skipped an empty argument
  before, so `export ""` fails now too, and so does `export =value`,
  which put a variable with no name into the environment. The other
  arguments are still exported.
- `~"$UNSET"` is one empty argument (it was none). It is not the home
  directory, which only the literal `~""` is. For the same reason
  `~""$UNSET` is an empty argument now. It was the home directory.
- `{"$A","$A"}` with an empty A is one empty argument (it was none).

Not changed: $VAR without quotes is no argument, "${VAR}" is not
expanded, and a brace expansion still drops an empty alternative at its
front, so `{"$A",b}` is `b`.

This needs the commit before it, which makes the file builtins fail on
an empty operand. Without that commit `touch "$UNSET"` touches the cwd,
and on Windows `rm -rf "$UNSET"` would empty it.

The substitution and the command name follow df0dbd6 and daa5626
of the branch farm/e0c3d2d4/shell-brace-empty-argv in oven-sh/bun, which
is not merged.

Neither commit was run on Windows. The Windows target passes cargo
check.
robobun added a commit to robobun/soup that referenced this pull request Nov 4, 2026
A variable in double quotes that is empty or not set expanded to no
argument. In POSIX, and in bash and dash, it is one empty argument, and
scripts are written for that. Without it these ran another command and
reported no error:

    [ -n "$CI" ] || bun run build      # ran `[ -n ]`, which is true
    [ -f "$CONFIG" ] && echo found     # ran `[ -f ]`, which is true
    git commit -m "$MESSAGE" --amend   # -m took --amend as the message
    export "$NAME"                     # ran `export`, which prints all
    cd "$DIR" && rm -rf build          # ran `cd`, which goes home

"$(command)" did the same when the command printed nothing, and so did
"$1". oven-sh#27231 kept the literal "" and '', and
oven-sh#46021 kept a quoted substitution that printed only NUL
bytes. This keeps the rest:

- The lexer ends a double-quoted region that held nothing but variables
  with a DoubleQuotedText token of length zero, which the parser already
  reads as a quoted empty. A region with text or a substitution in it
  gets no token.
- A quoted substitution marks its word as quoted, whatever it printed.
- A word that expands to "" where the command name goes fails with
  `bun: command not found: ` and exit code 1. Before, the command was
  skipped with exit code 0.

What changes for a script that was not broken, against bun 1.4.3:

- `"$UNSET" args` ran `args`. It fails now, as in bash and dash.
  `"" args`, and `${""} args` from JavaScript, ran nothing and exited 0.
  They fail too.
- A command gets the empty operand. `cd "$UNSET"` stays where it is (it
  went to the home directory), `ls "$UNSET"` fails (it listed the cwd),
  `cat "$UNSET"` fails (it read stdin) and `exit "$UNSET"` fails (it
  exited 0).
- `export "$UNSET"` fails with `export: not a valid variable name: ""`
  (it printed every exported variable). export skipped an empty argument
  before, so `export ""` fails now too, and so does `export =value`,
  which put a variable with no name into the environment. The other
  arguments are still exported.
- `~"$UNSET"` is one empty argument (it was none). It is not the home
  directory, which only the literal `~""` is. For the same reason
  `~""$UNSET` is an empty argument now. It was the home directory.
- `{"$A","$A"}` with an empty A is one empty argument (it was none).

Not changed: $VAR without quotes is no argument, "${VAR}" is not
expanded, and a brace expansion still drops an empty alternative at its
front, so `{"$A",b}` is `b`.

This needs the commit before it, which makes the file builtins fail on
an empty operand. Without that commit `touch "$UNSET"` touches the cwd,
and on Windows `rm -rf "$UNSET"` would empty it.

The substitution and the command name follow df0dbd6 and daa5626
of the branch farm/e0c3d2d4/shell-brace-empty-argv in oven-sh/bun, which
is not merged.

Neither commit was run on Windows. The Windows target passes cargo
check.
robobun added a commit to robobun/soup that referenced this pull request Nov 4, 2026
A variable in double quotes that is empty or not set expanded to no
argument. In POSIX, and in bash and dash, it is one empty argument, and
scripts are written for that. Without it these ran another command and
reported no error:

    [ -n "$CI" ] || bun run build      # ran `[ -n ]`, which is true
    [ -f "$CONFIG" ] && echo found     # ran `[ -f ]`, which is true
    git commit -m "$MESSAGE" --amend   # -m took --amend as the message
    export "$NAME"                     # ran `export`, which prints all
    cd "$DIR" && rm -rf build          # ran `cd`, which goes home

"$(command)" did the same when the command printed nothing, and so did
"$1". oven-sh#27231 kept the literal "" and '', and
oven-sh#46021 kept a quoted substitution that printed only NUL
bytes. This keeps the rest:

- The lexer ends a double-quoted region that held nothing but variables
  with a DoubleQuotedText token of length zero, which the parser already
  reads as a quoted empty. A region with text or a substitution in it
  gets no token.
- A quoted substitution marks its word as quoted, whatever it printed.
- A word that expands to "" where the command name goes fails with
  `bun: command not found: ` and exit code 1. Before, the command was
  skipped with exit code 0.

What changes for a script that was not broken, against bun 1.4.3:

- `"$UNSET" args` ran `args`. It fails now, as in bash and dash.
  `"" args`, and `${""} args` from JavaScript, ran nothing and exited 0.
  They fail too.
- A command gets the empty operand. `cd "$UNSET"` stays where it is (it
  went to the home directory), `ls "$UNSET"` fails (it listed the cwd),
  `cat "$UNSET"` fails (it read stdin) and `exit "$UNSET"` fails (it
  exited 0).
- `export "$UNSET"` fails with `export: not a valid variable name: ""`
  (it printed every exported variable). export skipped an empty argument
  before, so `export ""` fails now too, and so does `export =value`,
  which put a variable with no name into the environment. The other
  arguments are still exported.
- `~"$UNSET"` is one empty argument (it was none). It is not the home
  directory, which only the literal `~""` is. For the same reason
  `~""$UNSET` is an empty argument now. It was the home directory.
- `{"$A","$A"}` with an empty A is one empty argument (it was none).

Not changed: $VAR without quotes is no argument, "${VAR}" is not
expanded, and a brace expansion still drops an empty alternative at its
front, so `{"$A",b}` is `b`.

This needs the commit before it, which makes the file builtins fail on
an empty operand. Without that commit `touch "$UNSET"` touches the cwd,
and on Windows `rm -rf "$UNSET"` would empty it.

The substitution and the command name follow df0dbd6 and daa5626
of the branch farm/e0c3d2d4/shell-brace-empty-argv in oven-sh/bun, which
is not merged.

Neither commit was run on Windows. The Windows target passes cargo
check.
robobun added a commit to robobun/soup that referenced this pull request Nov 6, 2026
A variable in double quotes that is empty or not set expanded to no
argument. In POSIX, and in bash and dash, it is one empty argument, and
scripts are written for that. Without it these ran another command and
reported no error:

    [ -n "$CI" ] || bun run build      # ran `[ -n ]`, which is true
    [ -f "$CONFIG" ] && echo found     # ran `[ -f ]`, which is true
    git commit -m "$MESSAGE" --amend   # -m took --amend as the message
    export "$NAME"                     # ran `export`, which prints all
    cd "$DIR" && rm -rf build          # ran `cd`, which goes home

"$(command)" did the same when the command printed nothing, and so did
"$1". oven-sh#27231 kept the literal "" and '', and
oven-sh#46021 kept a quoted substitution that printed only NUL
bytes. This keeps the rest:

- The lexer ends a double-quoted region that held nothing but variables
  with a DoubleQuotedText token of length zero, which the parser already
  reads as a quoted empty. A region with text or a substitution in it
  gets no token.
- A quoted substitution marks its word as quoted, whatever it printed.
- A word that expands to "" where the command name goes fails with
  `bun: command not found: ` and exit code 1. Before, the command was
  skipped with exit code 0.

What changes for a script that was not broken, against bun 1.4.3:

- `"$UNSET" args` ran `args`. It fails now, as in bash and dash.
  `"" args`, and `${""} args` from JavaScript, ran nothing and exited 0.
  They fail too.
- A command gets the empty operand. `cd "$UNSET"` stays where it is (it
  went to the home directory), `ls "$UNSET"` fails (it listed the cwd),
  `cat "$UNSET"` fails (it read stdin) and `exit "$UNSET"` fails (it
  exited 0).
- `export "$UNSET"` fails with `export: not a valid variable name: ""`
  (it printed every exported variable). export skipped an empty argument
  before, so `export ""` fails now too, and so does `export =value`,
  which put a variable with no name into the environment. The other
  arguments are still exported.
- `~"$UNSET"` is one empty argument (it was none). It is not the home
  directory, which only the literal `~""` is. For the same reason
  `~""$UNSET` is an empty argument now. It was the home directory.
- `{"$A","$A"}` with an empty A is one empty argument (it was none).

Not changed: $VAR without quotes is no argument, "${VAR}" is not
expanded, and a brace expansion still drops an empty alternative at its
front, so `{"$A",b}` is `b`.

This needs the commit before it, which makes the file builtins fail on
an empty operand. Without that commit `touch "$UNSET"` touches the cwd,
and on Windows `rm -rf "$UNSET"` would empty it.

The substitution and the command name follow df0dbd6 and daa5626
of the branch farm/e0c3d2d4/shell-brace-empty-argv in oven-sh/bun, which
is not merged.

Neither commit was run on Windows. The Windows target passes cargo
check.
robobun added a commit that referenced this pull request Nov 6, 2026
main has the define through the receiver at the four sites (#46021).
This branch keeps what main does not have: the emitter stays alive
during a call, the methods that only read do not define this._events,
and an "error" event with no listener is reported when the emitter
has no receiver stored.
robobun added a commit to robobun/soup that referenced this pull request Nov 7, 2026
A variable in double quotes that is empty or not set expanded to no
argument. In POSIX, and in bash and dash, it is one empty argument, and
scripts are written for that. Without it these ran another command and
reported no error:

    [ -n "$CI" ] || bun run build      # ran `[ -n ]`, which is true
    [ -f "$CONFIG" ] && echo found     # ran `[ -f ]`, which is true
    git commit -m "$MESSAGE" --amend   # -m took --amend as the message
    export "$NAME"                     # ran `export`, which prints all
    cd "$DIR" && rm -rf build          # ran `cd`, which goes home

"$(command)" did the same when the command printed nothing, and so did
"$1". oven-sh#27231 kept the literal "" and '', and
oven-sh#46021 kept a quoted substitution that printed only NUL
bytes. This keeps the rest:

- The lexer ends a double-quoted region that held nothing but variables
  with a DoubleQuotedText token of length zero, which the parser already
  reads as a quoted empty. A region with text or a substitution in it
  gets no token.
- A quoted substitution marks its word as quoted, whatever it printed.
- A word that expands to "" where the command name goes fails with
  `bun: command not found: ` and exit code 1. Before, the command was
  skipped with exit code 0.

What changes for a script that was not broken, against bun 1.4.3:

- `"$UNSET" args` ran `args`. It fails now, as in bash and dash.
  `"" args`, and `${""} args` from JavaScript, ran nothing and exited 0.
  They fail too.
- A command gets the empty operand. `cd "$UNSET"` stays where it is (it
  went to the home directory), `ls "$UNSET"` fails (it listed the cwd),
  `cat "$UNSET"` fails (it read stdin) and `exit "$UNSET"` fails (it
  exited 0).
- `export "$UNSET"` fails with `export: not a valid variable name: ""`
  (it printed every exported variable). export skipped an empty argument
  before, so `export ""` fails now too, and so does `export =value`,
  which put a variable with no name into the environment. The other
  arguments are still exported.
- `~"$UNSET"` is one empty argument (it was none). It is not the home
  directory, which only the literal `~""` is. For the same reason
  `~""$UNSET` is an empty argument now. It was the home directory.
- `{"$A","$A"}` with an empty A is one empty argument (it was none).

Not changed: $VAR without quotes is no argument, "${VAR}" is not
expanded, and a brace expansion still drops an empty alternative at its
front, so `{"$A",b}` is `b`.

This needs the commit before it, which makes the file builtins fail on
an empty operand. Without that commit `touch "$UNSET"` touches the cwd,
and on Windows `rm -rf "$UNSET"` would empty it.

The substitution and the command name follow df0dbd6 and daa5626
of the branch farm/e0c3d2d4/shell-brace-empty-argv in oven-sh/bun, which
is not merged.

Neither commit was run on Windows. The Windows target passes cargo
check.
robobun added a commit that referenced this pull request Nov 8, 2026
…iler cache version

Since #46021, main leaves a function with a try body that cannot throw
as written. The change to remove_unnecessary_try_catch fixes no wrong
output there, and it made functions with a do-while whose test is
unreachable compile to wrong code. The helper is back to the text of
main. Its test and the two todo tests leave with it.

bun test --react-compiler keeps compiled output in the runtime
transpiler cache, so EXPECTED_VERSION goes from 51 to 52.
robobun added a commit to robobun/soup that referenced this pull request Nov 8, 2026
A variable in double quotes that is empty or not set expanded to no
argument. In POSIX, and in bash and dash, it is one empty argument, and
scripts are written for that. Without it these ran another command and
reported no error:

    [ -n "$CI" ] || bun run build      # ran `[ -n ]`, which is true
    [ -f "$CONFIG" ] && echo found     # ran `[ -f ]`, which is true
    git commit -m "$MESSAGE" --amend   # -m took --amend as the message
    export "$NAME"                     # ran `export`, which prints all
    cd "$DIR" && rm -rf build          # ran `cd`, which goes home

"$(command)" did the same when the command printed nothing, and so did
"$1". oven-sh#27231 kept the literal "" and '', and
oven-sh#46021 kept a quoted substitution that printed only NUL
bytes. This keeps the rest:

- The lexer ends a double-quoted region that held nothing but variables
  with a DoubleQuotedText token of length zero, which the parser already
  reads as a quoted empty. A region with text or a substitution in it
  gets no token.
- A quoted substitution marks its word as quoted, whatever it printed.
- A word that expands to "" where the command name goes fails with
  `bun: command not found: ` and exit code 1. Before, the command was
  skipped with exit code 0.

What changes for a script that was not broken, against bun 1.4.3:

- `"$UNSET" args` ran `args`. It fails now, as in bash and dash.
  `"" args`, and `${""} args` from JavaScript, ran nothing and exited 0.
  They fail too.
- A command gets the empty operand. `cd "$UNSET"` stays where it is (it
  went to the home directory), `ls "$UNSET"` fails (it listed the cwd),
  `cat "$UNSET"` fails (it read stdin) and `exit "$UNSET"` fails (it
  exited 0).
- `export "$UNSET"` fails with `export: not a valid variable name: ""`
  (it printed every exported variable). export skipped an empty argument
  before, so `export ""` fails now too, and so does `export =value`,
  which put a variable with no name into the environment. The other
  arguments are still exported.
- `~"$UNSET"` is one empty argument (it was none). It is not the home
  directory, which only the literal `~""` is. For the same reason
  `~""$UNSET` is an empty argument now. It was the home directory.
- `{"$A","$A"}` with an empty A is one empty argument (it was none).

Not changed: $VAR without quotes is no argument, "${VAR}" is not
expanded, and a brace expansion still drops an empty alternative at its
front, so `{"$A",b}` is `b`.

This needs the commit before it, which makes the file builtins fail on
an empty operand. Without that commit `touch "$UNSET"` touches the cwd,
and on Windows `rm -rf "$UNSET"` would empty it.

The substitution and the command name follow df0dbd6 and daa5626
of the branch farm/e0c3d2d4/shell-brace-empty-argv in oven-sh/bun, which
is not merged.

Neither commit was run on Windows. The Windows target passes cargo
check.
robobun added a commit to robobun/soup that referenced this pull request Nov 10, 2026
A variable in double quotes that is empty or not set expanded to no
argument. In POSIX, and in bash and dash, it is one empty argument, and
scripts are written for that. Without it these ran another command and
reported no error:

    [ -n "$CI" ] || bun run build      # ran `[ -n ]`, which is true
    [ -f "$CONFIG" ] && echo found     # ran `[ -f ]`, which is true
    git commit -m "$MESSAGE" --amend   # -m took --amend as the message
    export "$NAME"                     # ran `export`, which prints all
    cd "$DIR" && rm -rf build          # ran `cd`, which goes home

"$(command)" did the same when the command printed nothing, and so did
"$1". oven-sh#27231 kept the literal "" and '', and
oven-sh#46021 kept a quoted substitution that printed only NUL
bytes. This keeps the rest:

- The lexer ends a double-quoted region that held nothing but variables
  with a DoubleQuotedText token of length zero, which the parser already
  reads as a quoted empty. A region with text or a substitution in it
  gets no token.
- A quoted substitution marks its word as quoted, whatever it printed.
- A word that expands to "" where the command name goes fails with
  `bun: command not found: ` and exit code 1. Before, the command was
  skipped with exit code 0.

What changes for a script that was not broken, against bun 1.4.3:

- `"$UNSET" args` ran `args`. It fails now, as in bash and dash.
  `"" args`, and `${""} args` from JavaScript, ran nothing and exited 0.
  They fail too.
- A command gets the empty operand. `cd "$UNSET"` stays where it is (it
  went to the home directory), `ls "$UNSET"` fails (it listed the cwd),
  `cat "$UNSET"` fails (it read stdin) and `exit "$UNSET"` fails (it
  exited 0).
- `export "$UNSET"` fails with `export: not a valid variable name: ""`
  (it printed every exported variable). export skipped an empty argument
  before, so `export ""` fails now too, and so does `export =value`,
  which put a variable with no name into the environment. The other
  arguments are still exported.
- `~"$UNSET"` is one empty argument (it was none). It is not the home
  directory, which only the literal `~""` is. For the same reason
  `~""$UNSET` is an empty argument now. It was the home directory.
- `{"$A","$A"}` with an empty A is one empty argument (it was none).

Not changed: $VAR without quotes is no argument, "${VAR}" is not
expanded, and a brace expansion still drops an empty alternative at its
front, so `{"$A",b}` is `b`.

This needs the commit before it, which makes the file builtins fail on
an empty operand. Without that commit `touch "$UNSET"` touches the cwd,
and on Windows `rm -rf "$UNSET"` would empty it.

The substitution and the command name follow df0dbd6 and daa5626
of the branch farm/e0c3d2d4/shell-brace-empty-argv in oven-sh/bun, which
is not merged.

Neither commit was run on Windows. The Windows target passes cargo
check.
robobun added a commit to robobun/soup that referenced this pull request Nov 10, 2026
A variable in double quotes that is empty or not set expanded to no
argument. In POSIX, and in bash and dash, it is one empty argument, and
scripts are written for that. Without it these ran another command and
reported no error:

    [ -n "$CI" ] || bun run build      # ran `[ -n ]`, which is true
    [ -f "$CONFIG" ] && echo found     # ran `[ -f ]`, which is true
    git commit -m "$MESSAGE" --amend   # -m took --amend as the message
    export "$NAME"                     # ran `export`, which prints all
    cd "$DIR" && rm -rf build          # ran `cd`, which goes home

"$(command)" did the same when the command printed nothing, and so did
"$1". oven-sh#27231 kept the literal "" and '', and
oven-sh#46021 kept a quoted substitution that printed only NUL
bytes. This keeps the rest:

- The lexer ends a double-quoted region that held nothing but variables
  with a DoubleQuotedText token of length zero, which the parser already
  reads as a quoted empty. A region with text or a substitution in it
  gets no token.
- A quoted substitution marks its word as quoted, whatever it printed.
- A word that expands to "" where the command name goes fails with
  `bun: command not found: ` and exit code 1. Before, the command was
  skipped with exit code 0.

What changes for a script that was not broken, against bun 1.4.3:

- `"$UNSET" args` ran `args`. It fails now, as in bash and dash.
  `"" args`, and `${""} args` from JavaScript, ran nothing and exited 0.
  They fail too.
- A command gets the empty operand. `cd "$UNSET"` stays where it is (it
  went to the home directory), `ls "$UNSET"` fails (it listed the cwd),
  `cat "$UNSET"` fails (it read stdin) and `exit "$UNSET"` fails (it
  exited 0).
- `export "$UNSET"` fails with `export: not a valid variable name: ""`
  (it printed every exported variable). export skipped an empty argument
  before, so `export ""` fails now too, and so does `export =value`,
  which put a variable with no name into the environment. The other
  arguments are still exported.
- `~"$UNSET"` is one empty argument (it was none). It is not the home
  directory, which only the literal `~""` is. For the same reason
  `~""$UNSET` is an empty argument now. It was the home directory.
- `{"$A","$A"}` with an empty A is one empty argument (it was none).

Not changed: $VAR without quotes is no argument, "${VAR}" is not
expanded, and a brace expansion still drops an empty alternative at its
front, so `{"$A",b}` is `b`.

This needs the commit before it, which makes the file builtins fail on
an empty operand. Without that commit `touch "$UNSET"` touches the cwd,
and on Windows `rm -rf "$UNSET"` would empty it.

The substitution and the command name follow df0dbd6 and daa5626
of the branch farm/e0c3d2d4/shell-brace-empty-argv in oven-sh/bun, which
is not merged.

Neither commit was run on Windows. The Windows target passes cargo
check.
robobun added a commit to robobun/soup that referenced this pull request Nov 11, 2026
A variable in double quotes that is empty or not set expanded to no
argument. In POSIX, and in bash and dash, it is one empty argument, and
scripts are written for that. Without it these ran another command and
reported no error:

    [ -n "$CI" ] || bun run build      # ran `[ -n ]`, which is true
    [ -f "$CONFIG" ] && echo found     # ran `[ -f ]`, which is true
    git commit -m "$MESSAGE" --amend   # -m took --amend as the message
    export "$NAME"                     # ran `export`, which prints all
    cd "$DIR" && rm -rf build          # ran `cd`, which goes home

"$(command)" did the same when the command printed nothing, and so did
"$1". oven-sh#27231 kept the literal "" and '', and
oven-sh#46021 kept a quoted substitution that printed only NUL
bytes. This keeps the rest:

- The lexer ends a double-quoted region that held nothing but variables
  with a DoubleQuotedText token of length zero, which the parser already
  reads as a quoted empty. A region with text or a substitution in it
  gets no token.
- A quoted substitution marks its word as quoted, whatever it printed.
- A word that expands to "" where the command name goes fails with
  `bun: command not found: ` and exit code 1. Before, the command was
  skipped with exit code 0.

What changes for a script that was not broken, against bun 1.4.3:

- `"$UNSET" args` ran `args`. It fails now, as in bash and dash.
  `"" args`, and `${""} args` from JavaScript, ran nothing and exited 0.
  They fail too.
- A command gets the empty operand. `cd "$UNSET"` stays where it is (it
  went to the home directory), `ls "$UNSET"` fails (it listed the cwd),
  `cat "$UNSET"` fails (it read stdin) and `exit "$UNSET"` fails (it
  exited 0).
- `export "$UNSET"` fails with `export: not a valid variable name: ""`
  (it printed every exported variable). export skipped an empty argument
  before, so `export ""` fails now too, and so does `export =value`,
  which put a variable with no name into the environment. The other
  arguments are still exported.
- `~"$UNSET"` is one empty argument (it was none). It is not the home
  directory, which only the literal `~""` is. For the same reason
  `~""$UNSET` is an empty argument now. It was the home directory.
- `{"$A","$A"}` with an empty A is one empty argument (it was none).

Not changed: $VAR without quotes is no argument, "${VAR}" is not
expanded, and a brace expansion still drops an empty alternative at its
front, so `{"$A",b}` is `b`.

This needs the commit before it, which makes the file builtins fail on
an empty operand. Without that commit `touch "$UNSET"` touches the cwd,
and on Windows `rm -rf "$UNSET"` would empty it.

The substitution and the command name follow df0dbd6 and daa5626
of the branch farm/e0c3d2d4/shell-brace-empty-argv in oven-sh/bun, which
is not merged.

Neither commit was run on Windows. The Windows target passes cargo
check.
nitrobass24 added a commit to nitrobass24/harbrr that referenced this pull request Nov 11, 2026
Bun 1.4.4 ships the node:vm fix (oven-sh/bun#46021) for the EventTarget
brand check that broke every vitest worker with jsdom >= 30.1 on Linux.
Move the CI images and packageManager to 1.4.4 in one step (the bun-pin
sync check requires all references to agree) and restore jsdom to a
caret range; the lockfile resolves 31.0.0.

Verified in the oven/bun:1.4.4-alpine image on linux/amd64: 55 files,
481 tests pass with jsdom 31.0.0 (1.4.0 fails all 55 files).

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

Labels

None yet

Projects

None yet

2 participants