Make bun_core::String own its WTF ref - #40238
Conversation
`bun_core::String` was a `Copy` POD with no destructor; every +1 producer (`clone_utf8`, `create_format`, `to_bun_string`, URL accessors, C++ `toStringRef` out-params, ...) relied on the caller remembering to `.deref()` or wrap it in `OwnedString`. An audit found ~60 sites that did not, several per-request/per-import. Ownership is now in the type: - `String` is no longer `Copy`. `Drop` derefs, `Clone` refs. `ref_`/`deref` are private; `dupe_ref` and `OwnedString` are gone. - `RawString` (the `Copy` C layout) is the only thing that crosses FFI by value; `String::into_raw`/`from_raw` are the explicit hand-off points. C++ functions that only read a string now take `const BunString*`. - `StringView<'a>` is the by-value borrow (property-iterator names, `substring`, `StringCell::get`). - `StringJsc::into_js(self)` replaces `transfer_to_js(&mut self)`; `to_error_instance` borrows. - `Errorable<T>` owns its payload; `ResolvedSource` is an owning struct (no `specifier` field, typed `bytecode_cache`/`module_info`), mirrored by a C++ `ErrorableResolvedSource` destructor. `Zig::SourceProvider` takes the strings it keeps by transfer, so the builtin/JSON/object loader arms no longer leak `source_url`/`source_code`. - `Bun::toString(JSGlobalObject*, JSValue)` (a +1 producer named like a borrow) is removed in favor of `toStringRef`.
|
Verified this closes the jsonb bind leak from #40102 with the regression test from #32293 ( Debug+ASAN build, same machine: This PR has no test files. If you want a guard for #40102 in the same change, that file cherry-picks cleanly onto this branch. Happy to leave it to you either way. |
…ty .into() - `String::into_raw`/`from_raw_ref` use `ManuallyDrop`/`ptr::from_ref().cast()`. - `JSFunction::create` takes `impl Into<StringView>` so callers pass `&str` / `&String` without a ref; bun:test scope names are `&'static str`. - `AsyncModule::fulfill`, `write_bind`, shell `handle_js_string_ref` borrow. - Remove the now-identity `String -> String` `.into()` conversions left over from `OwnedString`.
|
Updated 8:56 PM PT - Aug 23rd, 2026
❌ @dylan-conway, your commit 7007f7a has 1 failures in
🧪 To try this PR locally: bunx bun-pr 40238That installs a local version of the PR into your bun-40238 --bun |
…dto errors - test/js/sql/sql-postgres-json-bind-leak.test.ts: in-process mock Postgres answers ParameterDescription([json]); binding a 512 KiB payload 300 times must not grow RSS. Fails on 1.4.0 (166 MiB), passes here. - UDPSocket address getters return JsResult instead of swallowing an into_dto failure as undefined.
…g-leaks-3ff1dd # Conflicts: # src/runtime/bake/BakeGlobalObject.cpp
…path - TOML stringify header path holds borrowed `RawString`s (bracketed by the property iterator) instead of taking a ref per key. - `into_js` where the string is done: ResolveMessage code atom, Blob text paths, quic qlog data, Bun.resolveSync result. - installBinding test helper: borrow instead of clone_utf8 before JSONParse. - ZigSourceProvider: only build the coverage BunString when coverage is on; move sourceURL into the provider. Drop the dead memset in ~ErrorableResolvedSource.
… out-params
- `StringCell` → `JsCell<String>`; `StringView::to_owned` → `.clone()`.
- Loader hooks return `Option<ResolvedSource>` / `Result<ResolvedSource, _>`
instead of writing `ErrorableResolvedSource` out-params; `Errorable` is
now only built at the C++ edge. Same for `resolve_maybe_needs_trailing_slash`
(returns `Result<String, JSValue>`) and `AsyncModule::fulfill` (takes a
`Result`).
- `RuntimeTranspilerCache::Entry::output_code` is a plain `String` (the
`Utf8` arm was only ever empty).
- `to_slice`/`to_thread_safe_slice` consume self (`into_slice`); fold
`to_utf8_bytes` into `to_owned_slice`, `static_str` into `static_`.
- `Bytecode::{owned,borrowed}` normalize empty; `ZigException::Holder` uses
`Option` instead of `MaybeUninit` + flag; shell lexer borrows its string
table immutably; `SocketAddress::into_dto` takes self.
- Remove dead `ErrorableResolvedSource::reset`, `JSCommonJSModule::create`
(by-value overload), `Bun::toString(const char*, size_t)`, `Route`'s Drop,
`From<WTFStringImpl> for String`, `Errorable::value{,_mut}`.
…view nits - JSC__JSValue__putIndex downcast to JSArray; the http2 custom-settings object is a plain object. Use getObject()->putDirectIndex. - Drop a vestigial reborrow in ConsoleObject table printer, a stale comment in get_hardcoded_module, and document why JSPropertyIterator names carry the global lifetime.
Declare +1 returns from C++ (`Bun::toStringRef`) as `-> String` and Rust exports that C++ `transferToWTFString()`s as returning `String`, the same way `Box<T>` is used in FFI. Borrowed (+0) by-value results are `StringView<'_>`. This removes every `String::from_raw(EXTERN(..))` / `.into_raw()` call site and the `adopt()` helpers; `RawString` remains only for the property-iterator out-param slot C++ fills in place. Also give the TOML stringifier a lifetime so its key path holds `StringView`s instead of raw strings.
|
Awesome |
- `process_fetch_log` returns the error `JSValue`; `Errorable::err` takes just the value; `loader_hooks()` is infallible; the two transpile shims share one error tail. `Errorable::unwrap` and the pre-seeded dummies go. - Single-out-param exports now return by value: `Bun__VM__entryRootKey`, `Bun__Process__getTitle`, `Bun__Node__Path_joinWTF`, `Bun__getEnvValueBunString`, `Bun__Node__getRedirectWarnings`, `Bun__resolveEmbeddedNodeFile`. `WebWorker__dispatchError` and `Bun__Dirent__toJS` take their strings by value (they consume them). - Drop the dead `ResolvedSource.allocator` field (144 → 136 bytes) and derive `Default`; C++ `ErrorableResolvedSource` value-initializes. - `JSFunction::create` takes `&'static str`; `JSPropertyIterator` fills a `StringView` directly, so `RawString`, `String::from_raw` and `StringView::from_raw` are no longer exported. - `String::create_if_different`, `AlreadyBundled::into_bytecode`; remove `String::to_wtf`, redundant `StringView` `From` impls; const-correct the borrowed FFI params; `putIndex` uses `asObject`. - Rewrite the jsonb leak test on top of wire-frames.ts with a fixture and an ASAN/debug-branched RSS bound.
The backing arrays are fixed-size (32 frames, 6 lines) but C++ only fills `..frames_len` / `..source_lines_len`; release just those, as before, instead of running drop glue over every slot.
No-Verification-Needed: comment-only change
…RawString alias No-Verification-Needed: rename-only, no runtime surface
No-Verification-Needed: comment-only change
There was a problem hiding this comment.
Automated review ran clean on e34fd40 — no findings this pass, and the earlier nits (stale comments, the ConsoleObject reborrow, the JSPropertyIterator lifetime note) are all addressed. Given the scope — a String ownership-model change across 225 files plus FFI-shared struct layout changes on both the Rust and C++ sides — this still warrants a human pass.
Checked: ResolvedSource/ErrorableResolvedSource layout agrees on both sides (136-byte static_assert in headers-handwritten.h ↔ assert_ffi_layout! in ResolvedSource.rs); Zig::SourceProvider::create now takes every retained field by transferToWTFString()/std::exchange so the C++ destructor and Rust Drop don't both release. Errorable<T>::Drop only drops the value arm when success, matching ~ErrorableResolvedSource. The to_error_instance family no longer consumes the caller's ref (C++ side unchanged — it never deref'd), so removing the post-call .deref() is correct. JsCell<String>::get() returning a borrow (was OwnedStringCell) — verified callers either .clone() or use it within the cell's lifetime.
Extended reasoning...
Overview
This PR converts bun_core::String from a Copy POD with manual .deref() to an owning type where Drop derefs and Clone refs. It removes OwnedString/OwnedStringCell/OwnedResolvedSource, introduces StringView<'a> for by-value borrows, and updates ~220 call sites. On the C++ side it removes the Bun::toString(JSGlobalObject*, JSValue) +1 producer, gives ErrorableResolvedSource a destructor, shrinks ResolvedSource (drops specifier/allocator/needsDeref), and makes Zig::SourceProvider take retained fields by transfer instead of storing a ResolvedSource copy. FFI signatures are updated so by-value String = ownership crosses, &String/const BunString* = borrow.
Security risks
None specific to this change. The PR is a memory-ownership refactor; the risk class is use-after-free / double-deref / leak at the FFI boundary, not injection or auth. The regression test added (postgres-json-bind-leak) guards one of the ~60 leaks the PR closes.
Level of scrutiny
High. This is a foundational memory-safety change: every bun_core::String producer/consumer's refcount contract changed, struct layouts shared with C++ changed (with static_asserts on both sides), and the module-loader's ResolvedSource ownership protocol was rewritten. A missed site means either a UAF (extra deref) or a leak (missing one). The author ran ASAN + targeted test files locally; full CI is pending on build #104335.
Other factors
Three prior automated review passes surfaced only comment/cosmetic nits, all fixed. The bug hunting system found nothing on this pass. The change is internally consistent and well-reasoned (the PR description accurately maps every struct-size change and asserts layouts on both sides). But 225 files of refcount-contract changes across an FFI boundary is exactly the category REVIEW.md's "Native code: memory safety" section flags as most-blocked — a human familiar with the ResolvedSource/SourceProvider lifecycle should confirm the C++-side transfer points (ZigSourceProvider::create, ~ErrorableResolvedSource, fetchCommonJSModule's builtin path) match the new Rust ownership.
…ime-tied `JSValue::to_zig_string` handed back a `ZigString` aliasing a JSString's characters with no lifetime and no ref. Delete it (and C++ `JSC__JSValue__toZigString`); callers now either take an owned `to_bun_string()` / `to_slice_clone()`, or hold the `&JSString` from `to_js_string()` in a local and borrow via `JSString::view()`, which now returns `StringView<'a>` tied to that handle. Fixes three places that used the borrowed view across user JS: `Bun.dlopen` (library name read again after walking the symbols object), and the JSX tag name in `console.log`/`expect` formatting (reused after printing props/children).
There was a problem hiding this comment.
I've reviewed this PR across several rounds and the bug-hunting pass on the latest commits found no issues (earlier rounds surfaced only stale-comment nits, all addressed). Given the scope — 243 files rewriting bun_core::String's ownership model across the Rust/C++ FFI boundary, plus ResolvedSource/ErrorableResolvedSource gaining destructors on both sides and the removal of JSValue::get_zig_string — a human sign-off on the overall ownership design is still warranted before merge.
What was reviewed
StringDrop/Clonevs. former manual.deref()sites — checked for double-frees where old explicit derefs remained; none found (allscopeguard/.deref()calls in the diff were removed, not left alongsideDrop).- FFI signature agreement: spot-checked that by-value
Stringreturns on the Rust side pair withtransferToWTFString()/Bun::toStringRefon the C++ side, and&String↔const BunString*. ErrorableResolvedSourceC++ destructor vs. RustDrop for Errorable<T>— both gate onsuccessand release the same fields;Zig::SourceProvider::createnowstd::exchanges out what it keeps.ZigException::HoldernewDroponly releases the populated..frames_len/..source_lines_lenprefixes — matches how C++ populates them.
Extended reasoning...
Overview
This PR changes bun_core::String from a Copy POD (manual .deref()) to an owning type where Drop = deref and Clone = ref, then propagates that through 243 files: every +1 producer now returns an owned String, borrows become &String/StringView<'_>, OwnedString/OwnedStringCell/OwnedResolvedSource are deleted, transfer_to_js becomes consuming into_js, and ResolvedSource/Errorable<T> gain real destructors on both the Rust and C++ sides. It also removes JSValue::get_zig_string/to_zig_string (unsound borrow with no lifetime) and reworks ZigException::Holder, Zig::SourceProvider, and the module-loader hook signatures.
Security risks
None net-new. The change is memory-ownership plumbing; it does not add parsing of untrusted input, auth, or crypto. The risk class is memory safety (double-free / UAF / leak) rather than a security boundary — and the direction of the change is toward fewer leak/UAF hazards by moving the +1/-1 rule into the type system.
Level of scrutiny
High. This is a foundational refactor of the most-used string type across the FFI boundary, with coordinated layout and destructor changes in both Rust and C++ (headers-handwritten.h, ZigSourceProvider, ~ErrorableResolvedSource). Per the approval guidelines this is squarely "complex, large, touches critical code paths" — not a candidate for bot-only approval regardless of how clean the individual hunks look.
Other factors
Four prior bug-hunting rounds surfaced only stale-comment nits (all fixed in b7691c1, e22735d, e34fd40). This run found nothing. A regression test for #40102 was pulled in. Layout is guarded by assert_ffi_layout!/static_assert. One maintainer left a positive comment but no formal approval is visible. The two newest commits (045baf2, 83737ac) are further cleanups in the same vein and were covered by this run.
…gErrorType
- `JSValue::to_js_string_view()` returns `(&JSString, StringView)` in one
call (C++ `JSC__JSValue__toStringAndView`); `JSString::view()` is a
direct FFI return. Hot `get_zig_string` replacements (node:path, fs
flags/mode, CryptoHasher, TextEncoderStream, console formatter) are back
to a single FFI call. `StringView::to_utf8` never refs.
- `get_class_name` / `get_name_property` / `get_description` return an
owned `String` (they aliased strings user JS could drop); `symbol_for`
takes bytes; `js_type_string` returns a view; `as_string` returns
`&JSString`. `to_slice_or_null`/`to_slice_clone` folded into `to_slice`.
- `ZigErrorType` was `{code, value}` with `code` never read: the
`Errorable` error arm is now just the `JSValue` on both sides.
- `ZigException::Holder` back to plain array fields: hidden frames swapped
past `frames_len` still own strings, so dropping only the prefix leaked.
- `AsyncModule::on_done` goes through `fulfill`; `StringView::static_` /
`borrow_utf8`; const/`&` FFI params; assorted stale comments and
duplicate helpers removed; jsonb leak fixture renamed to `.fixture.ts`.
…one URL wrapper - `bun_jsc` calls `__bun_transpile_source_code` / `__bun_fetch_builtin_module` as `extern "Rust"` fns defined in `bun_runtime` (same convention as `dispatch.rs`); the four C-ABI entry points that only forwarded (`Bun__transpileFile`, `Bun__transpileVirtualModule`, `Bun__resolveAndFetchBuiltinModule`, `Bun__resolveEmbeddedNodeFile`) live next to their bodies and take references. - `JSPropertyIterator::next(&self)` yields `(StringView<'_>, JSValue)`; the name borrows the iterator instead of the global. TOML stringify threads the header path as a stack-linked list; parseArgs/XML keep borrowing. - `bun_jsc::URL` re-exports `bun_url::whatwg::URL` (+ `URLJsc` for the two JS-value entry points) instead of a second wrapper with its own externs; `hosted_git_info` uses `bun_url::whatwg::Parsed`; `resolver_jsc` uses `bun_string_jsc::to_js_array`.
…n, typed TranspileArgs.extra; commit Cargo.lock - Cargo.lock picks up bun_url's bun_opaque dep and sourcemap_jsc's bun_url dep (CI builds with --locked). - `symbol_for` takes a `'static` key (`String::static_`, no UTF-8 copy); `get_description` returns a view borrowed from the Symbol; `getClassName` fallback returns the static `className()` without allocating. - `JSC__JSValue__toStringAndView` checks for an exception after `value()`. - `TranspileArgs.extra` is a typed `*mut TranspileExtra`; the duplicate param on `transpile_source_code_inner` is gone. - JSBundler/Glob/Blob/dns/WebSocketServerContext borrow the argument string via `to_js_string_view` instead of `to_slice`. - `JSValue::type_name`, `StringView::static_` delegates to `String::static_`, naming/doc nits.
…ot moved onto the stack
Adapt telemetry code to bun_core::String owning its ref (#40238): drop OwnedString wrappers, destructure JSPropertyIterator::next(), ManuallyDrop the String member of the AttrValue union.
…ng for any syscall (#38383) ### Problem - `fs.stat(p, cb)`, `fs.readFile(p, cb)`, `fs.access`, `open`, `readdir`, `realpath`, `unlink`, `mkdir`, `readlink`, `rmdir`, `rename`, `link`, `symlink`, `copyFile`, `writeFile`, `appendFile`, `truncate`, `statfs`, `mkdtemp`, `opendir`, `chmod`, `chown`, `utimes` (every callback API taking a path) throw `ENAMETOOLONG: name too long, open` synchronously when the path is `MAX_PATH_BYTES` (4096 Linux, 1024 macOS, 98302 Windows) or longer, and the callback never runs. Node 26 calls back with `ENAMETOOLONG` for all of them (outputs below). - Cause: `PathLike::from_js` (src/runtime/node/types.rs, `Valid::path_string_length` / `Valid::path_buffer`) throws the error while the binding is still converting arguments. `fs.promises.*` only behaves because its `asyncWrap` is an `async function`; the callback layer in src/js/node/fs.ts calls the native binding directly, so the throw escapes to the caller. `Bun.file(p)` throws at construction for the same reason; that is a different entry point and is not changed here (see below). - Fixes #25659. #36324 is an earlier attempt at the same bug that removes the parse-time length guard and re-adds the check at every dispatch site; this PR keeps the guard and is the smaller alternative (trade-off: #36324 also fills in a per-operation `err.syscall`, this PR leaves it `undefined` as today). ### Fix - `ArgumentsSlice` (src/jsc/CallFrame.rs) gets `deferred_error: Option<Box<bun_sys::SystemError>>`. - `Valid::path_length` (types.rs) runs once for every path form (string, `file:` URL, Buffer, ArrayBuffer) after conversion. Sync parsing (`will_be_async == false`) throws exactly as before. When the binding is parsing for an async operation it records the error on the slice instead and returns an empty placeholder path, so the remaining arguments are still validated (an invalid `encoding` still throws synchronously, as in node). The first recorded error wins for two-path operations. - `parse_async_args` (src/runtime/node/node_fs_binding.rs) is the argument-parsing step the three promise-returning bindings (`run_async`, `cp`, `readdir`) now share; after a successful parse it returns a promise rejected with the recorded error, so the operation is never started on the placeholder. The already-aborted `AbortSignal` check stays ahead of it (node rejects with `AbortError` for `readFile(tooLong, { signal })` too). `args::Cp` joins the `FsArgument` list so `cp` can use the helper. This is the line that changes behaviour; the rest of the diff in that file is the three copies of the parse block collapsing into the helper. - The already-aborted branch builds its rejected promise with `JSPromise::rejected_promise` like the deferred-error branch, instead of the deprecated `dangerously_create_rejected_promise_value_without_notifying_vm`. Nothing observable changes through `fs.*`/`fs.promises.*`, which always attach handlers to the binding's promise; it removes the deprecated call from the shared helper. - The error now also carries `err.path` (sync and async). Since `to_system_error` formats the message into a 4096-byte buffer, the message of a Linux-length path is cut off after 4096 bytes (#38201 is changing that formatter); `err.path` itself is complete. `err.syscall` stays `undefined`, as before. - Order change for a path that is both too long and contains NUL bytes: the NUL-byte `ERR_INVALID_ARG_VALUE` now wins, which is node's order (it validates NUL bytes before issuing the syscall). - Not changed: `Bun.file(p)` / `Bun.write(p, ...)` still throw synchronously at argument conversion. Making those lazy means letting an over-long path into a Blob store and auditing every Blob/sendfile/copy path that copies the path into a fixed buffer, which is separate work. - Verified: test/js/node/fs/fs-path-length.test.ts gains a describe covering 31 callback operations (string, Buffer and URL paths, both operands of `rename`, `mkdir`/`readdir` recursive, `opendir`, `realpath.native`), the callback not running synchronously, `fs.exists` answering `false`, an invalid option still throwing synchronously, the sync forms still throwing (now with `path`), and the #25659 repro. 34 of the 35 new tests fail on the unfixed build: 31 because the error is thrown synchronously, and `rm`, `cp` (already routed through a JS promise) and the sync-forms test because `err.path` was missing. All pass with the fix. - Also run against the debug build: fs.test.ts, cp.test.ts, promises.test.js, dir.test.ts, fs-mkdir.test.ts, readdirSync-recursive-error-leak.test.ts and 35 ported node `test-fs-*` files touching path validation, abort signals and error shapes: no failures (abort-signal-leak-read-write-file.test.ts times out in this container on an unmodified build as well, 100k iterations at ~7ms each under debug ASAN). The matrix below also runs clean under `BUN_JSC_validateExceptionChecks=1`. `cargo clippy` on `bun_jsc` and `bun_runtime` is clean. ### Background - `ArgumentsSlice` is the cursor the native bindings walk over a call's arguments. node:fs async bindings set its `will_be_async` flag before parsing so string arguments get copied into thread-safe forms; this PR uses the same flag to mean "this binding reports errors through a promise". - `PathLike` is the parsed path argument. Every fs operation copies it into a `PathBuffer` (`[u8; MAX_PATH_BYTES]`, plus NUL) right before the syscall, which is why the parser rejects paths of `MAX_PATH_BYTES` or more up front: the copy is infallible and the rest of node_fs.rs relies on the length invariant. The kernel's own limit is the same number (PATH_MAX), so the early `ENAMETOOLONG` is the error the syscall would have produced; only its delivery was wrong. - `bun_sys::SystemError` is the JS-facing error record (`code`, `errno`, `message`, `path`, ...); `to_error_instance` turns it into the JS `Error` that node-style fs errors are made of. The deferred slot holds this record and the binding converts it when it builds the rejected promise. - The callback APIs in fs.ts are all of the form `binding.stat(path, options).then(ok, callback)`: whatever the binding returns as a rejected promise reaches the callback, whatever it throws reaches the caller. Rebases: onto #40251 (main dropped the `has_exception()` guards after `?`-checked calls; the helper follows) and #40238 (`bun_core::String` owns its WTF ref, so `from_bun_string` / `path_like_from_string` take the string by value and use `into_slice` / `into_thread_safe_slice`). Both resolutions are mechanical; the behaviour is the one described above. <details> <summary>Matrix: node 26.3.0 vs this branch (sync = did the call throw; cb = code passed to the callback)</summary> Node 26.3.0: ``` access returned cb:ENAMETOOLONG syncCb:false path===input:true syscall:access appendFile returned cb:ENAMETOOLONG syncCb:false path===input:true syscall:open chmod returned cb:ENAMETOOLONG syncCb:false path===input:true syscall:chmod chown returned cb:ENAMETOOLONG syncCb:false path===input:true syscall:chown copyFile (src) returned cb:ENAMETOOLONG syncCb:false path===input:true syscall:copyfile lstat returned cb:ENAMETOOLONG syncCb:false path===input:true syscall:lstat mkdir returned cb:ENAMETOOLONG syncCb:false path===input:true syscall:mkdir mkdir recursive returned cb:ENAMETOOLONG syncCb:false path===input:true syscall:mkdir mkdtemp returned cb:ENAMETOOLONG syncCb:false path===input:false syscall:mkdtemp open returned cb:ENAMETOOLONG syncCb:false path===input:true syscall:open opendir returned cb:ENAMETOOLONG syncCb:false path===input:true syscall:opendir readdir returned cb:ENAMETOOLONG syncCb:false path===input:true syscall:scandir readdir recursive returned cb:ENAMETOOLONG syncCb:false path===input:true syscall:scandir readFile returned cb:ENAMETOOLONG syncCb:false path===input:true syscall:open readlink returned cb:ENAMETOOLONG syncCb:false path===input:true syscall:readlink realpath returned cb:ENAMETOOLONG syncCb:false path===input:true syscall:lstat realpath.native returned cb:ENAMETOOLONG syncCb:false path===input:true syscall:realpath rename (oldPath) returned cb:ENAMETOOLONG syncCb:false path===input:true syscall:rename rename (newPath) returned cb:ENAMETOOLONG syncCb:false path===input:false syscall:rename rm returned cb:ENAMETOOLONG syncCb:false path===input:true syscall:lstat rmdir returned cb:ENAMETOOLONG syncCb:false path===input:true syscall:rmdir stat returned cb:ENAMETOOLONG syncCb:false path===input:true syscall:stat statfs returned cb:ENAMETOOLONG syncCb:false path===input:true syscall:statfs symlink (path) returned cb:ENAMETOOLONG syncCb:false path===input:false syscall:symlink truncate returned cb:ENAMETOOLONG syncCb:false path===input:true syscall:open unlink returned cb:ENAMETOOLONG syncCb:false path===input:true syscall:unlink utimes returned cb:ENAMETOOLONG syncCb:false path===input:true syscall:utime writeFile returned cb:ENAMETOOLONG syncCb:false path===input:true syscall:open stat (Buffer) returned cb:ENAMETOOLONG syncCb:false path===input:true syscall:stat stat (URL) returned cb:ENAMETOOLONG syncCb:false path===input:true syscall:stat cp returned cb:ENAMETOOLONG syncCb:false path===input:true syscall:lstat exists -> false readdir bogus encoding: THREW ERR_INVALID_ARG_VALUE statSync: ENAMETOOLONG path===input:true syscall:stat ``` Bun 1.4.0 (unfixed): every line above except `rm`, `cp` and `exists` reads `THREW SYNCHRONOUSLY ENAMETOOLONG, callback NOT called`; `statSync` has no `path`. This branch: every line reads `returned cb:ENAMETOOLONG syncCb:false path===input:true syscall:undefined` (`path` is the over-long operand also for `rename (newPath)`, `symlink` and `mkdtemp`, where node reports it as `dest` / with the template suffix), `exists -> false`, `readdir bogus encoding: THREW ERR_INVALID_ARG_VALUE`, `statSync: ENAMETOOLONG path===input:true`. Known remaining difference: for `copyFile(missing, tooLong)` and `link(missing, tooLong)` node's kernel call fails on the first operand and reports `ENOENT`; this branch reports `ENAMETOOLONG` for the second. Both arrive through the callback. The tests use `rename`/`symlink` for the second-operand cases, where node also reports `ENAMETOOLONG`. `readFile`/`writeFile` with an already-aborted signal and an over-long path reject with `AbortError` on both node and this branch; with a live signal both give `ENAMETOOLONG`; an unhandled `fs.promises.stat(tooLong)` reaches `unhandledRejection` on both. </details> <!-- robobun:evidence:begin --> --- **no test proof** · iteration 2 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/js/node/fs/fs-path-length.test.ts <!-- robobun:evidence:end -->
…rship contracts compile errors Rebased onto main as a single commit; the branch history (with its merge commits) is not preserved. Adds a zero-cost branded scope layer over the raw JSC FFI (`src/jsc/scope.rs`: `Scope<'s>`, `Local<'s>`) that makes two boundary bug classes compile errors for code that stays on the scoped API: - a JS value escaping its host call unrooted (persisting requires the explicit `Scope::persist` -> `Strong`); - a JS-heap view (`Local::array_buffer_bytes`) held across an operation that can re-enter user JS (`&mut Scope`), e.g. a coercion that detaches the buffer. Codegen integration: - `#[bun_jsc::host_fn(scoped)]`: functions written as `fn(scope: &mut Scope, callframe) -> JsResult<Local>` get a macro-synthesized wrapper under their original name and unscoped signature, so js2native / `.classes.ts` / direct-call wiring stays byte-compatible. User `cfg`/doc/lint attributes propagate to the public wrapper and the extern shims. - `ZIG_EXPORT(tag, reenters_js | no_user_js)` effect markers on the C++ declarations; explicitly classified functions get branded wrappers generated into `bun_jsc::cpp::scoped` (`&mut Scope` / `&Scope`), unclassified and `null_is_throw` functions get none. All classified exports are verified against their C++ (`toMatch` and `putMayBeIndex` are `reenters_js`: a non-uint32 `lastIndex` goes through ToNumber, and index puts on exotic receivers reach `defineOwnProperty` traps). Migration: ~470 host functions are converted to the scoped form (behavior-preserving); the remaining escape hatches (`unscoped_global()` / `unscoped_bun_vm()` / `.unscoped()` and unscoped `#[host_fn]`s) are pinned per file by `test/internal/source-lints/scope-escapes.test.ts`. Also expresses the `make_*_with_bytes_no_copy` "pointer stays valid until the deallocator runs" contracts as ownership transfer (`typed_array_from_owned_slice` / `_from_vec` / `_from_owner`, `ForeignBytes`, `external_string_from_utf16`, `OwnedUrl`, `OwnedTextCodec`, `StoreRef::adopt`, `bun_sys::Mmap`, `EventLoop::scope`), replacing hand-paired create/destroy and leak-and-remember-to-free code paths. The static `Bun.CryptoHasher.hash` / `Bun.password.verifySync` argument detach bugs that motivated the layer were fixed independently on main (#36165) by coercing every argument first; here the same behavior is expressed through deferred `materialize` under the shared scope borrow, so reordering the view capture before a coercion is a borrow error. Both main's regression tests and the layer's suites pass. Rebase onto main (461 commits): 40 files conflicted; resolved by taking main's text and re-applying only the scope transformation. Changes that main made obsolete were dropped (TextEncoderStreamEncoder host fns, EventLoop::with_pipe_read_buffer, JSC__JSMap__size global arg, the sendHelperChild scoping). Follow-ups main's newer code required: JSValue::create_buffer_from_foreign now returns JsResult (the binding became fallible on main), ArrayBufferSink::end_from_js uses or_pending_exception (empty-jsvalue-laundering lint), TextDecoder createForStream uses struct update syntax (clippy, since the PR removes TextDecoder's Drop impl), ForeignBytes recorded in the vm-thread-door inventory, scope-escape limits regenerated, and the ratchet's regeneration mode is gated on an explicit --update flag. Second rebase (30 more commits, onto 8bc4d2a): two conflicts from the zero-fill removal (#39417). The zstd sync functions keep main's Failure enum and create_buffer_from_box behind the scoped signatures, and the latin1 TextDecoder path keeps main's uninitialized Vec but hands it to JSC through external_string_from_utf16_vec instead of the raw to_external_u16, matching the file's other two decode paths. Third rebase (6 more commits, onto 0002bf8): conflicts were all with the dead-code sweeps (#39420, #39448). Dropped the scoping of things main deleted (Bun.nanoseconds' host fn, ArrayBufferSink::to_js, the unreachable csrf error arm, three unused node:: re-exports) and kept the ownership refactor of ArrayBufferSink::end_from_js. Inventories regenerated; the jsresult-swallow one also picks up a count #39448 left stale on main. Fourth rebase (17 more commits, onto 258517a): do_publish keeps #39389's shape (topic JSString held and ensure_still_alive'd across the message conversion) under the scoped signature, and the valkey publish scoping sits after the command block #29339 added. Fifth rebase (42 more commits, onto 6948a12): OwnedTextCodec is gone with the WebKit codecs (#39485); TextDecoder keeps main's encoding_rs path and this PR's external_string_from_utf16_vec hand-offs, including on the new path. ArrayBuffer::from_owned_bytes stays deleted (no callers; its u32 cast that #39558 fixed never existed in the replacements), and the no-copy deallocator contract now states both the Err-path timing (the deallocator can run before Err, per #39558) and the cross-thread timing this PR's Send bounds rely on. Sixth rebase (18 more commits, onto 681a49b): three dead-code conflicts. The scoped js_assert_settings goes away with the native assertSettings (#38900, node:http2 validates in JS now), ParseArgumentsCfg's unused Default impl stays removed (#39585), and TimeoutObject keeps main's generated cached-accessor import next to the scoped imports. Seventh rebase (14 more commits, onto 32e8703): valkey subscribe() keeps the structure #39547 gave it (channel type check first, dial plus send_rejection() before a listener is stored, no trailing else) with the rejection and the new check spelled through the scope. Eighth rebase (36 more commits, onto 56c4e3d): the three expect matcher utils take #36912's propagating print_value; the conflict was only the line wrapping. memory_pressure.rs (new on main) is added to the scope-escape limits. Ninth rebase (17 more commits, onto 72ec6e2, which includes the #39839 build fix): FileSink::on_close combines this PR's with_mut probe with the parameterless ReadableStream::done() and is_some() guard from #39732. Tenth rebase (5 more commits, onto 4448a2e): the Windows cluster handle path keeps #39804's `?` on attach_windows_socket_payload under the scoped argument spelling. Eleventh rebase (7 more commits, onto a21f02a): pbkdf2/pbkdf2Sync take the bodies #39922 gave them (from_js also returns the callback, pbkdf2 returns undefined, length 6) under the scoped signatures. Twelfth rebase (16 more commits, onto 6fb7102): the safe vm_loop_ctx is re-applied onto #40002's Cell-based upgrade client, including inside the new clear_data's with_mut. Thirteenth rebase (15 more commits, onto d0f6486): the assert binding is scoped (5 hatches now, was 1); #39995's named-pipe live-count testing fn is scoped like its neighbours. Fourteenth rebase (12 more commits, onto e8300da): cron_remove's tail takes #40024's safe ThisPtr start_linux call under the scoped return; the rest of cron.rs's Cell/ThisPtr rewrite auto-merged. node_crypto_binding's scope-escape limit rises by the two unscoped argon2 host fns #37015 added. Fifteenth rebase (6 more commits, onto 1423031): two conflicts with the defer-comment sweep (#40051): PasswordObject's verifySync keeps this PR's deferred materialize of both arguments, and NodeHTTPResponse's on_resolve keeps the scoped call, both without the removed defer comments. Sixteenth rebase (31 more commits, onto 01008f8): the socket ref()/unref() host fns take #39856's bodies (hold the loop while connecting, apply the recorded state on open) under the scoped signatures. Seventeenth rebase (8 more commits, onto 4bb20e5): #40055 made the websocket upgrade client's loop-context plumbing safe itself (and dropped the adapter), so this PR's vm_loop_ctx change there is retired and both http_jsc files are main's. Eighteenth rebase (8 more commits, onto 3e347b3): the rootError crash hook keeps #37181's one-argument handle_root_error under the scoped signature. Nineteenth rebase (6 more commits, onto 7a5d837): #40251 removed the exception checks that follow already-checked calls and made JSString::to_slice / view, JSValue::get_zig_string and handle_ipc_message return JsResult. Eight files conflicted inside scoped bodies (BunObject, CryptoHasher, PasswordObject, ipc_host, node_util_binding, server_body, expect, ObjectURLRegistry); main's control flow is kept (the guards go, the ? is added) under the scoped spellings. The four has_exception checks left in BunObject.rs are the ones main kept (print_table / format2 swallow nested throws). Twentieth rebase (9 more commits, onto f2fe7d3): 32 files conflicted, nearly all with #40238 (bun_core::String owns its WTF ref). Main's ownership idioms replace this PR's: OwnedString / scopeguard deref wrappers and manual .deref() calls go (String drops its ref), into_js replaces transfer_to_js (Scope::transfer_string now consumes the String), JSValue::get_zig_string is gone so Local::get_zig_string becomes Local::to_js_string_view (the JSStringView guard keeps the cell alive), and to_slice_or_null collapses into to_slice. OwnedUrl is retired: main's whatwg::Parsed is the same RAII handle, so src/jsc/URL.rs and js_valkey.rs are main's again. ScopeFunctions.rs is rebuilt from main's text (the strings module is gone, names are &'static str) with the 14 host fns scoped and rustfmt applied; jest.rs and expect.rs take main's literals under this PR's wrapping. CachedStructure keeps main's assume_init_mut / drop_in_place sequence over this PR's slice-taking create_structure. UDP address getters add the ? main's create_sock_addr now needs. Scope-escape limits drop by one in BunObject, node_util_binding and server_body and by two in FormData (hatches replaced by scoped calls). Twenty-first rebase (7 more commits, onto 8335017): one import-line conflict in node_fs_binding.rs, where #38383 added the SystemErrorJsc trait import next to this PR's scoped imports. Both kept; no inventory changes.
…>) (#40374) ### What does this PR do? Follow-up to #40238. Two string helper types still had no lifetime: - `ZigString` — a `{ptr, len}` view with encoding bits (Latin-1 / UTF-8 / UTF-16). Now **`EncodedSlice<'a>`**, same 16-byte layout, `'a` = the bytes it was built over. The lifetime-free POD remains only as the `BunString` union arm. - `ZigStringSlice` — "these characters as UTF-8 bytes": borrowed when already UTF-8/ASCII, a transcoded `Vec` otherwise, or a view kept alive by a `StringImpl` ref. Its borrowed variants were raw pointers, so this compiled: ```rust let s = value.to_bun_string(global)?.to_utf8(); // temp String dropped here use(s.slice()); // only valid because to_utf8() had taken a ref ``` Now **`Utf8Bytes<'a>`**: ```rust pub enum Utf8Bytes<'a> { Borrowed(&'a [u8]), Owned(Vec<u8>), Shared(String) /* 8-bit ASCII, holds the ref */ } impl String { fn to_utf8(&self) -> Utf8Bytes<'_>; // borrow (no ref) or transcode; scoped to self fn into_utf8(self) -> Utf8Bytes<'static>; // for storing: moves self's ref into Shared } ``` `to_utf8_without_ref` / `to_utf8_borrowed` fold into `to_utf8` (the `&self` borrow is what keeps the impl alive now). `StringView<'a>::to_utf8` is `'a`-scoped; `JSStringView::to_utf8` is scoped to the guard. `SliceWithUnderlyingString` becomes `Utf8WithString { utf8: Option<Vec<u8>>, string: String }` (built by `String::into_utf8_with_string()`): the `Vec` only when transcoding was needed, the slice derived from `string` otherwise, replacing a self-referential raw view. With lifetimes in place the compiler pointed at code that kept a slice past the Rust value it came from. At runtime these were kept valid by the ref the old `to_utf8()` took (so none is a live use-after-free on `main`), but that was invisible in the types; they now own their bytes via `into_utf8()` or keep the source alive explicitly: - code-coverage source-URL map (`ByteRangeMapping`), shell worker argv (`vm_args_utf8`), `SocketConfig.hostname_or_unix`, MySQL `Value`/query string, S3 credential option strings, bake framework config strings, valkey command arguments, hosted-git-info committish, `console` JSX tag / `%s` args, `Crypto` algorithm name - `path.join`/`path.resolve`: each argument's `JSStringView` guard was dropped when the loop advanced while its bytes were kept; for a `String` object argument whose `toString` returns a fresh string (Bun's `is_string()` accepts boxed strings, unlike Node), nothing else rooted that string. The guards are now held until the join/resolve runs. `path.basename(path, ext)` and `udpSocket.send`/`sendMany` get the same scoping (their cells were already rooted). - `fetch("data:…")` moved the URL `String` value while a slice borrowed through it, and `ZigStackFrame.source_url` was reassigned while its UTF-8 borrow was live — both fine at runtime (the bytes live in the `StringImpl`, which held a ref), both rejected by the borrow checker now; restructured ### Fixes Renaming the neutral `ZigString::init` to `EncodedSlice::latin1` made these visibly wrong — they built a Latin-1 view over UTF-8 bytes (the same code existed in Zig and on `main`), so non-ASCII input decoded as mojibake: - `Bun.Transpiler#scan` / `scanImports`: import specifiers (`"./módulo-ü.js"` came back as `"./módulo-ü.js"`) — test added - `bun test --rerun-each=N` under a non-ASCII path: the module-registry key was Latin-1-decoded, the entry was never evicted, and the file body ran once instead of N times — test added - `Bun.cwd`, `Bun.origin`, and `Bun.main` when the entry is `[eval]`/`[stdin]` or not openable; `bun:ffi` `cc` compiler error text and non-ASCII symbol names (as the `symbols` property key and the function's `name`); the `"N errors building <path>"` AggregateError message - `fs.readFile(path, "utf8")` (any string encoding) of an empty file embedded in a `bun build --compile` executable returned `ENOMEM` (the empty result was built on a `Dead` string); it returns `""` — test added. - `Zig::getErrorInstance` (C++) used the non-copying `toString`, so an `Error` created from a temporary Rust buffer via `EncodedSlice::to_error_instance` kept `message` pointing at freed memory (auto-install / package-resolution failure messages, the `bun:ffi` `cc()` compile-error message). It now copies, like `getTypeErrorInstance` already did. Other observable changes: - node:http `upgrade` with `options.headers`: a `Sec-WebSocket-Protocol` / `Sec-WebSocket-Extensions` value containing bytes ≥ 0x80 was transcoded Latin-1→UTF-8 twice on the wire (`é` became `C3 83 C2 A9`); it is passed through once now. - `new Blob([...parts])` where the string parts come from nested arrays or non-literal values: an all-ASCII result now takes the ASCII fast path (main pessimistically marked it as possibly non-ASCII because the old slice type couldn't tell "borrowed with a ref" from "transcoded"). - If building the `specifier`/`referrer`/`url` properties of a module-resolution or auto-install error itself fails (string too long / OOM), that exception is cleared and the `import()` promise is rejected with the error as built so far (main set such a property to `""`). - `valkey.hset(key, {field: value, …})` / `hmset` read the field-name bytes after the property iterator that owned them was destroyed; the iterator now outlives the command send. - Not JS-visible: error messages built from a temporary Rust buffer (`EncodedSlice::utf8(bytes).to_*_error_instance`, formatted `create_*_error_instance`) are copied once; a `String` message shares its impl and an argument-free ASCII literal is atomized, all through one `BunString__toErrorInstance(kind)`; coded errors with a literal message (`global.err(CODE, "…")`, `throw_invalid_arguments("…")`) get an atomized message because `String::create_format` returns `static_` for an argument-free ASCII literal; `EncodedSlice::to_owned_slice` no longer writes a hidden NUL past `len` (no caller read it; `to_owned_slice_z` is the NUL-terminated form); `EncodedSlice::slice()` no longer truncates at `u32::MAX`; `JSC__JSFunction__getSourceCode` returns an owned string instead of a view into the SourceProvider; S3 credential option strings are stored once (main kept both a boxed copy and the source slice, pinning the JS string for the request). Also done while every site was being touched: - Constructors say what they tag: `EncodedSlice::{latin1, utf8, utf16, from_bytes}` (was `init`/`init_utf8`/`init_utf16`/`from_bytes`+`with_encoding`); `String::init` (an alias of `from_bytes`/`static_`) is removed. `JSValue::to_slice` → `JSValue::to_utf8`; `eql_comptime`/`has_prefix_comptime` → `eq_ascii`/`starts_with_ascii`; `PathLike<'a>`/`StringOrBuffer<'a>`/`PathOrFileDescriptor<'a>` arms are `String(Utf8WithString) | ThreadsafeString | Utf8(Utf8Bytes<'a>)` (`PathLike::Bytes(CowSlice)` folded into `Utf8`): a path borrowed for a synchronous syscall carries the lifetime of its bytes instead of an unchecked `CowSlice`, and `to_thread_safe`/`from_js` exist only on the `'static` instantiation so a borrowed path cannot reach the work pool. - One spelling for "Rust UTF-8 bytes → JS string": `bun_string_jsc::create_utf8_for_js(global, bytes)` (≈95 sites that used `EncodedSlice::…(x).to_js` / `String::…(x).to_js`); ASCII literals use `String::static_("..").to_js`. Decoding is identical for valid and invalid UTF-8; the only difference is that a result over the maximum JS string length now throws (`STRING_TOO_LONG` for 8-bit, an out-of-memory error for 16-bit) instead of evaluating to `""`, and this path does not consult the synthetic allocation limit (`setSyntheticAllocationLimitForTesting` / `BUN_FEATURE_FLAG_SYNTHETIC_MEMORY_LIMIT`), so strings between that limit and the real maximum are returned rather than `""`. - One path for "owned UTF-8 `Vec` → JS string" (`String::from_owned_utf8` / `owned_utf8_into_js`, used by `Utf8WithString`, `Blob.text()`, `TextDecoder`, Build/ResolveMessage `toString`); `owned_utf16_into_js(global, Vec<u16>)` is the UTF-16 counterpart (the separate `EncodedSlice__toExternalU16` export is removed). `EncodedSliceJsc::to_external_value`/`external` return `JsResult` instead of `ZERO` with a pending exception. - Errors from a message: `global.create_*_error_instance(format_args!(..))` everywhere; `JSC__create{Error,TypeError,RangeError}`, `EncodedSlice__to{Error,TypeError,SyntaxError,RangeError}Instance` and helpers.h `get*ErrorInstance` collapse into `BunString__toErrorInstance(str, global, BunErrorKind)`. `create_aggregate_error` takes `format_args!` like its siblings. - The `BunString` PODs (`Tag`, `EncodedSlice`, `StringImpl`, `String`) move from `bun_alloc` into `bun_core::string`; the newtype/`Deref` layering over them is gone. - Removed duplicates/dead code the rename exposed: `EncodedSliceJsc::with_encoding` (= `from_bytes`), `EncodedSlice::static_` (= `latin1`), `set_output_encoding`/`detect_encoding` (= `from_bytes`), node_os's private copy of the trait, `Utf8Bytes::{init_dupe, is_allocated, take_owned_raw}`, nine write-only `S3CredentialsWithOptions::_*_slice` fields, `Terminal::_term_name`, `Response::get_utf8_url`, `bun_ast_jsc::{msg_to_js, log_to_js, log_to_js_aggregate_error, log_to_js_array}` and `css_jsc::error_jsc` (callers use `bun_jsc::LogJsc` / `create_error_instance`), and unused exports `EncodedSlice__free/__toAtomicValue/__to16BitValue/__toExternalValueWithCallback`, `JSC__JSValue__symbolKeyFor/createRangeError/createTypeError/putRecord/createStringArray/hasOwnProperty`, `JSC__JSGlobalObject__{get,put}CachedObject`, `JSC__JSObject__{get,put}Direct`, `WebCore__DOMURL__href_/pathname_`, `Zig::toJSString`, `Zig::toString(EncodedSlice, StringPointer)`. C++ side renamed to match: `struct EncodedSlice`, `BunStringTag::EncodedSlice`/`StaticEncodedSlice`, `Zig::toEncodedSlice`, `EncodedSlice__*` exports (cppbind type map updated). New debug assertions: `String::static_` requires ASCII, `EncodedSliceJsc::to_js` requires a non-global-allocated slice, `EncodedSlice__toExternalValue` requires a non-UTF-8 tag. No behaviour change intended beyond the fixes above; `to_utf8()` on a WTF-backed ASCII string is now zero atomics instead of a ref/deref pair. ### How did you verify your code works? New tests: `test/bundler/transpiler/transpiler.test.js` (scanImports non-ASCII specifiers), `test/cli/test/rerun-each.test.ts` (non-ASCII path), and `test/bundler/compile-asset-bunfs.test.ts` (empty embedded file via `readFileSync(.., "utf8")`, dirent `parentPath`, ENOENT `err.path`) — all fail on 1.4. `cargo check`/`clippy` on linux, windows-msvc and macOS targets; debug+ASAN build driven through `node:path` (join/resolve/basename/relative with non-ASCII), `fs` with string/Buffer/URL paths, `Bun.$` interpolation and `$.escape`, `udpSocket.send`/`sendMany`, `fetch("data:")`, `CryptoHasher`, `Bun.inspect`, WebSocket send/publish, module loading, `Bun.Transpiler`, `RedisClient` URL parsing; `BUN_JSC_validateExceptionChecks=1` clean on the same script. Ran path, fs, shell, udp, inspect, inspect-error, fetch, serve, http2, websocket-server, socket, text-encoder, zlib, postgres-string-leak and valkey-gc test files on debug and release builds; results match `main`'s debug build (remaining local failures — `mkdtempSync() empty name` EACCES, `broken pipe subproc`, Blob utf16 gc — reproduce on `main`).
…rship contracts compile errors Rebased onto main as a single commit; the branch history (with its merge commits) is not preserved. Adds a zero-cost branded scope layer over the raw JSC FFI (`src/jsc/scope.rs`: `Scope<'s>`, `Local<'s>`) that makes two boundary bug classes compile errors for code that stays on the scoped API: - a JS value escaping its host call unrooted (persisting requires the explicit `Scope::persist` -> `Strong`); - a JS-heap view (`Local::array_buffer_bytes`) held across an operation that can re-enter user JS (`&mut Scope`), e.g. a coercion that detaches the buffer. Codegen integration: - `#[bun_jsc::host_fn(scoped)]`: functions written as `fn(scope: &mut Scope, callframe) -> JsResult<Local>` get a macro-synthesized wrapper under their original name and unscoped signature, so js2native / `.classes.ts` / direct-call wiring stays byte-compatible. User `cfg`/doc/lint attributes propagate to the public wrapper and the extern shims. - `ZIG_EXPORT(tag, reenters_js | no_user_js)` effect markers on the C++ declarations; explicitly classified functions get branded wrappers generated into `bun_jsc::cpp::scoped` (`&mut Scope` / `&Scope`), unclassified and `null_is_throw` functions get none. All classified exports are verified against their C++ (`toMatch` and `putMayBeIndex` are `reenters_js`: a non-uint32 `lastIndex` goes through ToNumber, and index puts on exotic receivers reach `defineOwnProperty` traps). Migration: ~470 host functions are converted to the scoped form (behavior-preserving); the remaining escape hatches (`unscoped_global()` / `unscoped_bun_vm()` / `.unscoped()` and unscoped `#[host_fn]`s) are pinned per file by `test/internal/source-lints/scope-escapes.test.ts`. Also expresses the `make_*_with_bytes_no_copy` "pointer stays valid until the deallocator runs" contracts as ownership transfer (`typed_array_from_owned_slice` / `_from_vec` / `_from_owner`, `ForeignBytes`, `external_string_from_utf16`, `OwnedUrl`, `OwnedTextCodec`, `StoreRef::adopt`, `bun_sys::Mmap`, `EventLoop::scope`), replacing hand-paired create/destroy and leak-and-remember-to-free code paths. The static `Bun.CryptoHasher.hash` / `Bun.password.verifySync` argument detach bugs that motivated the layer were fixed independently on main (#36165) by coercing every argument first; here the same behavior is expressed through deferred `materialize` under the shared scope borrow, so reordering the view capture before a coercion is a borrow error. Both main's regression tests and the layer's suites pass. Rebase onto main (461 commits): 40 files conflicted; resolved by taking main's text and re-applying only the scope transformation. Changes that main made obsolete were dropped (TextEncoderStreamEncoder host fns, EventLoop::with_pipe_read_buffer, JSC__JSMap__size global arg, the sendHelperChild scoping). Follow-ups main's newer code required: JSValue::create_buffer_from_foreign now returns JsResult (the binding became fallible on main), ArrayBufferSink::end_from_js uses or_pending_exception (empty-jsvalue-laundering lint), TextDecoder createForStream uses struct update syntax (clippy, since the PR removes TextDecoder's Drop impl), ForeignBytes recorded in the vm-thread-door inventory, scope-escape limits regenerated, and the ratchet's regeneration mode is gated on an explicit --update flag. Second rebase (30 more commits, onto 8bc4d2a): two conflicts from the zero-fill removal (#39417). The zstd sync functions keep main's Failure enum and create_buffer_from_box behind the scoped signatures, and the latin1 TextDecoder path keeps main's uninitialized Vec but hands it to JSC through external_string_from_utf16_vec instead of the raw to_external_u16, matching the file's other two decode paths. Third rebase (6 more commits, onto 0002bf8): conflicts were all with the dead-code sweeps (#39420, #39448). Dropped the scoping of things main deleted (Bun.nanoseconds' host fn, ArrayBufferSink::to_js, the unreachable csrf error arm, three unused node:: re-exports) and kept the ownership refactor of ArrayBufferSink::end_from_js. Inventories regenerated; the jsresult-swallow one also picks up a count #39448 left stale on main. Fourth rebase (17 more commits, onto 258517a): do_publish keeps #39389's shape (topic JSString held and ensure_still_alive'd across the message conversion) under the scoped signature, and the valkey publish scoping sits after the command block #29339 added. Fifth rebase (42 more commits, onto 6948a12): OwnedTextCodec is gone with the WebKit codecs (#39485); TextDecoder keeps main's encoding_rs path and this PR's external_string_from_utf16_vec hand-offs, including on the new path. ArrayBuffer::from_owned_bytes stays deleted (no callers; its u32 cast that #39558 fixed never existed in the replacements), and the no-copy deallocator contract now states both the Err-path timing (the deallocator can run before Err, per #39558) and the cross-thread timing this PR's Send bounds rely on. Sixth rebase (18 more commits, onto 681a49b): three dead-code conflicts. The scoped js_assert_settings goes away with the native assertSettings (#38900, node:http2 validates in JS now), ParseArgumentsCfg's unused Default impl stays removed (#39585), and TimeoutObject keeps main's generated cached-accessor import next to the scoped imports. Seventh rebase (14 more commits, onto 32e8703): valkey subscribe() keeps the structure #39547 gave it (channel type check first, dial plus send_rejection() before a listener is stored, no trailing else) with the rejection and the new check spelled through the scope. Eighth rebase (36 more commits, onto 56c4e3d): the three expect matcher utils take #36912's propagating print_value; the conflict was only the line wrapping. memory_pressure.rs (new on main) is added to the scope-escape limits. Ninth rebase (17 more commits, onto 72ec6e2, which includes the #39839 build fix): FileSink::on_close combines this PR's with_mut probe with the parameterless ReadableStream::done() and is_some() guard from #39732. Tenth rebase (5 more commits, onto 4448a2e): the Windows cluster handle path keeps #39804's `?` on attach_windows_socket_payload under the scoped argument spelling. Eleventh rebase (7 more commits, onto a21f02a): pbkdf2/pbkdf2Sync take the bodies #39922 gave them (from_js also returns the callback, pbkdf2 returns undefined, length 6) under the scoped signatures. Twelfth rebase (16 more commits, onto 6fb7102): the safe vm_loop_ctx is re-applied onto #40002's Cell-based upgrade client, including inside the new clear_data's with_mut. Thirteenth rebase (15 more commits, onto d0f6486): the assert binding is scoped (5 hatches now, was 1); #39995's named-pipe live-count testing fn is scoped like its neighbours. Fourteenth rebase (12 more commits, onto e8300da): cron_remove's tail takes #40024's safe ThisPtr start_linux call under the scoped return; the rest of cron.rs's Cell/ThisPtr rewrite auto-merged. node_crypto_binding's scope-escape limit rises by the two unscoped argon2 host fns #37015 added. Fifteenth rebase (6 more commits, onto 1423031): two conflicts with the defer-comment sweep (#40051): PasswordObject's verifySync keeps this PR's deferred materialize of both arguments, and NodeHTTPResponse's on_resolve keeps the scoped call, both without the removed defer comments. Sixteenth rebase (31 more commits, onto 01008f8): the socket ref()/unref() host fns take #39856's bodies (hold the loop while connecting, apply the recorded state on open) under the scoped signatures. Seventeenth rebase (8 more commits, onto 4bb20e5): #40055 made the websocket upgrade client's loop-context plumbing safe itself (and dropped the adapter), so this PR's vm_loop_ctx change there is retired and both http_jsc files are main's. Eighteenth rebase (8 more commits, onto 3e347b3): the rootError crash hook keeps #37181's one-argument handle_root_error under the scoped signature. Nineteenth rebase (6 more commits, onto 7a5d837): #40251 removed the exception checks that follow already-checked calls and made JSString::to_slice / view, JSValue::get_zig_string and handle_ipc_message return JsResult. Eight files conflicted inside scoped bodies (BunObject, CryptoHasher, PasswordObject, ipc_host, node_util_binding, server_body, expect, ObjectURLRegistry); main's control flow is kept (the guards go, the ? is added) under the scoped spellings. The four has_exception checks left in BunObject.rs are the ones main kept (print_table / format2 swallow nested throws). Twentieth rebase (9 more commits, onto f2fe7d3): 32 files conflicted, nearly all with #40238 (bun_core::String owns its WTF ref). Main's ownership idioms replace this PR's: OwnedString / scopeguard deref wrappers and manual .deref() calls go (String drops its ref), into_js replaces transfer_to_js (Scope::transfer_string now consumes the String), JSValue::get_zig_string is gone so Local::get_zig_string becomes Local::to_js_string_view (the JSStringView guard keeps the cell alive), and to_slice_or_null collapses into to_slice. OwnedUrl is retired: main's whatwg::Parsed is the same RAII handle, so src/jsc/URL.rs and js_valkey.rs are main's again. ScopeFunctions.rs is rebuilt from main's text (the strings module is gone, names are &'static str) with the 14 host fns scoped and rustfmt applied; jest.rs and expect.rs take main's literals under this PR's wrapping. CachedStructure keeps main's assume_init_mut / drop_in_place sequence over this PR's slice-taking create_structure. UDP address getters add the ? main's create_sock_addr now needs. Scope-escape limits drop by one in BunObject, node_util_binding and server_body and by two in FormData (hatches replaced by scoped calls). Twenty-first rebase (7 more commits, onto 8335017): one import-line conflict in node_fs_binding.rs, where #38383 added the SystemErrorJsc trait import next to this PR's scoped imports. Both kept; no inventory changes. Twenty-second rebase (6 more commits, onto 0823e50): 39 files conflicted, all with #40374 (Utf8Bytes<'a> / EncodedSlice<'a>). Main's types replace the PR's spellings inside scoped bodies: Local::to_slice is now Local::to_utf8 (Utf8Bytes<'static>), ZigString::init(..).to_js and create_utf8_for_js calls become scope.string_utf8 / scope.string, and ScopedStringOrBuffer names StringOrBuffer<'static>. Main's owned_utf16_into_js supersedes this PR's external_string_from_utf16*, so src/jsc/ZigString.rs stays deleted and bun_string_jsc.rs and TextDecoder.rs are main's again. Scope-escape limits drop in filesystem_router (13 to 7), server_body (17 to 15) and Listener (11 to 9).
…rship contracts compile errors Rebased onto main as a single commit; the branch history (with its merge commits) is not preserved. Adds a zero-cost branded scope layer over the raw JSC FFI (`src/jsc/scope.rs`: `Scope<'s>`, `Local<'s>`) that makes two boundary bug classes compile errors for code that stays on the scoped API: - a JS value escaping its host call unrooted (persisting requires the explicit `Scope::persist` -> `Strong`); - a JS-heap view (`Local::array_buffer_bytes`) held across an operation that can re-enter user JS (`&mut Scope`), e.g. a coercion that detaches the buffer. Codegen integration: - `#[bun_jsc::host_fn(scoped)]`: functions written as `fn(scope: &mut Scope, callframe) -> JsResult<Local>` get a macro-synthesized wrapper under their original name and unscoped signature, so js2native / `.classes.ts` / direct-call wiring stays byte-compatible. User `cfg`/doc/lint attributes propagate to the public wrapper and the extern shims. - `ZIG_EXPORT(tag, reenters_js | no_user_js)` effect markers on the C++ declarations; explicitly classified functions get branded wrappers generated into `bun_jsc::cpp::scoped` (`&mut Scope` / `&Scope`), unclassified and `null_is_throw` functions get none. All classified exports are verified against their C++ (`toMatch` and `putMayBeIndex` are `reenters_js`: a non-uint32 `lastIndex` goes through ToNumber, and index puts on exotic receivers reach `defineOwnProperty` traps). Migration: ~470 host functions are converted to the scoped form (behavior-preserving); the remaining escape hatches (`unscoped_global()` / `unscoped_bun_vm()` / `.unscoped()` and unscoped `#[host_fn]`s) are pinned per file by `test/internal/source-lints/scope-escapes.test.ts`. Also expresses the `make_*_with_bytes_no_copy` "pointer stays valid until the deallocator runs" contracts as ownership transfer (`typed_array_from_owned_slice` / `_from_vec` / `_from_owner`, `ForeignBytes`, `external_string_from_utf16`, `OwnedUrl`, `OwnedTextCodec`, `StoreRef::adopt`, `bun_sys::Mmap`, `EventLoop::scope`), replacing hand-paired create/destroy and leak-and-remember-to-free code paths. The static `Bun.CryptoHasher.hash` / `Bun.password.verifySync` argument detach bugs that motivated the layer were fixed independently on main (#36165) by coercing every argument first; here the same behavior is expressed through deferred `materialize` under the shared scope borrow, so reordering the view capture before a coercion is a borrow error. Both main's regression tests and the layer's suites pass. Rebase onto main (461 commits): 40 files conflicted; resolved by taking main's text and re-applying only the scope transformation. Changes that main made obsolete were dropped (TextEncoderStreamEncoder host fns, EventLoop::with_pipe_read_buffer, JSC__JSMap__size global arg, the sendHelperChild scoping). Follow-ups main's newer code required: JSValue::create_buffer_from_foreign now returns JsResult (the binding became fallible on main), ArrayBufferSink::end_from_js uses or_pending_exception (empty-jsvalue-laundering lint), TextDecoder createForStream uses struct update syntax (clippy, since the PR removes TextDecoder's Drop impl), ForeignBytes recorded in the vm-thread-door inventory, scope-escape limits regenerated, and the ratchet's regeneration mode is gated on an explicit --update flag. Second rebase (30 more commits, onto 8bc4d2a): two conflicts from the zero-fill removal (#39417). The zstd sync functions keep main's Failure enum and create_buffer_from_box behind the scoped signatures, and the latin1 TextDecoder path keeps main's uninitialized Vec but hands it to JSC through external_string_from_utf16_vec instead of the raw to_external_u16, matching the file's other two decode paths. Third rebase (6 more commits, onto 0002bf8): conflicts were all with the dead-code sweeps (#39420, #39448). Dropped the scoping of things main deleted (Bun.nanoseconds' host fn, ArrayBufferSink::to_js, the unreachable csrf error arm, three unused node:: re-exports) and kept the ownership refactor of ArrayBufferSink::end_from_js. Inventories regenerated; the jsresult-swallow one also picks up a count #39448 left stale on main. Fourth rebase (17 more commits, onto 258517a): do_publish keeps #39389's shape (topic JSString held and ensure_still_alive'd across the message conversion) under the scoped signature, and the valkey publish scoping sits after the command block #29339 added. Fifth rebase (42 more commits, onto 6948a12): OwnedTextCodec is gone with the WebKit codecs (#39485); TextDecoder keeps main's encoding_rs path and this PR's external_string_from_utf16_vec hand-offs, including on the new path. ArrayBuffer::from_owned_bytes stays deleted (no callers; its u32 cast that #39558 fixed never existed in the replacements), and the no-copy deallocator contract now states both the Err-path timing (the deallocator can run before Err, per #39558) and the cross-thread timing this PR's Send bounds rely on. Sixth rebase (18 more commits, onto 681a49b): three dead-code conflicts. The scoped js_assert_settings goes away with the native assertSettings (#38900, node:http2 validates in JS now), ParseArgumentsCfg's unused Default impl stays removed (#39585), and TimeoutObject keeps main's generated cached-accessor import next to the scoped imports. Seventh rebase (14 more commits, onto 32e8703): valkey subscribe() keeps the structure #39547 gave it (channel type check first, dial plus send_rejection() before a listener is stored, no trailing else) with the rejection and the new check spelled through the scope. Eighth rebase (36 more commits, onto 56c4e3d): the three expect matcher utils take #36912's propagating print_value; the conflict was only the line wrapping. memory_pressure.rs (new on main) is added to the scope-escape limits. Ninth rebase (17 more commits, onto 72ec6e2, which includes the #39839 build fix): FileSink::on_close combines this PR's with_mut probe with the parameterless ReadableStream::done() and is_some() guard from #39732. Tenth rebase (5 more commits, onto 4448a2e): the Windows cluster handle path keeps #39804's `?` on attach_windows_socket_payload under the scoped argument spelling. Eleventh rebase (7 more commits, onto a21f02a): pbkdf2/pbkdf2Sync take the bodies #39922 gave them (from_js also returns the callback, pbkdf2 returns undefined, length 6) under the scoped signatures. Twelfth rebase (16 more commits, onto 6fb7102): the safe vm_loop_ctx is re-applied onto #40002's Cell-based upgrade client, including inside the new clear_data's with_mut. Thirteenth rebase (15 more commits, onto d0f6486): the assert binding is scoped (5 hatches now, was 1); #39995's named-pipe live-count testing fn is scoped like its neighbours. Fourteenth rebase (12 more commits, onto e8300da): cron_remove's tail takes #40024's safe ThisPtr start_linux call under the scoped return; the rest of cron.rs's Cell/ThisPtr rewrite auto-merged. node_crypto_binding's scope-escape limit rises by the two unscoped argon2 host fns #37015 added. Fifteenth rebase (6 more commits, onto 1423031): two conflicts with the defer-comment sweep (#40051): PasswordObject's verifySync keeps this PR's deferred materialize of both arguments, and NodeHTTPResponse's on_resolve keeps the scoped call, both without the removed defer comments. Sixteenth rebase (31 more commits, onto 01008f8): the socket ref()/unref() host fns take #39856's bodies (hold the loop while connecting, apply the recorded state on open) under the scoped signatures. Seventeenth rebase (8 more commits, onto 4bb20e5): #40055 made the websocket upgrade client's loop-context plumbing safe itself (and dropped the adapter), so this PR's vm_loop_ctx change there is retired and both http_jsc files are main's. Eighteenth rebase (8 more commits, onto 3e347b3): the rootError crash hook keeps #37181's one-argument handle_root_error under the scoped signature. Nineteenth rebase (6 more commits, onto 7a5d837): #40251 removed the exception checks that follow already-checked calls and made JSString::to_slice / view, JSValue::get_zig_string and handle_ipc_message return JsResult. Eight files conflicted inside scoped bodies (BunObject, CryptoHasher, PasswordObject, ipc_host, node_util_binding, server_body, expect, ObjectURLRegistry); main's control flow is kept (the guards go, the ? is added) under the scoped spellings. The four has_exception checks left in BunObject.rs are the ones main kept (print_table / format2 swallow nested throws). Twentieth rebase (9 more commits, onto f2fe7d3): 32 files conflicted, nearly all with #40238 (bun_core::String owns its WTF ref). Main's ownership idioms replace this PR's: OwnedString / scopeguard deref wrappers and manual .deref() calls go (String drops its ref), into_js replaces transfer_to_js (Scope::transfer_string now consumes the String), JSValue::get_zig_string is gone so Local::get_zig_string becomes Local::to_js_string_view (the JSStringView guard keeps the cell alive), and to_slice_or_null collapses into to_slice. OwnedUrl is retired: main's whatwg::Parsed is the same RAII handle, so src/jsc/URL.rs and js_valkey.rs are main's again. ScopeFunctions.rs is rebuilt from main's text (the strings module is gone, names are &'static str) with the 14 host fns scoped and rustfmt applied; jest.rs and expect.rs take main's literals under this PR's wrapping. CachedStructure keeps main's assume_init_mut / drop_in_place sequence over this PR's slice-taking create_structure. UDP address getters add the ? main's create_sock_addr now needs. Scope-escape limits drop by one in BunObject, node_util_binding and server_body and by two in FormData (hatches replaced by scoped calls). Twenty-first rebase (7 more commits, onto 8335017): one import-line conflict in node_fs_binding.rs, where #38383 added the SystemErrorJsc trait import next to this PR's scoped imports. Both kept; no inventory changes. Twenty-second rebase (6 more commits, onto 0823e50): 39 files conflicted, all with #40374 (Utf8Bytes<'a> / EncodedSlice<'a>). Main's types replace the PR's spellings inside scoped bodies: Local::to_slice is now Local::to_utf8 (Utf8Bytes<'static>), ZigString::init(..).to_js and create_utf8_for_js calls become scope.string_utf8 / scope.string, and ScopedStringOrBuffer names StringOrBuffer<'static>. Main's owned_utf16_into_js supersedes this PR's external_string_from_utf16*, so src/jsc/ZigString.rs stays deleted and bun_string_jsc.rs and TextDecoder.rs are main's again. Scope-escape limits drop in filesystem_router (13 to 7), server_body (17 to 15) and Listener (11 to 9). Twenty-third rebase (9 more commits, onto adc354d): two files. FileSystemRouter::routes takes #40410's fallible JSValue::from_entries (mapped into the scope), and advanceTimersByTime keeps #40414's NaN check and main's message text under the scoped throws. The jsresult-swallow inventory is main's again (#40410 fixed the FakeTimers entry).
…rship contracts compile errors Rebased onto main as a single commit; the branch history (with its merge commits) is not preserved. Adds a zero-cost branded scope layer over the raw JSC FFI (`src/jsc/scope.rs`: `Scope<'s>`, `Local<'s>`) that makes two boundary bug classes compile errors for code that stays on the scoped API: - a JS value escaping its host call unrooted (persisting requires the explicit `Scope::persist` -> `Strong`); - a JS-heap view (`Local::array_buffer_bytes`) held across an operation that can re-enter user JS (`&mut Scope`), e.g. a coercion that detaches the buffer. Codegen integration: - `#[bun_jsc::host_fn(scoped)]`: functions written as `fn(scope: &mut Scope, callframe) -> JsResult<Local>` get a macro-synthesized wrapper under their original name and unscoped signature, so js2native / `.classes.ts` / direct-call wiring stays byte-compatible. User `cfg`/doc/lint attributes propagate to the public wrapper and the extern shims. - `ZIG_EXPORT(tag, reenters_js | no_user_js)` effect markers on the C++ declarations; explicitly classified functions get branded wrappers generated into `bun_jsc::cpp::scoped` (`&mut Scope` / `&Scope`), unclassified and `null_is_throw` functions get none. All classified exports are verified against their C++ (`toMatch` and `putMayBeIndex` are `reenters_js`: a non-uint32 `lastIndex` goes through ToNumber, and index puts on exotic receivers reach `defineOwnProperty` traps). Migration: ~470 host functions are converted to the scoped form (behavior-preserving); the remaining escape hatches (`unscoped_global()` / `unscoped_bun_vm()` / `.unscoped()` and unscoped `#[host_fn]`s) are pinned per file by `test/internal/source-lints/scope-escapes.test.ts`. Also expresses the `make_*_with_bytes_no_copy` "pointer stays valid until the deallocator runs" contracts as ownership transfer (`typed_array_from_owned_slice` / `_from_vec` / `_from_owner`, `ForeignBytes`, `external_string_from_utf16`, `OwnedUrl`, `OwnedTextCodec`, `StoreRef::adopt`, `bun_sys::Mmap`, `EventLoop::scope`), replacing hand-paired create/destroy and leak-and-remember-to-free code paths. The static `Bun.CryptoHasher.hash` / `Bun.password.verifySync` argument detach bugs that motivated the layer were fixed independently on main (#36165) by coercing every argument first; here the same behavior is expressed through deferred `materialize` under the shared scope borrow, so reordering the view capture before a coercion is a borrow error. Both main's regression tests and the layer's suites pass. Rebase onto main (461 commits): 40 files conflicted; resolved by taking main's text and re-applying only the scope transformation. Changes that main made obsolete were dropped (TextEncoderStreamEncoder host fns, EventLoop::with_pipe_read_buffer, JSC__JSMap__size global arg, the sendHelperChild scoping). Follow-ups main's newer code required: JSValue::create_buffer_from_foreign now returns JsResult (the binding became fallible on main), ArrayBufferSink::end_from_js uses or_pending_exception (empty-jsvalue-laundering lint), TextDecoder createForStream uses struct update syntax (clippy, since the PR removes TextDecoder's Drop impl), ForeignBytes recorded in the vm-thread-door inventory, scope-escape limits regenerated, and the ratchet's regeneration mode is gated on an explicit --update flag. Second rebase (30 more commits, onto 8bc4d2a): two conflicts from the zero-fill removal (#39417). The zstd sync functions keep main's Failure enum and create_buffer_from_box behind the scoped signatures, and the latin1 TextDecoder path keeps main's uninitialized Vec but hands it to JSC through external_string_from_utf16_vec instead of the raw to_external_u16, matching the file's other two decode paths. Third rebase (6 more commits, onto 0002bf8): conflicts were all with the dead-code sweeps (#39420, #39448). Dropped the scoping of things main deleted (Bun.nanoseconds' host fn, ArrayBufferSink::to_js, the unreachable csrf error arm, three unused node:: re-exports) and kept the ownership refactor of ArrayBufferSink::end_from_js. Inventories regenerated; the jsresult-swallow one also picks up a count #39448 left stale on main. Fourth rebase (17 more commits, onto 258517a): do_publish keeps #39389's shape (topic JSString held and ensure_still_alive'd across the message conversion) under the scoped signature, and the valkey publish scoping sits after the command block #29339 added. Fifth rebase (42 more commits, onto 6948a12): OwnedTextCodec is gone with the WebKit codecs (#39485); TextDecoder keeps main's encoding_rs path and this PR's external_string_from_utf16_vec hand-offs, including on the new path. ArrayBuffer::from_owned_bytes stays deleted (no callers; its u32 cast that #39558 fixed never existed in the replacements), and the no-copy deallocator contract now states both the Err-path timing (the deallocator can run before Err, per #39558) and the cross-thread timing this PR's Send bounds rely on. Sixth rebase (18 more commits, onto 681a49b): three dead-code conflicts. The scoped js_assert_settings goes away with the native assertSettings (#38900, node:http2 validates in JS now), ParseArgumentsCfg's unused Default impl stays removed (#39585), and TimeoutObject keeps main's generated cached-accessor import next to the scoped imports. Seventh rebase (14 more commits, onto 32e8703): valkey subscribe() keeps the structure #39547 gave it (channel type check first, dial plus send_rejection() before a listener is stored, no trailing else) with the rejection and the new check spelled through the scope. Eighth rebase (36 more commits, onto 56c4e3d): the three expect matcher utils take #36912's propagating print_value; the conflict was only the line wrapping. memory_pressure.rs (new on main) is added to the scope-escape limits. Ninth rebase (17 more commits, onto 72ec6e2, which includes the #39839 build fix): FileSink::on_close combines this PR's with_mut probe with the parameterless ReadableStream::done() and is_some() guard from #39732. Tenth rebase (5 more commits, onto 4448a2e): the Windows cluster handle path keeps #39804's `?` on attach_windows_socket_payload under the scoped argument spelling. Eleventh rebase (7 more commits, onto a21f02a): pbkdf2/pbkdf2Sync take the bodies #39922 gave them (from_js also returns the callback, pbkdf2 returns undefined, length 6) under the scoped signatures. Twelfth rebase (16 more commits, onto 6fb7102): the safe vm_loop_ctx is re-applied onto #40002's Cell-based upgrade client, including inside the new clear_data's with_mut. Thirteenth rebase (15 more commits, onto d0f6486): the assert binding is scoped (5 hatches now, was 1); #39995's named-pipe live-count testing fn is scoped like its neighbours. Fourteenth rebase (12 more commits, onto e8300da): cron_remove's tail takes #40024's safe ThisPtr start_linux call under the scoped return; the rest of cron.rs's Cell/ThisPtr rewrite auto-merged. node_crypto_binding's scope-escape limit rises by the two unscoped argon2 host fns #37015 added. Fifteenth rebase (6 more commits, onto 1423031): two conflicts with the defer-comment sweep (#40051): PasswordObject's verifySync keeps this PR's deferred materialize of both arguments, and NodeHTTPResponse's on_resolve keeps the scoped call, both without the removed defer comments. Sixteenth rebase (31 more commits, onto 01008f8): the socket ref()/unref() host fns take #39856's bodies (hold the loop while connecting, apply the recorded state on open) under the scoped signatures. Seventeenth rebase (8 more commits, onto 4bb20e5): #40055 made the websocket upgrade client's loop-context plumbing safe itself (and dropped the adapter), so this PR's vm_loop_ctx change there is retired and both http_jsc files are main's. Eighteenth rebase (8 more commits, onto 3e347b3): the rootError crash hook keeps #37181's one-argument handle_root_error under the scoped signature. Nineteenth rebase (6 more commits, onto 7a5d837): #40251 removed the exception checks that follow already-checked calls and made JSString::to_slice / view, JSValue::get_zig_string and handle_ipc_message return JsResult. Eight files conflicted inside scoped bodies (BunObject, CryptoHasher, PasswordObject, ipc_host, node_util_binding, server_body, expect, ObjectURLRegistry); main's control flow is kept (the guards go, the ? is added) under the scoped spellings. The four has_exception checks left in BunObject.rs are the ones main kept (print_table / format2 swallow nested throws). Twentieth rebase (9 more commits, onto f2fe7d3): 32 files conflicted, nearly all with #40238 (bun_core::String owns its WTF ref). Main's ownership idioms replace this PR's: OwnedString / scopeguard deref wrappers and manual .deref() calls go (String drops its ref), into_js replaces transfer_to_js (Scope::transfer_string now consumes the String), JSValue::get_zig_string is gone so Local::get_zig_string becomes Local::to_js_string_view (the JSStringView guard keeps the cell alive), and to_slice_or_null collapses into to_slice. OwnedUrl is retired: main's whatwg::Parsed is the same RAII handle, so src/jsc/URL.rs and js_valkey.rs are main's again. ScopeFunctions.rs is rebuilt from main's text (the strings module is gone, names are &'static str) with the 14 host fns scoped and rustfmt applied; jest.rs and expect.rs take main's literals under this PR's wrapping. CachedStructure keeps main's assume_init_mut / drop_in_place sequence over this PR's slice-taking create_structure. UDP address getters add the ? main's create_sock_addr now needs. Scope-escape limits drop by one in BunObject, node_util_binding and server_body and by two in FormData (hatches replaced by scoped calls). Twenty-first rebase (7 more commits, onto 8335017): one import-line conflict in node_fs_binding.rs, where #38383 added the SystemErrorJsc trait import next to this PR's scoped imports. Both kept; no inventory changes. Twenty-second rebase (6 more commits, onto 0823e50): 39 files conflicted, all with #40374 (Utf8Bytes<'a> / EncodedSlice<'a>). Main's types replace the PR's spellings inside scoped bodies: Local::to_slice is now Local::to_utf8 (Utf8Bytes<'static>), ZigString::init(..).to_js and create_utf8_for_js calls become scope.string_utf8 / scope.string, and ScopedStringOrBuffer names StringOrBuffer<'static>. Main's owned_utf16_into_js supersedes this PR's external_string_from_utf16*, so src/jsc/ZigString.rs stays deleted and bun_string_jsc.rs and TextDecoder.rs are main's again. Scope-escape limits drop in filesystem_router (13 to 7), server_body (17 to 15) and Listener (11 to 9). Twenty-third rebase (9 more commits, onto adc354d): two files. FileSystemRouter::routes takes #40410's fallible JSValue::from_entries (mapped into the scope), and advanceTimersByTime keeps #40414's NaN check and main's message text under the scoped throws. The jsresult-swallow inventory is main's again (#40410 fixed the FakeTimers entry). Twenty-fourth rebase (9 more commits, onto 82123d3): six files, all with #40478 (RefPtr releases on Drop). This PR's StoreRef::adopt is retired: main's RefPtr<Store> is the same owning handle, so webcore_types.rs is main's again and store_backed_buffer_to_js moves a RefPtr<Store> into the JS object as the *_from_owner owner (the view closure reaches the bytes through Store::data_mut). The sql event-loop guard keeps this PR's safe EventLoop::scope under main's renamed ref guard; expect.rs keeps this PR's wrapping over main's RefPtr comments. The vm-thread-door inventory follows main's StoreRef-to-Store rename.
…rship contracts compile errors Rebased onto main as a single commit; the branch history (with its merge commits) is not preserved. Adds a zero-cost branded scope layer over the raw JSC FFI (`src/jsc/scope.rs`: `Scope<'s>`, `Local<'s>`) that makes two boundary bug classes compile errors for code that stays on the scoped API: - a JS value escaping its host call unrooted (persisting requires the explicit `Scope::persist` -> `Strong`); - a JS-heap view (`Local::array_buffer_bytes`) held across an operation that can re-enter user JS (`&mut Scope`), e.g. a coercion that detaches the buffer. Codegen integration: - `#[bun_jsc::host_fn(scoped)]`: functions written as `fn(scope: &mut Scope, callframe) -> JsResult<Local>` get a macro-synthesized wrapper under their original name and unscoped signature, so js2native / `.classes.ts` / direct-call wiring stays byte-compatible. User `cfg`/doc/lint attributes propagate to the public wrapper and the extern shims. - `ZIG_EXPORT(tag, reenters_js | no_user_js)` effect markers on the C++ declarations; explicitly classified functions get branded wrappers generated into `bun_jsc::cpp::scoped` (`&mut Scope` / `&Scope`), unclassified and `null_is_throw` functions get none. All classified exports are verified against their C++ (`toMatch` and `putMayBeIndex` are `reenters_js`: a non-uint32 `lastIndex` goes through ToNumber, and index puts on exotic receivers reach `defineOwnProperty` traps). Migration: ~470 host functions are converted to the scoped form (behavior-preserving); the remaining escape hatches (`unscoped_global()` / `unscoped_bun_vm()` / `.unscoped()` and unscoped `#[host_fn]`s) are pinned per file by `test/internal/source-lints/scope-escapes.test.ts`. Also expresses the `make_*_with_bytes_no_copy` "pointer stays valid until the deallocator runs" contracts as ownership transfer (`typed_array_from_owned_slice` / `_from_vec` / `_from_owner`, `ForeignBytes`, `external_string_from_utf16`, `OwnedUrl`, `OwnedTextCodec`, `StoreRef::adopt`, `bun_sys::Mmap`, `EventLoop::scope`), replacing hand-paired create/destroy and leak-and-remember-to-free code paths. The static `Bun.CryptoHasher.hash` / `Bun.password.verifySync` argument detach bugs that motivated the layer were fixed independently on main (#36165) by coercing every argument first; here the same behavior is expressed through deferred `materialize` under the shared scope borrow, so reordering the view capture before a coercion is a borrow error. Both main's regression tests and the layer's suites pass. Rebase onto main (461 commits): 40 files conflicted; resolved by taking main's text and re-applying only the scope transformation. Changes that main made obsolete were dropped (TextEncoderStreamEncoder host fns, EventLoop::with_pipe_read_buffer, JSC__JSMap__size global arg, the sendHelperChild scoping). Follow-ups main's newer code required: JSValue::create_buffer_from_foreign now returns JsResult (the binding became fallible on main), ArrayBufferSink::end_from_js uses or_pending_exception (empty-jsvalue-laundering lint), TextDecoder createForStream uses struct update syntax (clippy, since the PR removes TextDecoder's Drop impl), ForeignBytes recorded in the vm-thread-door inventory, scope-escape limits regenerated, and the ratchet's regeneration mode is gated on an explicit --update flag. Second rebase (30 more commits, onto 8bc4d2a): two conflicts from the zero-fill removal (#39417). The zstd sync functions keep main's Failure enum and create_buffer_from_box behind the scoped signatures, and the latin1 TextDecoder path keeps main's uninitialized Vec but hands it to JSC through external_string_from_utf16_vec instead of the raw to_external_u16, matching the file's other two decode paths. Third rebase (6 more commits, onto 0002bf8): conflicts were all with the dead-code sweeps (#39420, #39448). Dropped the scoping of things main deleted (Bun.nanoseconds' host fn, ArrayBufferSink::to_js, the unreachable csrf error arm, three unused node:: re-exports) and kept the ownership refactor of ArrayBufferSink::end_from_js. Inventories regenerated; the jsresult-swallow one also picks up a count #39448 left stale on main. Fourth rebase (17 more commits, onto 258517a): do_publish keeps #39389's shape (topic JSString held and ensure_still_alive'd across the message conversion) under the scoped signature, and the valkey publish scoping sits after the command block #29339 added. Fifth rebase (42 more commits, onto 6948a12): OwnedTextCodec is gone with the WebKit codecs (#39485); TextDecoder keeps main's encoding_rs path and this PR's external_string_from_utf16_vec hand-offs, including on the new path. ArrayBuffer::from_owned_bytes stays deleted (no callers; its u32 cast that #39558 fixed never existed in the replacements), and the no-copy deallocator contract now states both the Err-path timing (the deallocator can run before Err, per #39558) and the cross-thread timing this PR's Send bounds rely on. Sixth rebase (18 more commits, onto 681a49b): three dead-code conflicts. The scoped js_assert_settings goes away with the native assertSettings (#38900, node:http2 validates in JS now), ParseArgumentsCfg's unused Default impl stays removed (#39585), and TimeoutObject keeps main's generated cached-accessor import next to the scoped imports. Seventh rebase (14 more commits, onto 32e8703): valkey subscribe() keeps the structure #39547 gave it (channel type check first, dial plus send_rejection() before a listener is stored, no trailing else) with the rejection and the new check spelled through the scope. Eighth rebase (36 more commits, onto 56c4e3d): the three expect matcher utils take #36912's propagating print_value; the conflict was only the line wrapping. memory_pressure.rs (new on main) is added to the scope-escape limits. Ninth rebase (17 more commits, onto 72ec6e2, which includes the #39839 build fix): FileSink::on_close combines this PR's with_mut probe with the parameterless ReadableStream::done() and is_some() guard from #39732. Tenth rebase (5 more commits, onto 4448a2e): the Windows cluster handle path keeps #39804's `?` on attach_windows_socket_payload under the scoped argument spelling. Eleventh rebase (7 more commits, onto a21f02a): pbkdf2/pbkdf2Sync take the bodies #39922 gave them (from_js also returns the callback, pbkdf2 returns undefined, length 6) under the scoped signatures. Twelfth rebase (16 more commits, onto 6fb7102): the safe vm_loop_ctx is re-applied onto #40002's Cell-based upgrade client, including inside the new clear_data's with_mut. Thirteenth rebase (15 more commits, onto d0f6486): the assert binding is scoped (5 hatches now, was 1); #39995's named-pipe live-count testing fn is scoped like its neighbours. Fourteenth rebase (12 more commits, onto e8300da): cron_remove's tail takes #40024's safe ThisPtr start_linux call under the scoped return; the rest of cron.rs's Cell/ThisPtr rewrite auto-merged. node_crypto_binding's scope-escape limit rises by the two unscoped argon2 host fns #37015 added. Fifteenth rebase (6 more commits, onto 1423031): two conflicts with the defer-comment sweep (#40051): PasswordObject's verifySync keeps this PR's deferred materialize of both arguments, and NodeHTTPResponse's on_resolve keeps the scoped call, both without the removed defer comments. Sixteenth rebase (31 more commits, onto 01008f8): the socket ref()/unref() host fns take #39856's bodies (hold the loop while connecting, apply the recorded state on open) under the scoped signatures. Seventeenth rebase (8 more commits, onto 4bb20e5): #40055 made the websocket upgrade client's loop-context plumbing safe itself (and dropped the adapter), so this PR's vm_loop_ctx change there is retired and both http_jsc files are main's. Eighteenth rebase (8 more commits, onto 3e347b3): the rootError crash hook keeps #37181's one-argument handle_root_error under the scoped signature. Nineteenth rebase (6 more commits, onto 7a5d837): #40251 removed the exception checks that follow already-checked calls and made JSString::to_slice / view, JSValue::get_zig_string and handle_ipc_message return JsResult. Eight files conflicted inside scoped bodies (BunObject, CryptoHasher, PasswordObject, ipc_host, node_util_binding, server_body, expect, ObjectURLRegistry); main's control flow is kept (the guards go, the ? is added) under the scoped spellings. The four has_exception checks left in BunObject.rs are the ones main kept (print_table / format2 swallow nested throws). Twentieth rebase (9 more commits, onto f2fe7d3): 32 files conflicted, nearly all with #40238 (bun_core::String owns its WTF ref). Main's ownership idioms replace this PR's: OwnedString / scopeguard deref wrappers and manual .deref() calls go (String drops its ref), into_js replaces transfer_to_js (Scope::transfer_string now consumes the String), JSValue::get_zig_string is gone so Local::get_zig_string becomes Local::to_js_string_view (the JSStringView guard keeps the cell alive), and to_slice_or_null collapses into to_slice. OwnedUrl is retired: main's whatwg::Parsed is the same RAII handle, so src/jsc/URL.rs and js_valkey.rs are main's again. ScopeFunctions.rs is rebuilt from main's text (the strings module is gone, names are &'static str) with the 14 host fns scoped and rustfmt applied; jest.rs and expect.rs take main's literals under this PR's wrapping. CachedStructure keeps main's assume_init_mut / drop_in_place sequence over this PR's slice-taking create_structure. UDP address getters add the ? main's create_sock_addr now needs. Scope-escape limits drop by one in BunObject, node_util_binding and server_body and by two in FormData (hatches replaced by scoped calls). Twenty-first rebase (7 more commits, onto 8335017): one import-line conflict in node_fs_binding.rs, where #38383 added the SystemErrorJsc trait import next to this PR's scoped imports. Both kept; no inventory changes. Twenty-second rebase (6 more commits, onto 0823e50): 39 files conflicted, all with #40374 (Utf8Bytes<'a> / EncodedSlice<'a>). Main's types replace the PR's spellings inside scoped bodies: Local::to_slice is now Local::to_utf8 (Utf8Bytes<'static>), ZigString::init(..).to_js and create_utf8_for_js calls become scope.string_utf8 / scope.string, and ScopedStringOrBuffer names StringOrBuffer<'static>. Main's owned_utf16_into_js supersedes this PR's external_string_from_utf16*, so src/jsc/ZigString.rs stays deleted and bun_string_jsc.rs and TextDecoder.rs are main's again. Scope-escape limits drop in filesystem_router (13 to 7), server_body (17 to 15) and Listener (11 to 9). Twenty-third rebase (9 more commits, onto adc354d): two files. FileSystemRouter::routes takes #40410's fallible JSValue::from_entries (mapped into the scope), and advanceTimersByTime keeps #40414's NaN check and main's message text under the scoped throws. The jsresult-swallow inventory is main's again (#40410 fixed the FakeTimers entry). Twenty-fourth rebase (9 more commits, onto 82123d3): six files, all with #40478 (RefPtr releases on Drop). This PR's StoreRef::adopt is retired: main's RefPtr<Store> is the same owning handle, so webcore_types.rs is main's again and store_backed_buffer_to_js moves a RefPtr<Store> into the JS object as the *_from_owner owner (the view closure reaches the bytes through Store::data_mut). The sql event-loop guard keeps this PR's safe EventLoop::scope under main's renamed ref guard; expect.rs keeps this PR's wrapping over main's RefPtr comments. The vm-thread-door inventory follows main's StoreRef-to-Store rename. Twenty-fifth rebase (23 more commits, onto 0e395c2): four files, all with #40511 (async fs calls no longer pin Buffer paths). pbkdf2 and scrypt take main's from_js_async parsers (ThreadIsolated params) under the scoped signatures, StringOrBuffer keeps main's from_js_async next to this PR's from_js_scoped / from_js_deferred, and the BlobOrStringOrBuffer::from_js_async this PR's insertion sat beside is gone with main. Import merges in node.rs and MarkdownObject.rs. The vm-thread-door inventory follows main's ThreadSafe-to-ThreadIsolated rename.
…rship contracts compile errors Rebased onto main as a single commit; the branch history (with its merge commits) is not preserved. Adds a zero-cost branded scope layer over the raw JSC FFI (`src/jsc/scope.rs`: `Scope<'s>`, `Local<'s>`) that makes two boundary bug classes compile errors for code that stays on the scoped API: - a JS value escaping its host call unrooted (persisting requires the explicit `Scope::persist` -> `Strong`); - a JS-heap view (`Local::array_buffer_bytes`) held across an operation that can re-enter user JS (`&mut Scope`), e.g. a coercion that detaches the buffer. Codegen integration: - `#[bun_jsc::host_fn(scoped)]`: functions written as `fn(scope: &mut Scope, callframe) -> JsResult<Local>` get a macro-synthesized wrapper under their original name and unscoped signature, so js2native / `.classes.ts` / direct-call wiring stays byte-compatible. User `cfg`/doc/lint attributes propagate to the public wrapper and the extern shims. - `ZIG_EXPORT(tag, reenters_js | no_user_js)` effect markers on the C++ declarations; explicitly classified functions get branded wrappers generated into `bun_jsc::cpp::scoped` (`&mut Scope` / `&Scope`), unclassified and `null_is_throw` functions get none. All classified exports are verified against their C++ (`toMatch` and `putMayBeIndex` are `reenters_js`: a non-uint32 `lastIndex` goes through ToNumber, and index puts on exotic receivers reach `defineOwnProperty` traps). Migration: ~470 host functions are converted to the scoped form (behavior-preserving); the remaining escape hatches (`unscoped_global()` / `unscoped_bun_vm()` / `.unscoped()` and unscoped `#[host_fn]`s) are pinned per file by `test/internal/source-lints/scope-escapes.test.ts`. Also expresses the `make_*_with_bytes_no_copy` "pointer stays valid until the deallocator runs" contracts as ownership transfer (`typed_array_from_owned_slice` / `_from_vec` / `_from_owner`, `ForeignBytes`, `external_string_from_utf16`, `OwnedUrl`, `OwnedTextCodec`, `StoreRef::adopt`, `bun_sys::Mmap`, `EventLoop::scope`), replacing hand-paired create/destroy and leak-and-remember-to-free code paths. The static `Bun.CryptoHasher.hash` / `Bun.password.verifySync` argument detach bugs that motivated the layer were fixed independently on main (#36165) by coercing every argument first; here the same behavior is expressed through deferred `materialize` under the shared scope borrow, so reordering the view capture before a coercion is a borrow error. Both main's regression tests and the layer's suites pass. Rebase onto main (461 commits): 40 files conflicted; resolved by taking main's text and re-applying only the scope transformation. Changes that main made obsolete were dropped (TextEncoderStreamEncoder host fns, EventLoop::with_pipe_read_buffer, JSC__JSMap__size global arg, the sendHelperChild scoping). Follow-ups main's newer code required: JSValue::create_buffer_from_foreign now returns JsResult (the binding became fallible on main), ArrayBufferSink::end_from_js uses or_pending_exception (empty-jsvalue-laundering lint), TextDecoder createForStream uses struct update syntax (clippy, since the PR removes TextDecoder's Drop impl), ForeignBytes recorded in the vm-thread-door inventory, scope-escape limits regenerated, and the ratchet's regeneration mode is gated on an explicit --update flag. Second rebase (30 more commits, onto 8bc4d2a): two conflicts from the zero-fill removal (#39417). The zstd sync functions keep main's Failure enum and create_buffer_from_box behind the scoped signatures, and the latin1 TextDecoder path keeps main's uninitialized Vec but hands it to JSC through external_string_from_utf16_vec instead of the raw to_external_u16, matching the file's other two decode paths. Third rebase (6 more commits, onto 0002bf8): conflicts were all with the dead-code sweeps (#39420, #39448). Dropped the scoping of things main deleted (Bun.nanoseconds' host fn, ArrayBufferSink::to_js, the unreachable csrf error arm, three unused node:: re-exports) and kept the ownership refactor of ArrayBufferSink::end_from_js. Inventories regenerated; the jsresult-swallow one also picks up a count #39448 left stale on main. Fourth rebase (17 more commits, onto 258517a): do_publish keeps #39389's shape (topic JSString held and ensure_still_alive'd across the message conversion) under the scoped signature, and the valkey publish scoping sits after the command block #29339 added. Fifth rebase (42 more commits, onto 6948a12): OwnedTextCodec is gone with the WebKit codecs (#39485); TextDecoder keeps main's encoding_rs path and this PR's external_string_from_utf16_vec hand-offs, including on the new path. ArrayBuffer::from_owned_bytes stays deleted (no callers; its u32 cast that #39558 fixed never existed in the replacements), and the no-copy deallocator contract now states both the Err-path timing (the deallocator can run before Err, per #39558) and the cross-thread timing this PR's Send bounds rely on. Sixth rebase (18 more commits, onto 681a49b): three dead-code conflicts. The scoped js_assert_settings goes away with the native assertSettings (#38900, node:http2 validates in JS now), ParseArgumentsCfg's unused Default impl stays removed (#39585), and TimeoutObject keeps main's generated cached-accessor import next to the scoped imports. Seventh rebase (14 more commits, onto 32e8703): valkey subscribe() keeps the structure #39547 gave it (channel type check first, dial plus send_rejection() before a listener is stored, no trailing else) with the rejection and the new check spelled through the scope. Eighth rebase (36 more commits, onto 56c4e3d): the three expect matcher utils take #36912's propagating print_value; the conflict was only the line wrapping. memory_pressure.rs (new on main) is added to the scope-escape limits. Ninth rebase (17 more commits, onto 72ec6e2, which includes the #39839 build fix): FileSink::on_close combines this PR's with_mut probe with the parameterless ReadableStream::done() and is_some() guard from #39732. Tenth rebase (5 more commits, onto 4448a2e): the Windows cluster handle path keeps #39804's `?` on attach_windows_socket_payload under the scoped argument spelling. Eleventh rebase (7 more commits, onto a21f02a): pbkdf2/pbkdf2Sync take the bodies #39922 gave them (from_js also returns the callback, pbkdf2 returns undefined, length 6) under the scoped signatures. Twelfth rebase (16 more commits, onto 6fb7102): the safe vm_loop_ctx is re-applied onto #40002's Cell-based upgrade client, including inside the new clear_data's with_mut. Thirteenth rebase (15 more commits, onto d0f6486): the assert binding is scoped (5 hatches now, was 1); #39995's named-pipe live-count testing fn is scoped like its neighbours. Fourteenth rebase (12 more commits, onto e8300da): cron_remove's tail takes #40024's safe ThisPtr start_linux call under the scoped return; the rest of cron.rs's Cell/ThisPtr rewrite auto-merged. node_crypto_binding's scope-escape limit rises by the two unscoped argon2 host fns #37015 added. Fifteenth rebase (6 more commits, onto 1423031): two conflicts with the defer-comment sweep (#40051): PasswordObject's verifySync keeps this PR's deferred materialize of both arguments, and NodeHTTPResponse's on_resolve keeps the scoped call, both without the removed defer comments. Sixteenth rebase (31 more commits, onto 01008f8): the socket ref()/unref() host fns take #39856's bodies (hold the loop while connecting, apply the recorded state on open) under the scoped signatures. Seventeenth rebase (8 more commits, onto 4bb20e5): #40055 made the websocket upgrade client's loop-context plumbing safe itself (and dropped the adapter), so this PR's vm_loop_ctx change there is retired and both http_jsc files are main's. Eighteenth rebase (8 more commits, onto 3e347b3): the rootError crash hook keeps #37181's one-argument handle_root_error under the scoped signature. Nineteenth rebase (6 more commits, onto 7a5d837): #40251 removed the exception checks that follow already-checked calls and made JSString::to_slice / view, JSValue::get_zig_string and handle_ipc_message return JsResult. Eight files conflicted inside scoped bodies (BunObject, CryptoHasher, PasswordObject, ipc_host, node_util_binding, server_body, expect, ObjectURLRegistry); main's control flow is kept (the guards go, the ? is added) under the scoped spellings. The four has_exception checks left in BunObject.rs are the ones main kept (print_table / format2 swallow nested throws). Twentieth rebase (9 more commits, onto f2fe7d3): 32 files conflicted, nearly all with #40238 (bun_core::String owns its WTF ref). Main's ownership idioms replace this PR's: OwnedString / scopeguard deref wrappers and manual .deref() calls go (String drops its ref), into_js replaces transfer_to_js (Scope::transfer_string now consumes the String), JSValue::get_zig_string is gone so Local::get_zig_string becomes Local::to_js_string_view (the JSStringView guard keeps the cell alive), and to_slice_or_null collapses into to_slice. OwnedUrl is retired: main's whatwg::Parsed is the same RAII handle, so src/jsc/URL.rs and js_valkey.rs are main's again. ScopeFunctions.rs is rebuilt from main's text (the strings module is gone, names are &'static str) with the 14 host fns scoped and rustfmt applied; jest.rs and expect.rs take main's literals under this PR's wrapping. CachedStructure keeps main's assume_init_mut / drop_in_place sequence over this PR's slice-taking create_structure. UDP address getters add the ? main's create_sock_addr now needs. Scope-escape limits drop by one in BunObject, node_util_binding and server_body and by two in FormData (hatches replaced by scoped calls). Twenty-first rebase (7 more commits, onto 8335017): one import-line conflict in node_fs_binding.rs, where #38383 added the SystemErrorJsc trait import next to this PR's scoped imports. Both kept; no inventory changes. Twenty-second rebase (6 more commits, onto 0823e50): 39 files conflicted, all with #40374 (Utf8Bytes<'a> / EncodedSlice<'a>). Main's types replace the PR's spellings inside scoped bodies: Local::to_slice is now Local::to_utf8 (Utf8Bytes<'static>), ZigString::init(..).to_js and create_utf8_for_js calls become scope.string_utf8 / scope.string, and ScopedStringOrBuffer names StringOrBuffer<'static>. Main's owned_utf16_into_js supersedes this PR's external_string_from_utf16*, so src/jsc/ZigString.rs stays deleted and bun_string_jsc.rs and TextDecoder.rs are main's again. Scope-escape limits drop in filesystem_router (13 to 7), server_body (17 to 15) and Listener (11 to 9). Twenty-third rebase (9 more commits, onto adc354d): two files. FileSystemRouter::routes takes #40410's fallible JSValue::from_entries (mapped into the scope), and advanceTimersByTime keeps #40414's NaN check and main's message text under the scoped throws. The jsresult-swallow inventory is main's again (#40410 fixed the FakeTimers entry). Twenty-fourth rebase (9 more commits, onto 82123d3): six files, all with #40478 (RefPtr releases on Drop). This PR's StoreRef::adopt is retired: main's RefPtr<Store> is the same owning handle, so webcore_types.rs is main's again and store_backed_buffer_to_js moves a RefPtr<Store> into the JS object as the *_from_owner owner (the view closure reaches the bytes through Store::data_mut). The sql event-loop guard keeps this PR's safe EventLoop::scope under main's renamed ref guard; expect.rs keeps this PR's wrapping over main's RefPtr comments. The vm-thread-door inventory follows main's StoreRef-to-Store rename. Twenty-fifth rebase (23 more commits, onto 0e395c2): four files, all with #40511 (async fs calls no longer pin Buffer paths). pbkdf2 and scrypt take main's from_js_async parsers (ThreadIsolated params) under the scoped signatures, StringOrBuffer keeps main's from_js_async next to this PR's from_js_scoped / from_js_deferred, and the BlobOrStringOrBuffer::from_js_async this PR's insertion sat beside is gone with main. Import merges in node.rs and MarkdownObject.rs. The vm-thread-door inventory follows main's ThreadSafe-to-ThreadIsolated rename. Twenty-sixth rebase (8 more commits, onto 72ffcd8): one import-line conflict in ffi_body.rs, where #40592 added ErrorCode next to this PR's scoped imports. Both kept; no inventory changes.
…rship contracts compile errors Rebased onto main as a single commit; the branch history (with its merge commits) is not preserved. Adds a zero-cost branded scope layer over the raw JSC FFI (`src/jsc/scope.rs`: `Scope<'s>`, `Local<'s>`) that makes two boundary bug classes compile errors for code that stays on the scoped API: - a JS value escaping its host call unrooted (persisting requires the explicit `Scope::persist` -> `Strong`); - a JS-heap view (`Local::array_buffer_bytes`) held across an operation that can re-enter user JS (`&mut Scope`), e.g. a coercion that detaches the buffer. Codegen integration: - `#[bun_jsc::host_fn(scoped)]`: functions written as `fn(scope: &mut Scope, callframe) -> JsResult<Local>` get a macro-synthesized wrapper under their original name and unscoped signature, so js2native / `.classes.ts` / direct-call wiring stays byte-compatible. User `cfg`/doc/lint attributes propagate to the public wrapper and the extern shims. - `ZIG_EXPORT(tag, reenters_js | no_user_js)` effect markers on the C++ declarations; explicitly classified functions get branded wrappers generated into `bun_jsc::cpp::scoped` (`&mut Scope` / `&Scope`), unclassified and `null_is_throw` functions get none. All classified exports are verified against their C++ (`toMatch` and `putMayBeIndex` are `reenters_js`: a non-uint32 `lastIndex` goes through ToNumber, and index puts on exotic receivers reach `defineOwnProperty` traps). Migration: ~470 host functions are converted to the scoped form (behavior-preserving); the remaining escape hatches (`unscoped_global()` / `unscoped_bun_vm()` / `.unscoped()` and unscoped `#[host_fn]`s) are pinned per file by `test/internal/source-lints/scope-escapes.test.ts`. Also expresses the `make_*_with_bytes_no_copy` "pointer stays valid until the deallocator runs" contracts as ownership transfer (`typed_array_from_owned_slice` / `_from_vec` / `_from_owner`, `ForeignBytes`, `external_string_from_utf16`, `OwnedUrl`, `OwnedTextCodec`, `StoreRef::adopt`, `bun_sys::Mmap`, `EventLoop::scope`), replacing hand-paired create/destroy and leak-and-remember-to-free code paths. The static `Bun.CryptoHasher.hash` / `Bun.password.verifySync` argument detach bugs that motivated the layer were fixed independently on main (#36165) by coercing every argument first; here the same behavior is expressed through deferred `materialize` under the shared scope borrow, so reordering the view capture before a coercion is a borrow error. Both main's regression tests and the layer's suites pass. Rebase onto main (461 commits): 40 files conflicted; resolved by taking main's text and re-applying only the scope transformation. Changes that main made obsolete were dropped (TextEncoderStreamEncoder host fns, EventLoop::with_pipe_read_buffer, JSC__JSMap__size global arg, the sendHelperChild scoping). Follow-ups main's newer code required: JSValue::create_buffer_from_foreign now returns JsResult (the binding became fallible on main), ArrayBufferSink::end_from_js uses or_pending_exception (empty-jsvalue-laundering lint), TextDecoder createForStream uses struct update syntax (clippy, since the PR removes TextDecoder's Drop impl), ForeignBytes recorded in the vm-thread-door inventory, scope-escape limits regenerated, and the ratchet's regeneration mode is gated on an explicit --update flag. Second rebase (30 more commits, onto 8bc4d2a): two conflicts from the zero-fill removal (#39417). The zstd sync functions keep main's Failure enum and create_buffer_from_box behind the scoped signatures, and the latin1 TextDecoder path keeps main's uninitialized Vec but hands it to JSC through external_string_from_utf16_vec instead of the raw to_external_u16, matching the file's other two decode paths. Third rebase (6 more commits, onto 0002bf8): conflicts were all with the dead-code sweeps (#39420, #39448). Dropped the scoping of things main deleted (Bun.nanoseconds' host fn, ArrayBufferSink::to_js, the unreachable csrf error arm, three unused node:: re-exports) and kept the ownership refactor of ArrayBufferSink::end_from_js. Inventories regenerated; the jsresult-swallow one also picks up a count #39448 left stale on main. Fourth rebase (17 more commits, onto 258517a): do_publish keeps #39389's shape (topic JSString held and ensure_still_alive'd across the message conversion) under the scoped signature, and the valkey publish scoping sits after the command block #29339 added. Fifth rebase (42 more commits, onto 6948a12): OwnedTextCodec is gone with the WebKit codecs (#39485); TextDecoder keeps main's encoding_rs path and this PR's external_string_from_utf16_vec hand-offs, including on the new path. ArrayBuffer::from_owned_bytes stays deleted (no callers; its u32 cast that #39558 fixed never existed in the replacements), and the no-copy deallocator contract now states both the Err-path timing (the deallocator can run before Err, per #39558) and the cross-thread timing this PR's Send bounds rely on. Sixth rebase (18 more commits, onto 681a49b): three dead-code conflicts. The scoped js_assert_settings goes away with the native assertSettings (#38900, node:http2 validates in JS now), ParseArgumentsCfg's unused Default impl stays removed (#39585), and TimeoutObject keeps main's generated cached-accessor import next to the scoped imports. Seventh rebase (14 more commits, onto 32e8703): valkey subscribe() keeps the structure #39547 gave it (channel type check first, dial plus send_rejection() before a listener is stored, no trailing else) with the rejection and the new check spelled through the scope. Eighth rebase (36 more commits, onto 56c4e3d): the three expect matcher utils take #36912's propagating print_value; the conflict was only the line wrapping. memory_pressure.rs (new on main) is added to the scope-escape limits. Ninth rebase (17 more commits, onto 72ec6e2, which includes the #39839 build fix): FileSink::on_close combines this PR's with_mut probe with the parameterless ReadableStream::done() and is_some() guard from #39732. Tenth rebase (5 more commits, onto 4448a2e): the Windows cluster handle path keeps #39804's `?` on attach_windows_socket_payload under the scoped argument spelling. Eleventh rebase (7 more commits, onto a21f02a): pbkdf2/pbkdf2Sync take the bodies #39922 gave them (from_js also returns the callback, pbkdf2 returns undefined, length 6) under the scoped signatures. Twelfth rebase (16 more commits, onto 6fb7102): the safe vm_loop_ctx is re-applied onto #40002's Cell-based upgrade client, including inside the new clear_data's with_mut. Thirteenth rebase (15 more commits, onto d0f6486): the assert binding is scoped (5 hatches now, was 1); #39995's named-pipe live-count testing fn is scoped like its neighbours. Fourteenth rebase (12 more commits, onto e8300da): cron_remove's tail takes #40024's safe ThisPtr start_linux call under the scoped return; the rest of cron.rs's Cell/ThisPtr rewrite auto-merged. node_crypto_binding's scope-escape limit rises by the two unscoped argon2 host fns #37015 added. Fifteenth rebase (6 more commits, onto 1423031): two conflicts with the defer-comment sweep (#40051): PasswordObject's verifySync keeps this PR's deferred materialize of both arguments, and NodeHTTPResponse's on_resolve keeps the scoped call, both without the removed defer comments. Sixteenth rebase (31 more commits, onto 01008f8): the socket ref()/unref() host fns take #39856's bodies (hold the loop while connecting, apply the recorded state on open) under the scoped signatures. Seventeenth rebase (8 more commits, onto 4bb20e5): #40055 made the websocket upgrade client's loop-context plumbing safe itself (and dropped the adapter), so this PR's vm_loop_ctx change there is retired and both http_jsc files are main's. Eighteenth rebase (8 more commits, onto 3e347b3): the rootError crash hook keeps #37181's one-argument handle_root_error under the scoped signature. Nineteenth rebase (6 more commits, onto 7a5d837): #40251 removed the exception checks that follow already-checked calls and made JSString::to_slice / view, JSValue::get_zig_string and handle_ipc_message return JsResult. Eight files conflicted inside scoped bodies (BunObject, CryptoHasher, PasswordObject, ipc_host, node_util_binding, server_body, expect, ObjectURLRegistry); main's control flow is kept (the guards go, the ? is added) under the scoped spellings. The four has_exception checks left in BunObject.rs are the ones main kept (print_table / format2 swallow nested throws). Twentieth rebase (9 more commits, onto f2fe7d3): 32 files conflicted, nearly all with #40238 (bun_core::String owns its WTF ref). Main's ownership idioms replace this PR's: OwnedString / scopeguard deref wrappers and manual .deref() calls go (String drops its ref), into_js replaces transfer_to_js (Scope::transfer_string now consumes the String), JSValue::get_zig_string is gone so Local::get_zig_string becomes Local::to_js_string_view (the JSStringView guard keeps the cell alive), and to_slice_or_null collapses into to_slice. OwnedUrl is retired: main's whatwg::Parsed is the same RAII handle, so src/jsc/URL.rs and js_valkey.rs are main's again. ScopeFunctions.rs is rebuilt from main's text (the strings module is gone, names are &'static str) with the 14 host fns scoped and rustfmt applied; jest.rs and expect.rs take main's literals under this PR's wrapping. CachedStructure keeps main's assume_init_mut / drop_in_place sequence over this PR's slice-taking create_structure. UDP address getters add the ? main's create_sock_addr now needs. Scope-escape limits drop by one in BunObject, node_util_binding and server_body and by two in FormData (hatches replaced by scoped calls). Twenty-first rebase (7 more commits, onto 8335017): one import-line conflict in node_fs_binding.rs, where #38383 added the SystemErrorJsc trait import next to this PR's scoped imports. Both kept; no inventory changes. Twenty-second rebase (6 more commits, onto 0823e50): 39 files conflicted, all with #40374 (Utf8Bytes<'a> / EncodedSlice<'a>). Main's types replace the PR's spellings inside scoped bodies: Local::to_slice is now Local::to_utf8 (Utf8Bytes<'static>), ZigString::init(..).to_js and create_utf8_for_js calls become scope.string_utf8 / scope.string, and ScopedStringOrBuffer names StringOrBuffer<'static>. Main's owned_utf16_into_js supersedes this PR's external_string_from_utf16*, so src/jsc/ZigString.rs stays deleted and bun_string_jsc.rs and TextDecoder.rs are main's again. Scope-escape limits drop in filesystem_router (13 to 7), server_body (17 to 15) and Listener (11 to 9). Twenty-third rebase (9 more commits, onto adc354d): two files. FileSystemRouter::routes takes #40410's fallible JSValue::from_entries (mapped into the scope), and advanceTimersByTime keeps #40414's NaN check and main's message text under the scoped throws. The jsresult-swallow inventory is main's again (#40410 fixed the FakeTimers entry). Twenty-fourth rebase (9 more commits, onto 82123d3): six files, all with #40478 (RefPtr releases on Drop). This PR's StoreRef::adopt is retired: main's RefPtr<Store> is the same owning handle, so webcore_types.rs is main's again and store_backed_buffer_to_js moves a RefPtr<Store> into the JS object as the *_from_owner owner (the view closure reaches the bytes through Store::data_mut). The sql event-loop guard keeps this PR's safe EventLoop::scope under main's renamed ref guard; expect.rs keeps this PR's wrapping over main's RefPtr comments. The vm-thread-door inventory follows main's StoreRef-to-Store rename. Twenty-fifth rebase (23 more commits, onto 0e395c2): four files, all with #40511 (async fs calls no longer pin Buffer paths). pbkdf2 and scrypt take main's from_js_async parsers (ThreadIsolated params) under the scoped signatures, StringOrBuffer keeps main's from_js_async next to this PR's from_js_scoped / from_js_deferred, and the BlobOrStringOrBuffer::from_js_async this PR's insertion sat beside is gone with main. Import merges in node.rs and MarkdownObject.rs. The vm-thread-door inventory follows main's ThreadSafe-to-ThreadIsolated rename. Twenty-sixth rebase (8 more commits, onto 72ffcd8): one import-line conflict in ffi_body.rs, where #40592 added ErrorCode next to this PR's scoped imports. Both kept; no inventory changes. Twenty-seventh rebase (24 more commits, onto 49ff888): five files, all with #40516 (refcounted types own their teardown). The serve-plugins .then callbacks adopt their ref through main's RefPtr::from_raw under the scoped argument spellings (this PR's ServePluginsRef guard is gone with main's newtypes), FileSink keeps this PR's with_mut spelling over main's RefPtr<FileSink> construction (create is main's one-liner), the StatWatcher deinit hook stays deleted next to the scoped do_ref, and ipc_host.rs / socket_body.rs are import and return-spelling merges. No inventory changes.
…rship contracts compile errors Rebased onto main as a single commit; the branch history (with its merge commits) is not preserved. Adds a zero-cost branded scope layer over the raw JSC FFI (`src/jsc/scope.rs`: `Scope<'s>`, `Local<'s>`) that makes two boundary bug classes compile errors for code that stays on the scoped API: - a JS value escaping its host call unrooted (persisting requires the explicit `Scope::persist` -> `Strong`); - a JS-heap view (`Local::array_buffer_bytes`) held across an operation that can re-enter user JS (`&mut Scope`), e.g. a coercion that detaches the buffer. Codegen integration: - `#[bun_jsc::host_fn(scoped)]`: functions written as `fn(scope: &mut Scope, callframe) -> JsResult<Local>` get a macro-synthesized wrapper under their original name and unscoped signature, so js2native / `.classes.ts` / direct-call wiring stays byte-compatible. User `cfg`/doc/lint attributes propagate to the public wrapper and the extern shims. - `ZIG_EXPORT(tag, reenters_js | no_user_js)` effect markers on the C++ declarations; explicitly classified functions get branded wrappers generated into `bun_jsc::cpp::scoped` (`&mut Scope` / `&Scope`), unclassified and `null_is_throw` functions get none. All classified exports are verified against their C++ (`toMatch` and `putMayBeIndex` are `reenters_js`: a non-uint32 `lastIndex` goes through ToNumber, and index puts on exotic receivers reach `defineOwnProperty` traps). Migration: ~470 host functions are converted to the scoped form (behavior-preserving); the remaining escape hatches (`unscoped_global()` / `unscoped_bun_vm()` / `.unscoped()` and unscoped `#[host_fn]`s) are pinned per file by `test/internal/source-lints/scope-escapes.test.ts`. Also expresses the `make_*_with_bytes_no_copy` "pointer stays valid until the deallocator runs" contracts as ownership transfer (`typed_array_from_owned_slice` / `_from_vec` / `_from_owner`, `ForeignBytes`, `external_string_from_utf16`, `OwnedUrl`, `OwnedTextCodec`, `StoreRef::adopt`, `bun_sys::Mmap`, `EventLoop::scope`), replacing hand-paired create/destroy and leak-and-remember-to-free code paths. The static `Bun.CryptoHasher.hash` / `Bun.password.verifySync` argument detach bugs that motivated the layer were fixed independently on main (#36165) by coercing every argument first; here the same behavior is expressed through deferred `materialize` under the shared scope borrow, so reordering the view capture before a coercion is a borrow error. Both main's regression tests and the layer's suites pass. Rebase onto main (461 commits): 40 files conflicted; resolved by taking main's text and re-applying only the scope transformation. Changes that main made obsolete were dropped (TextEncoderStreamEncoder host fns, EventLoop::with_pipe_read_buffer, JSC__JSMap__size global arg, the sendHelperChild scoping). Follow-ups main's newer code required: JSValue::create_buffer_from_foreign now returns JsResult (the binding became fallible on main), ArrayBufferSink::end_from_js uses or_pending_exception (empty-jsvalue-laundering lint), TextDecoder createForStream uses struct update syntax (clippy, since the PR removes TextDecoder's Drop impl), ForeignBytes recorded in the vm-thread-door inventory, scope-escape limits regenerated, and the ratchet's regeneration mode is gated on an explicit --update flag. Second rebase (30 more commits, onto 8bc4d2a): two conflicts from the zero-fill removal (#39417). The zstd sync functions keep main's Failure enum and create_buffer_from_box behind the scoped signatures, and the latin1 TextDecoder path keeps main's uninitialized Vec but hands it to JSC through external_string_from_utf16_vec instead of the raw to_external_u16, matching the file's other two decode paths. Third rebase (6 more commits, onto 0002bf8): conflicts were all with the dead-code sweeps (#39420, #39448). Dropped the scoping of things main deleted (Bun.nanoseconds' host fn, ArrayBufferSink::to_js, the unreachable csrf error arm, three unused node:: re-exports) and kept the ownership refactor of ArrayBufferSink::end_from_js. Inventories regenerated; the jsresult-swallow one also picks up a count #39448 left stale on main. Fourth rebase (17 more commits, onto 258517a): do_publish keeps #39389's shape (topic JSString held and ensure_still_alive'd across the message conversion) under the scoped signature, and the valkey publish scoping sits after the command block #29339 added. Fifth rebase (42 more commits, onto 6948a12): OwnedTextCodec is gone with the WebKit codecs (#39485); TextDecoder keeps main's encoding_rs path and this PR's external_string_from_utf16_vec hand-offs, including on the new path. ArrayBuffer::from_owned_bytes stays deleted (no callers; its u32 cast that #39558 fixed never existed in the replacements), and the no-copy deallocator contract now states both the Err-path timing (the deallocator can run before Err, per #39558) and the cross-thread timing this PR's Send bounds rely on. Sixth rebase (18 more commits, onto 681a49b): three dead-code conflicts. The scoped js_assert_settings goes away with the native assertSettings (#38900, node:http2 validates in JS now), ParseArgumentsCfg's unused Default impl stays removed (#39585), and TimeoutObject keeps main's generated cached-accessor import next to the scoped imports. Seventh rebase (14 more commits, onto 32e8703): valkey subscribe() keeps the structure #39547 gave it (channel type check first, dial plus send_rejection() before a listener is stored, no trailing else) with the rejection and the new check spelled through the scope. Eighth rebase (36 more commits, onto 56c4e3d): the three expect matcher utils take #36912's propagating print_value; the conflict was only the line wrapping. memory_pressure.rs (new on main) is added to the scope-escape limits. Ninth rebase (17 more commits, onto 72ec6e2, which includes the #39839 build fix): FileSink::on_close combines this PR's with_mut probe with the parameterless ReadableStream::done() and is_some() guard from #39732. Tenth rebase (5 more commits, onto 4448a2e): the Windows cluster handle path keeps #39804's `?` on attach_windows_socket_payload under the scoped argument spelling. Eleventh rebase (7 more commits, onto a21f02a): pbkdf2/pbkdf2Sync take the bodies #39922 gave them (from_js also returns the callback, pbkdf2 returns undefined, length 6) under the scoped signatures. Twelfth rebase (16 more commits, onto 6fb7102): the safe vm_loop_ctx is re-applied onto #40002's Cell-based upgrade client, including inside the new clear_data's with_mut. Thirteenth rebase (15 more commits, onto d0f6486): the assert binding is scoped (5 hatches now, was 1); #39995's named-pipe live-count testing fn is scoped like its neighbours. Fourteenth rebase (12 more commits, onto e8300da): cron_remove's tail takes #40024's safe ThisPtr start_linux call under the scoped return; the rest of cron.rs's Cell/ThisPtr rewrite auto-merged. node_crypto_binding's scope-escape limit rises by the two unscoped argon2 host fns #37015 added. Fifteenth rebase (6 more commits, onto 1423031): two conflicts with the defer-comment sweep (#40051): PasswordObject's verifySync keeps this PR's deferred materialize of both arguments, and NodeHTTPResponse's on_resolve keeps the scoped call, both without the removed defer comments. Sixteenth rebase (31 more commits, onto 01008f8): the socket ref()/unref() host fns take #39856's bodies (hold the loop while connecting, apply the recorded state on open) under the scoped signatures. Seventeenth rebase (8 more commits, onto 4bb20e5): #40055 made the websocket upgrade client's loop-context plumbing safe itself (and dropped the adapter), so this PR's vm_loop_ctx change there is retired and both http_jsc files are main's. Eighteenth rebase (8 more commits, onto 3e347b3): the rootError crash hook keeps #37181's one-argument handle_root_error under the scoped signature. Nineteenth rebase (6 more commits, onto 7a5d837): #40251 removed the exception checks that follow already-checked calls and made JSString::to_slice / view, JSValue::get_zig_string and handle_ipc_message return JsResult. Eight files conflicted inside scoped bodies (BunObject, CryptoHasher, PasswordObject, ipc_host, node_util_binding, server_body, expect, ObjectURLRegistry); main's control flow is kept (the guards go, the ? is added) under the scoped spellings. The four has_exception checks left in BunObject.rs are the ones main kept (print_table / format2 swallow nested throws). Twentieth rebase (9 more commits, onto f2fe7d3): 32 files conflicted, nearly all with #40238 (bun_core::String owns its WTF ref). Main's ownership idioms replace this PR's: OwnedString / scopeguard deref wrappers and manual .deref() calls go (String drops its ref), into_js replaces transfer_to_js (Scope::transfer_string now consumes the String), JSValue::get_zig_string is gone so Local::get_zig_string becomes Local::to_js_string_view (the JSStringView guard keeps the cell alive), and to_slice_or_null collapses into to_slice. OwnedUrl is retired: main's whatwg::Parsed is the same RAII handle, so src/jsc/URL.rs and js_valkey.rs are main's again. ScopeFunctions.rs is rebuilt from main's text (the strings module is gone, names are &'static str) with the 14 host fns scoped and rustfmt applied; jest.rs and expect.rs take main's literals under this PR's wrapping. CachedStructure keeps main's assume_init_mut / drop_in_place sequence over this PR's slice-taking create_structure. UDP address getters add the ? main's create_sock_addr now needs. Scope-escape limits drop by one in BunObject, node_util_binding and server_body and by two in FormData (hatches replaced by scoped calls). Twenty-first rebase (7 more commits, onto 8335017): one import-line conflict in node_fs_binding.rs, where #38383 added the SystemErrorJsc trait import next to this PR's scoped imports. Both kept; no inventory changes. Twenty-second rebase (6 more commits, onto 0823e50): 39 files conflicted, all with #40374 (Utf8Bytes<'a> / EncodedSlice<'a>). Main's types replace the PR's spellings inside scoped bodies: Local::to_slice is now Local::to_utf8 (Utf8Bytes<'static>), ZigString::init(..).to_js and create_utf8_for_js calls become scope.string_utf8 / scope.string, and ScopedStringOrBuffer names StringOrBuffer<'static>. Main's owned_utf16_into_js supersedes this PR's external_string_from_utf16*, so src/jsc/ZigString.rs stays deleted and bun_string_jsc.rs and TextDecoder.rs are main's again. Scope-escape limits drop in filesystem_router (13 to 7), server_body (17 to 15) and Listener (11 to 9). Twenty-third rebase (9 more commits, onto adc354d): two files. FileSystemRouter::routes takes #40410's fallible JSValue::from_entries (mapped into the scope), and advanceTimersByTime keeps #40414's NaN check and main's message text under the scoped throws. The jsresult-swallow inventory is main's again (#40410 fixed the FakeTimers entry). Twenty-fourth rebase (9 more commits, onto 82123d3): six files, all with #40478 (RefPtr releases on Drop). This PR's StoreRef::adopt is retired: main's RefPtr<Store> is the same owning handle, so webcore_types.rs is main's again and store_backed_buffer_to_js moves a RefPtr<Store> into the JS object as the *_from_owner owner (the view closure reaches the bytes through Store::data_mut). The sql event-loop guard keeps this PR's safe EventLoop::scope under main's renamed ref guard; expect.rs keeps this PR's wrapping over main's RefPtr comments. The vm-thread-door inventory follows main's StoreRef-to-Store rename. Twenty-fifth rebase (23 more commits, onto 0e395c2): four files, all with #40511 (async fs calls no longer pin Buffer paths). pbkdf2 and scrypt take main's from_js_async parsers (ThreadIsolated params) under the scoped signatures, StringOrBuffer keeps main's from_js_async next to this PR's from_js_scoped / from_js_deferred, and the BlobOrStringOrBuffer::from_js_async this PR's insertion sat beside is gone with main. Import merges in node.rs and MarkdownObject.rs. The vm-thread-door inventory follows main's ThreadSafe-to-ThreadIsolated rename. Twenty-sixth rebase (8 more commits, onto 72ffcd8): one import-line conflict in ffi_body.rs, where #40592 added ErrorCode next to this PR's scoped imports. Both kept; no inventory changes. Twenty-seventh rebase (24 more commits, onto 49ff888): five files, all with #40516 (refcounted types own their teardown). The serve-plugins .then callbacks adopt their ref through main's RefPtr::from_raw under the scoped argument spellings (this PR's ServePluginsRef guard is gone with main's newtypes), FileSink keeps this PR's with_mut spelling over main's RefPtr<FileSink> construction (create is main's one-liner), the StatWatcher deinit hook stays deleted next to the scoped do_ref, and ipc_host.rs / socket_body.rs are import and return-spelling merges. No inventory changes. Twenty-eighth rebase (36 more commits, onto 69c6138): one import-line conflict in csrf_jsc.rs, where #40697 added IntegerRange next to this PR's scoped imports. Both kept; no inventory changes.
* chore(bun): bump to 1.4.1 (jsonb leak fixed upstream) Bun 1.4.1 ships the fix for the bun:sql jsonb native memory leak that forced the revert to 1.3.14 in v3.3.6 (oven-sh/bun#40102, fixed by oven-sh/bun#40238). Verified against a real Postgres with 300 jsonb-bound queries of ~780 KB: 1.4.0 grows 33 -> 128 MB and keeps climbing, 1.4.1 stays flat. DO NOT MERGE yet: 1.4.1 has a bundler regression that makes the compiled server fail to boot with SyntaxError: Cannot declare a var variable that shadows a let/const/class variable: 'Check2'. The bundler emits `var Check2 = Check2` beside a `let Check2` in the same scope. Waiting for 1.4.2. * fix(api): minify identifiers and add an inline sourcemap Minifying identifiers shrinks the compiled server and sidesteps a Bun 1.4.1 renamer bug that emits invalid JS for Elysia, so the binary failed to start with SyntaxError: Cannot declare a var variable that shadows a let/const/class variable: 'Check2'. Upstream confirmed this approach in oven-sh/bun#41351. Minified names alone would leave stack traces hard to read, so add an inline sourcemap. Traces keep the real source file and line, which is more useful than the bundled offsets they carried before. The sourcemap costs what the minification saves, so the binary stays at 63.5 MB. Verified with 16 integration and 14 auth integration tests on Bun 1.4.1. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
What does this PR do?
bun_core::String(the Rust mirror of C++BunString) was aCopyPOD with no destructor, so every +1 producer —clone_utf8,create_format,to_bun_string, URL accessors, C++toStringRefout-params — relied on the caller remembering.deref()orOwnedString. An audit found ~60 places that didn't (per-importResolvedSource.source_url/specifier, whole HMR bundles in bake, Postgres error fields, valkey/UDP/S3 option strings,parseArgsargv, QUIC headers, …). This moves the +1/-1 rule into the type so those, and any not yet found, are released byDrop.Ownership is the type now
FFI signatures carry ownership.
Stringhas the same layout as C++BunString, so a by-valueStringin an extern signature means the ref crosses (likeBox<T>in FFI);&String/StringView<'_>mean borrow. C++ that only reads takesconst BunString*.Borrows are
&String/StringView<'a>(a by-value borrow tied to whatever owns the characters: property-iterator names,substring/trunc,JsCell<String>::get), and consuming into JS isinto_js(self)(wastransfer_to_js(&mut self)):Reading a JS string's characters is a scoped borrow.
JSValue::get_zig_string()returned aZigStringaliasing a JSString's buffer with no lifetime, no ref, and nothing keeping the cell alive. It is removed; callers either take an ownedto_bun_string(), or:Use it for a scoped read; use
to_bun_string()when the string must be kept.JSString::view()returns the sameJSStringViewguard;JSPropertyIterator::next()yields(StringView<'_>, JSValue)tied to the iterator.get_class_name/get_name_propertyreturn an ownedString(they handed out views into C++ temporaries).ResolvedSourceis owned on both sides. RustResolvedSource/Errorable<T>own their strings, bytecode and module_info; C++ErrorableResolvedSourcegets a destructor, andZig::SourceProvider::createtakes what it keeps viatransferToWTFString(). The unusedspecifierfield is removed. Previously only~SourceProviderreleasedspecifier/source_url, so every loader arm that doesn't build a provider (JSON/TOML/text/object exports, builtins, already-required CJS) leaked two strings per import. TheLoaderHooksfn-pointer table betweenbun_jscandbun_runtimeis replaced by directextern "Rust"calls;Errorable's error arm is just theJSValue(thecodewas never read).Fixes
Leaks (a
WTF::StringImplref — or the whole string — never released), per occurrence unless noted:import/requireof JSON, JSONC, TOML, YAML, text, HTML,bun:main,internal/*, builtins and already-required CJS:ResolvedSource.specifierandsource_url(andsource_codefor the JSON/object/builtin arms)await import) module fulfilment:source_urlbun --hot/ bake dev server: the entire server HMR patch bundle on each reload; the initial server runtime;BakeProdResolve/BakeProdLoad/BakeToWindowsPathresultsBun.sql(Postgres): every field of every error/notice message; theJSON.stringifyresult for eachjson/jsonbbind parameter (1.4.0 regression: bun:sql leaks native memory on every query binding a jsonb parameter #40102); inline column-name atoms inCachedStructureBun.sql(MySQL): JSON column valuesnew Bun.RedisClient(url): the URL string and each parsed component; every RESP map keyBun.udpSocket({ hostname })/connectaddress;Bun.serve({ unix }); the server URL passed to the inspector; error-path hostname stringscode/messagenode:utilparseArgs: one string per argv element and per short optionnode:quic: header names/values, ALPN/SNI/status strings;node:http2custom settings keysHTMLRewriterelement.attributesiterator name/valuenode:modulenew SourceMap(payload)sources/names,findSourceMappathexpect(v, label),expect.extendmatcher names,describe(fn)/test(fn)derived names,console.logtable column names / class display names,console.trace()(built the exception twice)ResolveMessage.code,Bun.$escape, IPCcmdstrings,process.title = …,Workercreation error message, WebSocket client close reason on error paths,Bun.Transpilertransform result when warnings reject,bun:fficc/linkSymbolsstrings on error,import.meta.urldefine in the bundler,bun installhosted-git URL parts,bun build --bytecodechunk source URL, code-coverage source URL,fs.Direntname/path on early return, bundler pluginonResolve/onLoadstrings when the plugin is tombstonedVirtualMachine.main_resolved_pathat teardown.jscbytecode buffer andModuleInfofor already-bundled modules on some pathsZigException::Holder: strings of stack frames hidden by the frame filterUse-after-free / lifetime:
Bun.dlopenread its path argument after walking the usersymbolsobject (getters could drop it)console.log/expectformatting was reused after printing props/childrenBun.YAML.stringifykept anchor names past the property iterator that owned themgetClassName/getNamePropertyreturned views into function-localWTF::Stringsos.userInfo()options wereptr::readout of C++-owned memory (double free)toString()-created temporary JSString whose characters were read after a GC point (now pinned byJSStringView)Other:
BakeLoadInitialServerCodecopied the whole server HMR runtime into a new StringImpl at dev-server start; it now wraps the embedded bytesaddress/remoteAddressgetters swallowed a pending exception asundefinedAlso removes
Bun::toString(JSGlobalObject*, JSValue)(a +1 producer named like a borrow),StringCell/OwnedString, the duplicatebun_jsc::URLwrapper (nowbun_url::whatwg::URL, held via the RAIIwhatwg::Parsed), andto_slice_or_null/to_slice_clone(identical toto_slice).No extra refcount traffic or allocations vs
main:Drop/Cloneare the same tag-check + inc/dec the manual calls were;to_js_string_viewis one FFI call likeget_zig_stringwas; and a number ofref()/dupe_ref()pairs, a full transpiler-cache output copy, and per-modulespecifierrefs are gone.Struct size changes (nothing grows; shared layouts are asserted on both sides):
ResolvedSource(Rust ⇄ C++): 160 → 136 bytes —specifierand the unusedallocatorremoved,needsDeref→bytecode_cache_owned.ErrorableResolvedSourcefollows (168 → 144).Zig::SourceProvider(C++): no longer embeds aResolvedSource; keeps onlym_moduleInfo/m_tag/m_alreadyBundled(~144 bytes smaller per loaded module).TranspilerJobdrops itsfetcherfield;RuntimeTranspilerCache::Entry::output_codeis a plainStringinstead of an enum;ZigException::HolderusesOption<ZigException>instead ofMaybeUninit+loaded: bool.bun_core::String(24, asserted),StringView/JsCell<String>(transparent),ZigStackTrace/ZigStackFrame,SystemError, generated class payloads.Fixes #40102
Supersedes #32293.
How did you verify your code works?
cargo check/clippyon linux/macOS/windows targets; debug+ASAN build driven through module loading (ESM/CJS/JSON/TOML/text/?query, builtins, virtual modules),console.log/Bun.inspect(errors, JSX, boxed primitives, symbols, classes),node:path,node:moduleSourceMap, workers,process.title,os.userInfo, WebSocket send/publish, UDP/unix-socket serve,parseArgs, TOML/YAML/XML stringify,Bun.dlopen, bake dev server startup. Ran the module, resolve, path, parse-args, worker, inspect(-error), expect, shell, transpiler-cache, websocket-server, serve, zlib, udp, dns, http2 and serve-body-leak test files locally againstmain's debug build. New RSS regression tests (each repeats the operation with a ~256 KiB string and fails on 1.4.0 with the growth shown): Postgresjsonbbind (166 MiB) and ErrorResponse fields (125 MiB) via thewire-frames.tsmock,util.parseArgs(77),expect(v, label)(76),module.SourceMap(158),Bun.udpSocket({hostname})(73),HTMLRewriterattributes (90),new RedisClient(url)(145), RESP map keys (150), S3 error (47). The use-after-free fixes are not observable on a release build of 1.4.0 without ASAN, so they have no test.