You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
EXT_texture_compression_rgtc is fully supported in Firefox
I'm not sure where the partial implementation came from, but it definitely seems to be true today that this is supported everywhere, and I'm struggling to find any evidence from bugzilla that it wasn't supported on all platforms from the start (at least if they had the necessary hardware support).
db51e2text-decoration-skip-ink: link to Safari rendering bug (#29664)
This is a longstanding problem that hasn't been widely complained about,
so it's not a partial. But it did come up in my search when text-decoration-skip-ink: all advanced to Baseline newly available.
$ echo J1x48LmRnCf/ | base64 -d | bun-debug -panic: attempt to subtract with overflow (src/js_parser/lexer.rs:803:41)
The input is ' \ x F0 B9 91 9C ' FF — a string literal starting with \x immediately followed by a 4-byte UTF-8 codepoint.
Cause
In decode_escape_sequences, several error paths place the diagnostic
caret by computing start + iter.i - widthN — the position of the
offending character minus its byte width. When that character is the
very first thing in the string literal, iter.i can be smaller than widthN (e.g. iter.i = 1, widthN = 4), and the usize subtraction
underflows. Debug builds panic; release silently wraps to a nonsense self.end.
The original Zig source used -| (saturating subtract) in two of these
spots (lexer.zig:511, 517), but the rest used raw subtraction —
inherited as-is by the Rust port.
Fix
Use saturating_sub in every such error path in decode_escape_sequences. The result is only used to produce a caret
position inside an error message; saturating to 0 when iter.i < widthN
still gives a reasonable location. Patched sites:
2-digit \x — first hex invalid (the reported panic)
2-digit \x — second hex invalid
\u fixed-length — any of 4 hex digits invalid
JSON-mode checks inside \0, \u{, \r, \n, and the catch-all
non-JSON-allowed escape
octal_start computation for the legacy-octal range error
Four new tests in test/regression/issue/invalid-escape-sequences.test.ts cover \x
(1st-hex), \x2 (2nd-hex), \u (fixed-length), and \u{
(variable-length) each followed by a 4-byte codepoint. Without the fix:
3 of the 4 panic on their respective lines (803/817/909); the \u{
variant already used saturating_sub so it passes either way but is
retained for coverage.
� decodes to 0x76B22B, well beyond the maximum valid Unicode
codepoint U+10FFFF.
Cause
maybe_decode_jsx_entity (src/js_parser/lexer.rs:3312) parses the
numeric entity as i32 and only errors on InvalidCharacter / Overflow from bun_core::parse_int. Any value that fits in i32 is
forwarded to strings::push_codepoint_utf16 (called from decode_jsx_entities), which calls encode_surrogate_pair → u16_lead / u16_trail. Both assert supplementary <= 0x10FFFF
(src/bun_core/lib.rs:1319 and :1326). Debug builds panic; release
builds silently encode garbage surrogate pairs (the Zig original used @​truncate inline and hid the problem).
Fix
Reject values outside the Unicode range 0..=0x10FFFF in the same
arm that handles parse_int success, emitting the existing
"JSX entity escape is too big" diagnostic and substituting UNICODE_REPLACEMENT (U+FFFD) — matching the Overflow branch.
Verification
$ bun-debug -e 'const x = <div>�</div>'1 | const x = <div>�</div> ^error: JSX entity escape is too big: #7777707
Boundary cases covered:
 (max valid) — still encodes as a surrogate pair
� (max + 1) — "JSX entity escape is too big"
&#-1; (negative i32) — "JSX entity escape is too big"
� (i32::MAX) — "JSX entity escape is too big"
Test added to test/bundler/transpiler/transpiler.test.js alongside
the other JSX lexer tests. Without the fix the test binary panics in u16_lead; with the fix each out-of-range entity throws and the  boundary still compiles.
Release builds correctly emit a user-facing syntax error; only the debug
assertion fires, because the assertion is gated on cfg!(debug_assertions).
Cause
src/js_parser/parse/parse_property.rs at the static { ... } branch
entered the class-static-block path purely on identifier + {, without
checking whether the enclosing context was a class body:
}elseif p.lexer.token == T::TOpenBrace && name == b"static"{// ... parse statements until `}` ...returnOk(Some(G::Property{kind:PropertyKind::ClassStaticBlock,class_static_block:Some(js_ast::StoreRef::from_bump(block)),
..Default::default()// key = None, value = None}));}
Every sibling branch in the same match (PStatic, PDeclare, PAbstract, PAccessor, PPrivate/PProtected/PPublic/POverride/PReadonly)
already gates on opts.is_class; this one was the only exception. When
invoked from the object-literal path
(parse_prefix.rs::pfx_t_open_brace), opts.is_class is false and the returned property had neither key
nor value, tripping the debug_assert in the caller.
Fix
Add opts.is_class && to the condition so static { ... } is only
parsed
as a class static block inside a class body. Outside a class body static
now falls through to ordinary identifier handling and the trailing {
produces a syntax error, matching release-mode behavior.
The Zig sibling (parse_property.zig) had the same un-gated branch;
it's
updated in lockstep to keep the porting reference aligned.
Test
test/bundler/transpiler/transpiler.test.js — added four malformed
object-literal cases ([{static{}, ({static{}}), ({static{};}), ({static{},})) alongside the existing "class static blocks" suite,
each
asserting a SyntaxError with Expected "}" but found "{".
Verification
bun bd test test/bundler/transpiler/transpiler.test.js -t "class static blocks" — 1 pass, 24 expect() calls (130 filtered).
With the fix stashed, the same test panics at the original assertion
site → confirms the test exercises the fix.
bun bd test test/cli/run/syntax.test.ts — 590 pass; valid class A{static{}} still works.
Blob::borrowed_view() constructed its name field via OwnedStringCell::new(self.name.get()), which copies the String value without bumping its refcount. The surrounding doc comment assumed name was Copy raw data with no Drop, but OwnedStringCell does implement Drop (it deref()s the inner String).
So every time a borrowed_view() was dropped — which happens in BunFile.prototype.write(), Bun.write(file, ...), and WriteFile::create via PathOrBlob::Blob(...) — the original Blob's nameStringImpl was deref'd without a matching ref.
After two writes to a BunFile whose .name had been materialized, the StringImpl refcount hit 0 while the cached JSString in JSBlob::m_name still pointed at it. The next GC then hit StringImpl::costDuringGC() → divideRoundedUp(len, refCount()) with refCount() == 0 → SIGFPE.
Fix
Clone the OwnedStringCell (which dupe_ref()s, balanced by its Drop), same as the store field already does.
How did you verify your code works?
Added regression tests to test/js/bun/io/bun-write.test.js covering file.write(), Bun.write(file, ...), and concurrent writes followed
by GC. All crash on the unfixed binary and pass with the fix.
Found by Fuzzilli (fingerprint db407a6431bfbfa7).
c47ec9 Validate Bun.password.hash memoryCost against argon2 minimum (#30964)
Fixes #30960.
Problem
Bun.password.hash({ algorithm: "argon2id", memoryCost: N }) silently
rounded up any N < 8 to 8 and emitted the clamped value in the
encoded PHC string:
consthash=awaitBun.password.hash("test",{algorithm: "argon2id",memoryCost: 3,timeCost: 1,});// Actual: m=8,t=1,p=1// Expected: an error, or m=3,t=1,p=1
This is a 1.4.0 regression from the Rust port. The old Zig impl passed m through unchanged because its argon2 had a special clamp on working
memory (@​max(m_rounded_down, 2*sync_points*p)). rust-argon2 instead
hard-rejects mem_cost < 8*lanes with MemoryTooLittle, and the port
worked around that by clamping m up to 8 * p before the call — the
clamped value is what got baked into the PHC output.
Fix
Validate at the JS argument-parsing boundary (AlgorithmValue::from_js
in PasswordObject.rs) and reject memoryCost < 8 with "Memory cost
must be at least 8". Bun hard-codes parallelism = 1, so 8 * parallelism == 8. The clamp in the pwhash shim is removed — with the
validation in place, rust-argon2 sees only valid parameters and the
user-provided value round-trips through the encoded hash.
Docs (docs/runtime/hashing.mdx, docs/guides/util/hash-a-password.mdx) used memoryCost: 4 as an
example; bumped to the documented minimum of 8.
Verification
$ bun bd /tmp/repro.ts
error: Memory cost must be at least 8
Added regression coverage in test/js/bun/util/password.test.ts:
invalid algorithm throws now also asserts that memoryCost of 1, 3,
7 each throw with "Memory cost must be at least 8".
New argon2 memoryCost at the 8 minimum is encoded faithfully test
confirms memoryCost: 8 produces m=8,t=1,p=1 in the PHC string and
verifies round-trip.
The stashed-src gate confirms the new assertions fail against unmodified
source (clamp returns silently) and pass with the fix.
JSPromise is no longer JSInternalFieldObjectImpl<2>. The getPromiseInternalField/putPromiseInternalField bytecode intrinsics
and the promiseField*/promiseState* intrinsic constants were removed
upstream. Added three C++ host functions exposed as private globals — $peekPromiseStatus(p) (0/1/2 = pending/fulfilled/rejected), $peekPromiseSettledValue(p), and $pokePromiseAsHandled(p) — and
rewrote all 26 builtin call sites across Peek.ts, CommonJS.ts, BundlerPlugin.ts, StreamInternals.ts, WritableStreamInternals.ts, ReadableStreamInternals.ts, and internal/util/inspect.js. The $isPromiseFulfilled/$isPromiseRejected/$isPromisePending codegen
macros now expand to $peekPromiseStatus(...) === N.
C++ bindings (bindings.cpp, BunPlugin.cpp) that touched JSPromise::Field/internalField() switched to flags()/setFlags()/setSlot()/payloadCell().
RapidHash (ee2220df2080)
WTF replaced WyHash/SuperFastHash with RapidHash. The static-property
lookup tables (*.lut.h) embed the string hash, so the create_hash_table perl script must produce the same hash the runtime
computes. Replaced src/codegen/create_hash_table with the upstream
RapidHash version and re-grafted Bun's ConstantInteger extension.
Without this every static property on JSGlobalObject (Bun, fetch, process, ...) is invisible.
JSType enum (24cf2e544f58)
JSWebAssemblyStreamingContextType was inserted at slot 27 — src/jsc/JSType.rs/.zig re-numbered.
WebAssembly streaming hooks
compileStreaming/instantiateStreaming global hook signatures
changed: the JSPromise* is now passed in instead of returned.
JSModuleNamespaceObject/AbstractModuleRecord
getModuleNamespace() gains a ModulePhase argument (import defer);
Bun's shouldPreventExtensions parameter is preserved as a trailing
default.
Known regression (pre-existing investigation)
Error inside minified file snapshot tests in inspect-error.test.js
show an extra at require (native:50:24) frame. The require builtin's ImplementationVisibility::Private is no longer hiding the frame after
the upstream PCH/builtin refactor — needs follow-up.
9ecb98 Fix FileSink.start() crash when called without path/fd on an open writer (#30953)
What
Fixes a debug assertion failure (fd != Fd::INVALID in src/sys/Error.rs) when calling .start() on a FileSink created by Bun.file(path).writer() with an options object that does not include a path or fd property.
Start::from_js_with_tag::<FileSink> returns Start::FileSink { input_path: Fd(Fd::INVALID), .. } when the options object has neither path nor fd. FileSink::start() then unconditionally called setup(), which called dup_with_flags(Fd::INVALID, 0). The fcntl
fails with EBADF, and when building the error via .with_fd(Fd::INVALID) we hit the debug assertion.
In Blob::get_writer the invalid-fd placeholder is always overwritten
with the real file path/fd before start() is called, but the
JS-exposed .start() has no such override.
How
Add a match guard in FileSink::start() to skip setup() when the
incoming input_path is the invalid-fd placeholder — the writer is
already configured, so we only update done/started/signal state as
before. Calls with a real path or fd still re-run setup().
80a06a Add cargo-miri support and fix HiveArray aliasing UB (#30876)
Fixes #30719
Miri support
Adds bun run rust:miri (scripts/rust-miri.ts), which runs cargo miri test over the FFI-free crate set (bun_collections, bun_paths, bun_clap, bun_base64, bun_hash, bun_wyhash, bun_ptr, bun_md, bun_ast, bun_dispatch, bun_errno, bun_http_types, bun_resolve_builtins, bun_shell_parser).
Aliasing model is -Zmiri-tree-borrows, not the default Stacked
Borrows. Stacked Borrows pops every raw pointer derived through &mut self the moment a later &mut self is formed — that's the entire
premise of the slab/pool/slot types in this codebase. Tree Borrows is
the candidate replacement spec, allows that pattern, and still catches
the bugs we care about (UAF, OOB, uninit, races).
Bit-rot fixes
cargo test has never run on these crates. Enabling it surfaced compile
errors and wrong assertions that have been there since the original
port:
bun_collections: dead test stub needing generic_const_exprs, wrong
type name, ambiguous init(), plus a cfg(miri) iteration-count guard.
bun_paths: back_then_forward expected previous() → None to
rewind the cursor; it doesn't (matches std.fs.path.ComponentIterator).
Plus 2 doc-comments that compiled as failing doctests.
bun_clap: the errors test expected unrecognized long flags to
error; Bun's StreamingClap intentionally skips them (warn + continue).
bun_base64: test_base64_url_safe_no_pad fed unpadded input to decoder_with_ignore, which for URL_SAFE_NO_PAD is the padded
decoder (the field is shared with URL_SAFE). Reworked the test helpers
to feed each decoder the form it accepts; the impl is unchanged.
bun_ast: 3 doc-comments compiling as failing doctests.
HiveArray interior mutability
Tree Borrows immediately flagged HiveArray/Fallback/HiveRef as UB.
The pool used &mut self receivers and handed out raw *mut T pointers
into its internal buffer. Under noalias, any subsequent &mut self
reborrow invalidates pointers derived from a prior one. This was a
mechanical Zig→Rust translation: Zig's *Self carries no aliasing
contract, but Rust's &mut self is noalias.
Fix is the bumpalo/typed-arena shape — &self receivers + interior
mutability:
HiveArray::buffer → UnsafeCell<[MaybeUninit<T>; CAP]>. Slot
pointers come from UnsafeCell::get() and survive &self reborrows.
Made private; ptr_at() is the typed accessor.
HiveBitSet::masks → [Cell<usize>; 32]. Same size, same alignment,
no atomics; verified identical codegen.
All &mut self receivers → &self. HiveSlot::write() writes
through a raw pointer.
HiveRef::ref_count → Cell<u32>; unref(this: *mut Self) (not a
protected &mut self it would then free through the pool back-pointer).
New HiveRefHandle<T, CAP> smart pointer — Clone/Drop track the
refcount, into_raw()/from_raw() for raw round-trips, get_mut()
instead of DerefMut (a blanket DerefMut on a shared-ownership handle
is unsound). Safe API; unsafe only at raw-pointer ingress.
Adds 11 tests covering the previously-untested API surface. All 13 hive
tests pass under MIRIFLAGS=-Zmiri-tree-borrows cargo miri test -p bun_collections hive_array with no ignores.
HiveRefHandle migration
Request.body and RequestContext.request_body migrated from raw NonNull + manual ref/unref to HiveRefHandle. Value::ref_/unref
deleted (they recovered the parent HiveRef from a *mut Value via offsetof, only needed because RequestContext held a payload
pointer). The RuntimeHooks.init_request_body_value vtable slot
deleted: its only caller and only impl were both in bun_runtime. Net unsafe delta across the touched files: −17.
Verification
cargo +nightly check --workspace # clean
cargo +nightly test -p bun_collections # 35 passed
MIRIFLAGS=-Zmiri-tree-borrows cargo +nightly miri test -p bun_collections hive_array # 13 passed, 0 ignored
bun run rust:miri # all 14 crates pass
error: Duplicate package path
at bun.lock:XXX:5
InvalidPackageKey: failed to parse lockfile: 'bun.lock'
Also reproduces without -g — any `bun add ` run twice
duplicates the entry. It's the folder-install code path, not the
global-install code path, that's broken. The same shape also regresses
`bun add ` (previously fixed in #30499 for the Zig file,
which is no longer compiled after the Rust port).
Cause
`PackageJSONEditor::edit`'s initial name-match phase looks up the
existing entry by `UpdateRequest::get_name()`. For a non-aliased
folder/path/tarball positional, `get_name()` returns the version
literal (the path/URL the user typed), not the resolved package name,
so `query.expr.as_property(name)` never finds the existing
`""` key and the code falls through to the append path.
The fallback URL-value-match loop below only fired for `Tag::Github` /
`Tag::Git`, so `.folder` and `.tarball` both fell through to the
append path. `get_resolved_name(lockfile)` then returned the real
package name (`"myproject"`) which was written as a second key
next
to the existing `"myproject": "/tmp/myproject"` entry — producing
two keys with the same name.
Fix
Extend the fallback URL/path-match loop to also match on `.tarball`
and `.folder`, skip it when the user wrote `alias@url` (that form is
an explicit request to key by `alias`, so consolidating into an
existing entry under a different name would silently drop the alias),
and guard on `request.e_string.is_none()` so a match in an earlier
dependency list (e.g. `dependencies`) isn't re-counted when the
outer loop continues to the remaining lists (`devDependencies`, etc.).
The `.zig` sibling is kept aligned as a porting reference (it is no
longer compiled — CLAUDE.md: "not compiled, not shipped").
Verification
Two regression tests added in `test/cli/install/bun-add.test.ts`:
`should not add duplicate package.json entries when installing the
same local folder twice (#30933)` — this issue.
`should not add duplicate package.json entries when installing the
same tarball URL twice (#30499)` — re-port of the test dropped during
the Rust rewrite.
Gate:
`USE_SYSTEM_BUN=1 bun test test/cli/install/bun-add.test.ts -t
'30933|30499'` — both fail on the duplicate `"":` key.
`bun bd test test/cli/install/bun-add.test.ts -t '30933|30499'` —
both pass with the fix.
Existing tests that exercise the same fallback still pass:
`should add local tarball dependency` ✅
`should add dependency without duplication` ✅
`should add aliased dependency (npm)` ✅
Supersedes #30500 (which targeted the `.zig` file only).
Fixes a Vec allocator-layout UB in to_bun_string_from_owned_slice's Ucs2/Utf16le arm — the sole code path for fs.readFile(path, { encoding: "utf16le" | "ucs2" }).
What was wrong
The arm took an owned Vec<u8> and rebuilt it as a Vec<u16> via Vec::from_raw_parts(ptr.cast::<u16>(), len/2, cap/2), then handed that
to WebKit's external-string deallocator. This is a direct port of the
Zig original's @​alignCast(bytesAsSlice(u16, …)), but Rust's Vec is
stricter:
Vec::from_raw_parts requires that T's alignment equal the
alignment of the original allocation. Vec<u8> allocates with Layout
align 1; reclaiming as Vec<u16> makes the eventual dealloc use align
Per alloc::alloc::dealloc's safety contract, the Layout used to
free must match the one used to allocate. Mismatched align is UB.
The TODO(port) comment above the code already called this out:
Reinterpreting a Vec<u8> as Vec<u16> is not generally sound in
Rust (alignment + allocator layout).
Why it never crashed
mimalloc gives us over-aligned (>= 8 byte) pointers, so the layout
mismatch is benign in practice on the allocator we ship. Miri would flag
it, and a future allocator change would surface it as either a crash on
free or silent heap corruption.
The fix
Mirror the already-merged solution in construct_from_u16's utf16le arm
(same file). That arm had the symmetric Vec<u16> -> Vec<u8>
reinterpret problem when porting from Zig, and the porter resolved it
the same way — comment from encoding.rs:786-789:
The Zig original allocated u16-aligned then reinterpreted the Vec
header to u8, which is allocator-layout-dependent in Rust; a fresh u8
Vec sidesteps that…
Same shape here, opposite direction: allocate a fresh Vec<u16> and
copy the input bytes into it via bytemuck::cast_slice_mut. Sound by
construction — the Vec is allocated and freed with the same Layout.
- // TODO(port): Zig reinterpreted the owned u8 allocation as []u16 ...- let as_u16 = unsafe {- let mut input = core::mem::ManuallyDrop::new(input);- Vec::from_raw_parts(- input.as_mut_ptr().cast::<u16>(),- usable_len / 2,- input.capacity() / 2,- )- };+ let mut as_u16 = vec![0u16; usable_len / 2];+ let dst: &mut [u8] = bytemuck::cast_slice_mut(&mut as_u16);+ dst.copy_from_slice(&input[..usable_len]);
create_external_globally_allocated_utf16(as_u16)
Tradeoff
One extra usable_len-byte memcpy on the fs.readFile(path, "utf16le"|"ucs2") hot path. For typical file sizes (KB-MB) this is
negligible. The zero-copy path can be restored later by adding a bun_core::String constructor that accepts (ptr, len, cap, dtor) —
exactly what the original TODO(refactor) suggested.
Sole caller
src/runtime/node/node_fs.rs:7071 — the fs.readFile(path, { encoding }) return-as-string path, when the encoding is utf16le or ucs2
(Latin-1/UTF-8 paths take separate arms and aren't affected).
Test plan
bun bd test test/regression/issue/utf16-encoding-crash.test.ts — fs.readFileSync(path, "utf16le") plus "ucs2", including a 256 KB + 1
byte case that hits the dynamic-allocation branch
bun bd test test/js/node/buffer-utf16.test.ts — Buffer.from(str, "utf-16le") roundtrip
$ bun bd --asan=off test test/regression/issue/utf16-encoding-crash.test.ts test/js/node/buffer-utf16.test.ts
3 pass
0 fail
9 expect() calls
No new tests added — this is a soundness fix, not a behavior fix. The
existing tests already exercise the relevant code path (fs.readFile
with utf16le/ucs2 encodings, including the >256 KB
dynamic-allocation branch); their pre/post-fix behavior is identical, as
expected.
Co-authored-by: Khang Le Duy <khangl@nvidia.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
c7a757 Collapse ThreadPool::init dead generic and fix cpuid cfg gate (#30886)
ThreadPool::init dead generic.bundler::ThreadPool::init (and its
helper init_with_pool) were generic over V2 because, during the
phased Zig→Rust port, bundle_v2.rs carried two BundleV2 definitions
— the canonical one plus a bv2_impl draft module — and both needed to
call init. The PORT NOTE on the function explicitly said it should
collapse to &BundleV2<'_> once bv2_impl was dropped. That module is
gone (grep -rn "struct BundleV2" src/bundler/ finds exactly one
definition, bundle_v2.rs:75), so this collapses the generic, names the
concrete type, and drops the now-stale PORT NOTE. The body was already
storing a type-erased raw pointer, so the monomorphised code is
identical.
cpuid cfg-gate mismatch.perf/hw_timer.rs defines struct CpuidResult and fn cpuid() under #[cfg(target_arch = "x86_64")],
but their only callers live inside a #[cfg(not(any(target_os = "macos", target_os = "freebsd")))] block (macOS/FreeBSD read the boot-time TSC
frequency from sysctl instead of probing CPUID). On x86_64-apple-darwin and x86_64-unknown-freebsd the helpers were
therefore compiled with no callers, producing never constructed / never used warnings. The cfg gates now mirror the callers' conditions.
Verified with cargo check --workspace --keep-going (clean), bun run rust:check-all (10/10 targets), and cargo fmt -p bun_bundler -p bun_perf --check. Confirmed the dead-code warnings on x86_64-apple-darwin are present without this change and gone with it.
File set is disjoint from #30879.
172afa Replace bun assert helpers with Rust builtin assert macros (#30918)
880ee8 Clean up Zig-port phase comments and trivial lint warnings (#30877)
What
Removes the ~1,750 stale "Phase A" / "Phase B" references the Zig→Rust
port left across ~600 files. The port phases are complete; the
references confuse what's a real TODO vs. a finished process step.
Comments that encode real deferred work (e.g. PERF(port): was X — profile in Phase B) keep the substance and drop the phase framing
(PERF(port): was X — profile if hot.). Comments that only describe
past process steps are removed.
Also fixes the trivial lint warnings cargo check surfaced along the way:
unused imports, an unnecessary unsafe block over a safe extern "C" fn, unreachable_pub items, ambiguous glob re-exports, an unused #[must_use] result, and a private-type-in-public-alias. Two
SAFETY/rustdoc comments that referenced API methods removed in a
follow-up are rewritten to name the current entry points.
What this is not
No behavior changes. No public API changes. The hive-pool deprecation
warnings (HiveArrayFallback::get/try_get) are not silenced here — the
call sites are migrated to the safe API in a follow-up PR.
Verification
cargo check --workspace clean for everything this PR touches
Bump typescript from 4.9.5 to 5.0.2 #2 — Dangling proxy slice across reentrant JS getter — copy process.env proxy href to an owned Vec before reentrant getters can
free the env map (Blob.rs)
[lint/michijs/autofix] Autofix changes #110 — Async randomFill uses stale resizable buffer pointer — fill a
worker-owned scratch buffer; copy back on the JS thread after
re-validating bounds (node_crypto_binding.rs)
Bump rome from 12.0.0 to 12.1.0 #10 — Invalid lockfile tag causes panic DoS — replace unreachable!()
with logged error + Tag::Uninitialized (dependency.rs)
Bump actions/checkout from 3 to 4 #20 — Unchecked lockfile string offsets cause OOB slice — bounds-check
non-inline String pointers against ctx.buffer (dependency.rs)
Bump typescript from 5.3.3 to 5.4.3 #36 — Close reason length mismatch causes panic — clamp body_len to
125 and bail on overlong UTF-8 transcode (websocket_client.rs)
[main] Linting changes #39 — Unbounded brace expansion preallocation — cap expansion count at
65536 in Bun.$ and Bun.braces (BunObject.rs, Expansion.rs)
Bump @vscode/vsce from 2.22.0 to 2.23.0 #31 — SCRAM PBKDF2 parameters accepted from server — clamp iteration
count to [4096, 10M], salt length to [1, 1024]
(PostgresSQLConnection.rs)
Bump vscode-html-languageservice from 5.5.0 to 5.5.1 #83 — Strict TLS request reuses lax-verified pooled socket — track established_with_reject_unauthorized and refuse pool reuse for strict
callers (HTTPContext.rs, lib.rs, ClientSession.rs)
f7c692 Fix worker teardown crash from missing dupeRef on synthetic-module specifiers (#30882)
Summary
~SourceProvider() derefs m_resolvedSource.specifier and .source_url (introduced in c713ab53130b to fix a leak), which requires
every ResolvedSource producer to hand those BunStrings in as +1.
The synthetic-module paths in jsc_hooks.rs (bun:main, bun:wrap, macro:, standalone-graph, embedded sqlite) stored a bitwise copy of
the borrowed specifier (*specifier) with no extra ref, so the
destructor over-derefs. The atom impl frees while a JSString in the
worker heap still references it, and once that slot is reused, Heap::lastChanceToFinalize trips RELEASE_ASSERT(wasRemoved) in AtomStringImpl::remove during worker VM teardown — symptom is a
SIGABRT on a random atom string.
Fix: specifier.dupe_ref() for both specifier and source_url on
the paths whose ResolvedSource flows into Zig::SourceProvider::create(), matching RuntimeTranspilerStore::run_from_js_thread.
Test plan
test/js/node/test/parallel/test-worker-console-listeners.js —
480/480 clean (was ~5/240 SIGABRT) with BUN_DESTRUCT_VM_ON_EXIT=1
under 8× parallel debug-build loop
test/js/node/test/parallel/test-crypto-worker-thread.js —
400/400 clean under the same harness
Diagnostic heap walk before ~VM confirms worker heaps no longer
hold a bun:mainJSString whose StringImpl is absent from the
worker's atom table (foreign=1 → foreign=0)
8438ff resolver: split the port's module wrapper into files; type the extern-Rust pointers (#30880)
What
Module split
The Zig→Rust port wrapped the entire resolver implementation in a single
7,664-line pub mod __phase_a_body { ... } inside src/resolver/lib.rs
(lines 2609–10,273) — a port artifact ("this is the
mechanically-translated block"). There's no name for a module that wraps
a crate's whole body that isn't redundant, which is the tell the wrapper
shouldn't exist. Split it into sibling files following the crate's
existing convention (data_url.rs / dir_info.rs / package_json.rs):
lib.rs shrinks 10,273 → 2,615 lines. Public API surface is
byte-identical — bun_resolver::Resolver, ::Result, ::options, …
all resolve as before.
Typed extern-Rust pointers
Un-erase the extern "Rust" link-time pointers where the declaring
crate already names the type. The port applied "type-erase across the
crate boundary" uniformly to every #[no_mangle] upward call, but extern "Rust" carries full Rust types — both crates can name the
parameters. Where visible, use the typed pointer with the Zig pointer
shape (NonNull<T> for *T, Option<NonNull<T>> for ?*T):
The implementation-side cast::<T>() calls — the tell that the erasure
was unnecessary — are removed.
Sites where the type is genuinely not visible (bun_event_loop → bun_jsc::VirtualMachine, bun_js_parser → bun_bundler::Transpiler)
are left as-is — that's the real layering boundary the pattern exists
for.
Verification
cargo check --workspace clean
cargo fmt --check clean
bun run rust:check-all (all 6 target triples)
bun bd links and runs
grep -rn resolver_body src/ returns nothing
f85020 hooks: deny direct rustfmt, point at cargo fmt --all (#30881)
What
Adds a deny rule to the PreToolUse Bash hook
(.claude/hooks/pre-bash-zig-build.js) that intercepts rustfmt
invocations and rejects them with a message pointing at cargo fmt --all instead.
Why
CI's Format job runs cargo fmt --all --check (#30682). Standalone rustfmt doesn't read the workspace edition from Cargo.toml the way cargo fmt does — it falls back to edition 2015 — and our rustfmt.toml only carries the ignore list, not an edition override.
So rustfmt <file> can produce output that disagrees with what CI
expects, leading to a red Format job after a "formatting" commit.
How
Same denyWithReason pattern the hook already uses for zig build obj, bun bd under timeout, etc. Matches on argv0 after stripping inline
env assignments and path prefixes, so rustfmt, /path/to/rustfmt, and FOO=1 rustfmt are all caught. cargo fmt / cargo fmt --all are
unaffected.
$ echo '{"tool_name":"Bash","tool_input":{"command":"rustfmt src/foo.rs"}}' | bun .claude/hooks/pre-bash-zig-build.js
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"error: Don't run `rustfmt` directly. Run `cargo fmt --all` — it's what CI checks."}}
$ echo '{"tool_name":"Bash","tool_input":{"command":"cargo fmt --all"}}' | bun .claude/hooks/pre-bash-zig-build.js
(empty — allowed)
```</li><li><a href="https://redirect.github.com/oven-sh/bun/commit/2a3d0e7d2911e59d7367aed85da692a65605c015"><code>2a3d0e</code></a> resolver: keep forward slashes when imports target is a package specifier (#30845)
## Repro
Per the reporter on Windows (also reproduced on main):
```jsonc
// packages/app/package.json
{
"imports": { "#res": "@​myproject/resolver" }
}
// packages/resolver/package.json
{
"name": "@​myproject/resolver",
"main": "./index.cjs",
"exports": { ".": "./index.mjs" }
}
import{type}from'#res';// Node: 'esm (from exports)'// Bun (Windows): 'cjs (from main)'
Linux/macOS output the expected ESM value; Windows falls back to the
legacy main field.
Cause
In src/resolver/package_json.rsresolve_target, the branch that
handles an imports target which is itself a bare package specifier
(not ./…, ../…, /…) joins [target, subpath] via resolve_path::platform::Auto and hands the joined string back to
package-resolve for a second pass.
platform::Auto is platform::Windows on Windows, so the join calls normalize_string_node_t::<u8, Windows> which rewrites / to \. The
scoped package name @​myproject/resolver becomes @​myproject\resolver;
that no longer matches anything in node_modules and the resolver falls
back to legacy main-field lookup, producing the CJS file.
Per the Node.js packages spec, values inside imports and exports are
URL-like specifiers that always use forward slashes — they are not
OS-specific filesystem paths and must not be normalized to backslashes.
The Zig reference (package_json.zig:1782) uses .auto here too; the
Rust port inherited the bug.
Fix
Use platform::Posix for this one join so the scoped-package / is
preserved. The other two platform::Auto joins in the same function
operate on [package_url, str] / [package_url, str, subpath], where
the result IS a real filesystem path — Windows normalization there is
correct and stays as-is.
Test
Added describe.if(isWindows)("#30839 - imports entry pointing at a scoped package", …) in test/js/bun/resolve/resolve.test.ts that
mirrors the reporter's setup: a workspace with @​myproject/resolver
dual-built (main=cjs, exports=mjs) and an imports entry mapping #res
to the scoped name. Asserts that running bun test.mjs outputs the ESM
value.
The test is Windows-only because on Linux/macOS platform::Auto is
already platform::Posix and this join is a no-op for forward-slash
input; the misbehavior only surfaces when the join's platform is Windows. That matches the existing test.if(isWindows) regression
pattern in the repo (e.g. test/regression/issue/23292.test.ts).
Verification
Linux: bun bd test test/js/bun/resolve/resolve.test.ts — 37 pass, 2
skip (including the new Windows-only case), 0 fail.
Reproduction script from the issue, run against build/debug/bun-debug on Linux, prints Resolved type: esm (from exports) both before and after the fix (expected — Linux isn't
affected).
4d443e collections: funnel multi_array_list SoA ops through Col/ColMut primitives (#30726)
Part of the bun_collections unsafe-reduction roadmap. Reduces multi_array_list.rs from 35 → 12 unsafe occurrences while keeping
the single-allocation SoA layout (lockfile serialization reads/writes
raw column bytes, so a per-column Vec<F> is not an option).
What changed
Every raw operation now routes through a small primitive set so each
irreducible unsafe pattern appears exactly once:
primitive
unsafe op
column_base
NonNull::add
Col::as_slice / ColMut::as_mut_slice
from_raw_parts[_mut]
Slice::scatter / Slice::gather
per-field byte copy
MultiArrayList::zero
ptr::write_bytes
free_allocated_bytes
Allocator::deallocate
__mal_split_mut_impl macro
N-way disjoint from_raw_parts_mut
plus unsafe impl Send/Sync and the pub unsafe fn signatures on set_len / column_bytes_mut.
All row-level mutations (insert_assume_capacity, swap_remove, ordered_remove, append_list_assume_capacity, set_capacity, shrink_and_free, clone, sort_internal) are rebuilt on safe <[MaybeUninit<u8>]>::copy_within / split_at_mut / copy_from_slice
over Col/ColMut views.
Storage migration
bytes: *mut u8 → NonNull<u8> and Slice::ptrs: [*mut u8; 32] → [NonNull<u8>; 32]. The empty sentinel is NonNull::<T>::dangling().cast::<u8>() (= align_of::<T>(), ≥ every
field's alignment), so the per-accessor cap == 0 dangling-substitution
branches are no longer needed. The ZST-field branch is kept
(over-aligned ZST column offsets are not guaranteed aligned). Drops both NonNull::new_unchecked calls.
bun_collections_sort_context / _unstable_context / sift_down
swap closure: Fn → FnMut (so swap_rows(&mut self, ..) can be
captured).
Caller updates
src/sourcemap/Mapping.rs: SortContext now holds *const LineColumnOffset + len and reads via unsafe { *ptr.add(i) } in less_than. The previous code held a &[LineColumnOffset] over the generated column across sort, which swaps that column's bytes — UB
under Stacked Borrows. The &mut self receiver makes the borrow checker
reject that pattern, and the raw-pointer comparator is the correct
shape.
src/bundler/LinkerGraph.rs::load: no edit; self.files.zero() is
already on a &mut self.files path.
Perf notes
insert_assume_capacity / ordered_remove change from O(n)
per-element copy_nonoverlapping loops to one copy_within (memmove)
per column.
zero() keeps its ptr::write_bytes (memset). [MaybeUninit<u8>]::fill is not memset-specialized in std, and this is
on the bundler hot path (LinkerGraph::load).
Out of scope
Pre-existing soundness gap — Slice<T>: Copy lets a caller alias &mut
via two copies. Removing Copy breaks ~140 .slice() snapshot sites
that intentionally exploit it for borrowck. Documented on the Slice
type; tracked separately.
bb1973 build: generate bun_core::build_options from Config (#30749)
bun_core::build_options (the Rust analogue of Zig's @​import("build_options") — version, sha, baseline, canary, paths) was
populated via a dozen BUN_* env vars that scripts/build/rust.ts
exported and bun_core/lib.rs read back with option_env!. The names
were maintained in two places, and a bare cargo check / rust-analyzer
saw placeholder defaults ("0000…" sha, fake version, mixed-separator
Windows BASE_PATH).
Now scripts/build/buildOptionsRs.ts writes ${codegenDir}/build_options.rs at configure time with literal pub consts straight from the resolved Config, and bun_core include!()s it (same pattern as generated_classes.rs / cpp.rs). writeIfChanged keeps the mtime stable so an unchanged sha doesn't
recompile bun_core and its dependents.
Target/profile-derived constants (ENABLE_TINYCC, ENABLE_ASAN, ENABLE_LOGS) stay as cfg!() inside the generated file so
cross-target cargo check still evaluates them per-triple.
Also:
adds src/bun_core/build.rs to export BUN_CODEGEN_DIR + rerun-if-changed (mirrors src/{jsc,runtime}/build.rs)
drops the Windows mixed-separator BASE_PATH fallback hack
Verified: bun bd --revision reports the configured sha; empty-commit +
rebuild reports the new sha; reconfigure with unchanged Config
preserves build_options.rs mtime; cargo check -p bun_core passes.
314d04 JSON lexer: tokenize ?/*/(/) so define auto-quote can recover (#30679)
Summary
The JSON lexer raised Operators are not allowed in JSON eagerly on ?, *, (, ). That aborts Lexer::init before parse_env_json's
auto-quote fallback (which treats a non-JSON define: value as a string
literal) ever runs.
This broke building Bun itself: bun run build:debug:noasan failed in bake-codegen.ts because OVERLAY_CSS is a raw minified CSS string
starting with *{...}:
Errors while bundling Bake error runtime ...
1 |
*{box-sizing:border-box;margin:0;padding:0}.root{all:initial;--modal-bg:#202020;-
^
error: Unsupported syntax: Operators are not allowed in JSON
- Drop `?`, `*`, `(`, `)` from the operators-error list so they fall
through to the catch-all `TSyntaxError` path that
`JSONLikeParser::parse_expr`'s auto-quote arm handles. The other
operator characters (`%`, `&`, `|`, `^`, `+`, `=`, `<`, `>`, `!`, `` `
``) keep erroring as before.
- Adds a regression test that `Bun.build({define: {X:
"*{box-sizing:border-box}.root{}"}})` (and `?`/`(`/`)`/non-JSON-brace
values) auto-quote into string literals.
## Test plan
- [x] `bun bd test test/bundler/bun-build-api.test.ts -t "auto-quoted"`
— 1 pass
- [x] `bun bd test test/bundler/bun-build-api.test.ts` — 41 pass / 0
fail
- [x] `bun bd src/codegen/bake-codegen.ts --codegen-root=/tmp/x
--debug=false` — emits `bake.client.js`, `bake.server.js`,
`bake.error.js` with no errors
- [x] `cargo check -p bun_parsers` clean</li><li><a href="https://redirect.github.com/oven-sh/bun/commit/fe2635b460dda3892664e3d535991b1e52fc4b50"><code>fe2635</code></a> ci: replace zig fmt with cargo fmt --all in the format workflow (#30682)
## Summary
- Drop the Zig download + `zig fmt src` block from
`.github/workflows/format.yml` and run `cargo fmt --all` instead. The
codebase no longer has Zig source to format.
- The pinned nightly + `rustfmt` come from `rust-toolchain.toml` (now
lists `rustfmt` in `components`); `rustup` auto-installs on the first
`cargo` invocation, so no separate setup step.
- Run `cargo fmt --all` once against the current toolchain so the next
format run is a no-op — 92 source files reformatted (~640 lines, mostly
import-list reordering and over-long-line wraps).
- Update `.github/workflows/CLAUDE.md` to describe the rustfmt step and
toolchain-bump process.
## Test plan
- [x] `cargo fmt --all -- --check` exits 0 after the formatting pass
(idempotent)
- [x] `cargo check -p bun_core -p bun_runtime -p bun_bin` clean after
the reformat
- [x] `bun run rust:check` clean
- [x] `python3 -c "import yaml;
yaml.safe_load(open('.github/workflows/format.yml'))"` — YAML valid
- [ ] Confirm the `Format` GHA job on this PR runs `cargo fmt --all` and
emits no changes
---------
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@​users.noreply.redirect.github.com></li><li><a href="https://redirect.github.com/oven-sh/bun/commit/bbd3e624af078e3eaff79c9f4046b0b6cb4f0222"><code>bbd3e6</code></a> bun:ffi: extract embedded shared libraries from bunfs in `dlopen()` (#30720)
## Reproduction
```js
// repro.js
import libpath from "./libhello.so" with { type: "file" };
import { dlopen, FFIType } from "bun:ffi";
const lib = dlopen(libpath, { hello: { args: [], returns: FFIType.i32 } });
console.log("loaded:", lib.symbols.hello());
clang -shared -fPIC -o libhello.so hello.c
bun repro.js # works → "loaded: 42"
bun build --compile repro.js --outfile repro
./repro # broken on canary
Fails with:
error: Failed to open library "/$bunfs/root/libhello-XXXX.so":
/$bunfs/root/libhello-XXXX.so: cannot open shared object file: No such file or directory
syscall: "dlopen", code: "ERR_DLOPEN_FAILED"
Cause
Regression from the Rust rewrite (1.3.14-canary, commit 23427dbc12;
last known-good 1.3.13). The Rust port of FFI.open
(src/runtime/ffi/ffi_body.rs:1486) shipped a stub where the Zig
original called jsc.ModuleLoader.resolveEmbeddedFile to materialize
the bunfs-embedded library to an on-disk tmpfile. The raw /$bunfs/...
virtual path fell through to libc dlopen(2), which can't see the
bunfs virtual filesystem.
process.dlopen (.node addons) was unaffected — that path still
reaches the working Bun__resolveEmbeddedNodeFile → resolve_embedded_node_file_hook. The PORT NOTE at ModuleLoader.rs:561 enumerated only two Zig callers being ported; it
omitted ffi.zig:1030.
Fix
Factor the extraction body out of resolve_embedded_node_file_hook
into resolve_embedded_file_to_buf(input_path, extname, out_buf).
Keep the .node hook thin (pass b"node" and clone the result
into in_out_str). Call the helper from ffi_body with the
platform-chosen extname (so/dylib/dll). Same-crate call, no
new LoaderHooks entry needed.
Verification
test/regression/issue/30717.test.ts: compiles a C fixture, embeds
it with { type: "file" }, runs bun build --compile, deletes
the on-disk .so, runs the compiled binary from a different cwd,
asserts it prints loaded: 42 without ERR_DLOPEN_FAILED.
Fails on pre-fix tree (ERR_DLOPEN_FAILED /$bunfs/root/…), passes
with the fix.
The Rust port of #30583 covered the user-facing JS option names
("http3"/"http1") and validation error messages, but left the
internal ServerConfig struct fields as h3/h1 and one stderr
warning string as "h3: true with a unix socket — HTTP/3 listener skipped".
This finishes the rename:
ServerConfig::{h3,h1} → {http3,http1} (fields, defaults, clone,
all reads in ServerConfig.rs/mod.rs)
doc comments in mod.rs and uws_sys/quic/Context.rs
Transport-layer names (uws_sys::h3::, h3_app, on_h3_listen, AnyRequest::H1, HTTPClient.h3) are left as-is, matching #30583.
Adds serve-http3.test.ts → validation: http3 with unix socket warns and skips H3 listener to cover the warning text.
175f62 ffi: cfg-gate local tcc_delete extern to match bun_tcc_sys (#30743)
src/runtime/ffi/mod.rs declares its own unconditional extern "C" { fn tcc_delete }, separate from bun_tcc_sys::tcc_externs! which correctly
cfg-gates the libtcc externs on cfg.tinycc (= NOT android / freebsd /
windows-aarch64, where libtcc.a isn't built).
Function::drop calls it inside if let Some(state) — state is never Some on those targets so the call is dead at runtime, but with cargo
LTO overridden off (asan / linker-plugin-lto path in rust.ts) the
symbol reference survives into the staticlib and link fails:
lld-link: error: undefined symbol: tcc_delete
Gate the local extern on the same predicate as tcc_externs! and
provide an unreachable!() stub on the disabled targets, mirroring what src/tcc_sys/tcc.rs already does.
47330a Merge pull request #1233 from JoeRobich/dev/jorobich/set-exitcode
Return non-zero exit code when signature verification fails
7a8969 Return non-zero exit code when signature verification fails
d6d2ef Merge pull request #1232 from microsoft/benibenj/relevant-lamprey
Audit npm package
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Updated Packages