crypto: invoke the pbkdf2 callback from the native job - #39922
Conversation
…S promise bridge crypto.pbkdf2() went native -> Promise -> JS `.then(result => cb(null, result))` with a guardCallback wrapper, while scrypt already calls its callback straight from the work-pool job. Do the same for pbkdf2: the job holds the callback (with async context) and calls it as (null, Buffer) / (err) via run_callback, so a throwing callback is still an uncaughtException. The digest-omitted argument shuffle moves into PBKDF2::from_js. Removes the JS wrapper and one promise + two closures per call.
|
Warning Review limit reachedYour included review limit has been reached. You’re in a promotional period — use the checkbox below to run this review for free:
On-demand reviews are free for the next 30 days. After that, they cost $0.25 per reviewed file. How can I continue?Run this review now using the option above, or comment You can also wait for the limit to reset (next review available in 14 minutes), then comment An organization admin can change what happens after included review limits in Billing. How do review limits work?CodeRabbit enforces per-developer PR review limits within each organization. For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (1)
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (4)
Included review availability: Your plan provides up to 5 included reviews per hour; 0 remain after this review. WalkthroughThe native ChangesPBKDF2 callback execution
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
…from_js The native pbkdf2 function is now crypto.pbkdf2 itself, so its declared length is what crypto.pbkdf2.length reports. Node and the removed JS wrapper both report 6. Register it with 6. PBKDF2::from_js already picks the callback out of the 5th or 6th argument. Return it, as Scrypt::from_js does, instead of repeating the position check in the host function. Remove the TODO in crypto.ts that described the removed wrapper. Tests: pin name and length, util.promisify, the exact validation messages, a throwing callback reaching uncaughtException, and the callback surviving GC while the job is pending.
|
Adopted this PR. Pushed 9d3dc4d on top:
|
There was a problem hiding this comment.
LGTM — thanks for the follow-up commit. .length is now 6 (with a test), the stale TODO is gone, and returning the callback from from_js removes the dead position check. The remaining create_buffer ? note is a practically-unreachable edge case and needn't block.
Extended reasoning...
Overview
This PR replaces the JS-side crypto.pbkdf2 wrapper (native promise → .then → guardCallback(cb)) with direct callback invocation from Pbkdf2Job::then(), mirroring the existing ScryptJob pattern in the same file. PBKDF2::from_js now returns (PBKDF2, JSValue) with the validated callback (as Scrypt::from_js does), the digest-omitted argument shuffle moves into from_js, the callback is rooted via Strong and wrapped with with_async_context_if_needed, and run_callback invokes it as (null, Buffer) / (err). The JS wrapper, the guardCallback import, and the // TODO: move this to zig comment are deleted; the native function's registered arity is bumped to 6.
Prior feedback addressed
My earlier review (on the pre-9d3dc4d5 revision) raised four nits. Commit 9d3dc4d addresses three: the arity is now 6 with a test pinning [name, length] === ['pbkdf2', 6]; the stale TODO is removed; and from_js returns the callback so the dead argument(4).is_function() re-check in the host_fn is gone. The fourth — ?-propagating a create_buffer failure past the callback — remains, but as noted in the original comment it is essentially unreachable (tiny JSC cell wrapping an already-allocated Vec; if that OOMs the process is dead anyway) and does not block.
Security risks
None. The BoringSSL derivation path, input validation, and buffer handling are unchanged; only the completion-delivery mechanism moved from a JS promise bridge to event_loop.run_callback, which is exactly what scrypt, randomFill, and every ExternCryptoJob in this file already use.
Level of scrutiny
Medium — native crypto binding with GC rooting, but the change is a straight structural port to the established ScryptJob/ExternCryptoJob pattern in the same file. The new Pbkdf2Js { callback: Strong } matches ScryptJs; create_job matches the scrypt schedule call including with_async_context_if_needed. I checked that removing the guardCallback import is safe (still exported from internal/shared and used in fs.ts/dns.ts).
Other factors
Test coverage is thorough for the delta: (null, Buffer) shape + ALS propagation, a 400-callback GC-stress test covering both success and error paths (proving the Strong root works), util.promisify, name/length, exact synchronous validation messages for the digest-omitted and missing-callback cases, and a subprocess test asserting a throwing callback surfaces as uncaughtException (previously guaranteed by guardCallback, now by run_callback). No outstanding human reviewer comments.
|
@robobun sync with main and address all reviews |
|
Synced with main (merge fe26e9a, no conflicts). All review threads are addressed and resolved.
On the debug build at c343a82: |
… cannot be created Pbkdf2Job::then propagated a create_buffer failure out of the completion, so the user callback never ran for that derivation. Take the exception and pass it to the callback instead, as the derivation failure branch does. A termination is still handed back to the event loop.
There was a problem hiding this comment.
LGTM — all four prior findings (.length 5→6, stale TODO, dead 5th/6th-position branch, create_buffer failure skipping the callback) are addressed in the current diff, and the bug hunter found nothing this run.
Checked: Pbkdf2Js { callback: Strong } + run_callback mirrors ScryptJs/RandomFillJs in the same file; with_async_context_if_needed matches scrypt/ExternCryptoJob; the take_error→is_termination_exception branch correctly propagates termination while running the callback for OOM; removing the guardCallback import leaves it live in fs.ts/dns.ts.
Extended reasoning...
Overview
Moves crypto.pbkdf2's callback invocation from a JS wrapper (Promise + guardCallback + two closures) into the native Pbkdf2Job::then(), matching how crypto.scrypt, randomFill, and every ExternCryptoJob in the same file already work. PBKDF2::from_js now returns (PBKDF2, JSValue) with the validated callback (as Scrypt::from_js does), Pbkdf2Job carries the callback in a Strong inside a #[derive(JsAffine)] struct, and then() calls event_loop.run_callback with (null, Buffer) or (err). The JS wrapper and its guardCallback import are deleted; the native function is now the export, registered with length 6.
Prior review round
I left four nits on the earlier revision; all are addressed:
.lengthwas 5 → now registered as 6, with a test pinning[name, length] === ['pbkdf2', 6].- Stale
// TODO: move this to zig→ removed. - Dead
call_frame.argument(4).is_function()re-check in the host_fn → removed;from_jsreturns the callback directly. create_bufferfailure?-propagating past the callback → now amatchthattake_errors, returns on termination, and otherwise runs the callback with the error (commit 95ba32a).
Security risks
None. No new user-controlled input parsing — argument validation is unchanged from the pre-PR from_js (the arg4/arg5 shuffle just moved from JS into Rust with identical semantics, pinned by the new "Received undefined" test). No new syscalls, no cross-boundary data.
Level of scrutiny
Medium. This touches a work-pool job's JS-thread completion path, so GC rooting and callback-on-every-path matter. Both are handled by established patterns in the same file: Strong inside a JsAffine struct as the Job::Js type is exactly what ScryptJs, RandomFillJs, and ExternCryptoJob do, and run_callback is the standard invocation path. The GC-stress test (200×2 pending jobs across Bun.gc(true), both success and error paths) exercises the rooting.
Other factors
Test coverage is thorough for a refactor: ALS propagation, (null, Buffer) shape, util.promisify, arity, exact validation messages for the shuffled digest/callback slots, throwing-callback → uncaughtException (subprocess), and the GC stress test. The PR description reports the full file, Node's test-crypto-pbkdf2.js, and a 4000-derivation ASAN + validateExceptionChecks run pass. I verified guardCallback remains live (fs.ts, dns.ts), so the removed import doesn't orphan anything.
Bun__JSValue__call is the native to JS gate: on a stopping VM, run_callback is a no-op. The completion does not need its own check.
…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 from 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 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 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 #39922's bodies (callback handed to the native job, one from_js call) under the scoped signatures.
Main replaced the CryptoJobCtx trait and crypto_job_init_and_schedule with the JobContext trait (Job::schedule, run/then halves) in #39922. The merge kept the argon2 job on the removed API, which broke the build. Move the job to JobContext: the derivation owns copied inputs as the OffThread half, and a Strong callback is the Js half. create_buffer now returns JsResult, so then() reports an allocation failure to the callback and the sync path propagates it.
pbkdf2 lost its domain routing when its JS wrapper was replaced by the native callback path (#39922); scrypt never had it. Both now use the same wrapper as randomBytes, and the guardCallback import that the merge dropped is back.
…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).
…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).
…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.
…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.
What does this PR do?
crypto.pbkdf2(password, salt, iterations, keylen, digest, callback)was implemented as: native job resolves a Promise, then a JS wrapper insrc/js/node/crypto.tsdoespromise.then(result => cb(null, result), err => cb(err))throughguardCallback(there was a// TODO: move this to native).crypto.scryptalready invokes its callback directly from the work-pool job.This makes pbkdf2 do the same.
Pbkdf2Jobnow carries the callback (Strong, wrapped with the current async context) and calls it fromthen()viarun_callbackas(null, Buffer)on success or(err)on failure. The "digest omitted, callback in 5th position" shuffle moves intoPBKDF2::from_js, which returns the validated callback (asScrypt::from_jsdoes). The JS wrapper, itsguardCallbackimport, and one Promise plus two closures per call go away.crypto.pbkdf2is now the native function itself, so it is registered with length 6: that is what Node and the removed wrapper report.Unchanged: synchronous validation errors (
ERR_INVALID_ARG_TYPEfordigest/callback,ERR_CRYPTO_INVALID_DIGEST),err === nullon success, the result is aBuffer, a throwing callback surfaces asuncaughtException,util.promisify(crypto.pbkdf2)works, the return value isundefined,crypto.pbkdf2.nameand.length.How did you verify your code works?
test/js/node/crypto/pbkdf2.test.ts(51 pass on the debug build). New tests pin:(null, Buffer)and AsyncLocalStorage propagation,name/length,util.promisify, the exactdigest/callbackvalidation messages, a throwing callback reachinguncaughtException(subprocess), and 200 pending callbacks survivingBun.gc(true)on both the success and the error path. The whole file also passes on the current release, so it documents the behavior this refactor preserves.test/js/node/test/parallel/test-crypto-pbkdf2.js,test-domain-crypto.js,test-crypto-domain.js, thecrypto-pbkdf2case oftest/js/node/async_hooks/AsyncLocalStorage-tracking.test.ts, and thecrypto modulenames test innode-crypto.test.js, all on the debug build.BUN_JSC_validateExceptionChecks=1: no crash, no exception-check report.