Skip to content

crypto: invoke the pbkdf2 callback from the native job - #39922

Merged
dylan-conway merged 6 commits into
mainfrom
claude/crypto-pbkdf2-native-callback
Aug 21, 2026
Merged

dylan-conway merged 6 commits into
mainfrom
claude/crypto-pbkdf2-native-callback

Conversation

@dylan-conway

@dylan-conway dylan-conway commented Aug 21, 2026 •

Copy link
Copy Markdown
Member

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 in src/js/node/crypto.ts does promise.then(result => cb(null, result), err => cb(err)) through guardCallback (there was a // TODO: move this to native). crypto.scrypt already invokes its callback directly from the work-pool job.

This makes pbkdf2 do the same. Pbkdf2Job now carries the callback (Strong, wrapped with the current async context) and calls it from then() via run_callback as (null, Buffer) on success or (err) on failure. The "digest omitted, callback in 5th position" shuffle moves into PBKDF2::from_js, which returns the validated callback (as Scrypt::from_js does). The JS wrapper, its guardCallback import, and one Promise plus two closures per call go away. crypto.pbkdf2 is 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_TYPE for digest/callback, ERR_CRYPTO_INVALID_DIGEST), err === null on success, the result is a Buffer, a throwing callback surfaces as uncaughtException, util.promisify(crypto.pbkdf2) works, the return value is undefined, crypto.pbkdf2.name and .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 exact digest/callback validation messages, a throwing callback reaching uncaughtException (subprocess), and 200 pending callbacks surviving Bun.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, the crypto-pbkdf2 case of test/js/node/async_hooks/AsyncLocalStorage-tracking.test.ts, and the crypto module names test in node-crypto.test.js, all on the debug build.
  • 2000 success and 2000 failing derivations interleaved with full GCs under the ASAN debug build with BUN_JSC_validateExceptionChecks=1: no crash, no exception-check report.

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

coderabbitai Bot commented Aug 21, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

Your included review limit has been reached.

You’re in a promotional period — use the checkbox below to run this review for free:

  • Run 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 @coderabbitai review --use-credits.

You can also wait for the limit to reset (next review available in 14 minutes), then comment @coderabbitai review or push new commits to the PR.

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 configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a8c9862f-d2ab-4b2f-b5eb-e4b2c361e078

📥 Commits

Reviewing files that changed from the base of the PR and between 95ba32a and c343a82.

📒 Files selected for processing (1)
  • src/runtime/crypto/PBKDF2.rs

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 560c7d91-e663-4b35-a662-de3bbd2f0b9f

📥 Commits

Reviewing files that changed from the base of the PR and between a1f2e22 and 95ba32a.

📒 Files selected for processing (4)
  • src/js/node/crypto.ts
  • src/runtime/crypto/PBKDF2.rs
  • src/runtime/node/node_crypto_binding.rs
  • test/js/node/crypto/pbkdf2.test.ts

Included review availability: Your plan provides up to 5 included reviews per hour; 0 remain after this review.


Walkthrough

The native crypto.pbkdf2 implementation now handles callback-based asynchronous execution directly. It retains callbacks across jobs, invokes Node-style completion callbacks, returns undefined, and adds compatibility tests for validation, context, promisification, garbage collection, and exceptions.

Changes

PBKDF2 callback execution

Layer / File(s) Summary
Callback parsing and async completion
src/runtime/crypto/PBKDF2.rs
PBKDF2 parsing returns and validates callbacks. Async jobs retain callbacks with Strong and invoke them with Node-style success or error arguments.
Native binding and export wiring
src/runtime/node/node_crypto_binding.rs, src/js/node/crypto.ts
The native binding schedules asynchronous jobs directly and returns undefined. The JavaScript wrapper was removed, and the function arity is now six.
Node compatibility validation
test/js/node/crypto/pbkdf2.test.ts
Tests cover callback results, async context, garbage collection, promisification, function metadata, argument validation, and callback exceptions.

Suggested reviewers: robobun, jarred-sumner, alii

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: native PBKDF2 jobs invoke callbacks directly.
Description check ✅ Passed The description includes both required sections and provides a detailed change summary plus comprehensive verification results.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

Comment thread src/runtime/crypto/PBKDF2.rs Outdated
Comment thread src/runtime/node/node_crypto_binding.rs Outdated
Comment thread src/js/node/crypto.ts Outdated
Comment thread src/runtime/node/node_crypto_binding.rs Outdated
…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.
@robobun

robobun commented Aug 21, 2026 •

Copy link
Copy Markdown
Collaborator
Updated 1:37 PM PT - Aug 21st, 2026

@robobun, your commit c343a82 is building: #102811

@robobun

robobun commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

Adopted this PR. Pushed 9d3dc4d on top:

  • crypto.pbkdf2.length was 5 after this change (the native function is now exported directly, and it was registered with length 5). Node and the removed wrapper report 6. Registered it with 6.
  • PBKDF2::from_js returns the callback, as Scrypt::from_js does, instead of the host function repeating the 5th/6th position check.
  • Removed the TODO in crypto.ts that described the removed wrapper.
  • Added tests for name/length, util.promisify, exact validation messages, a throwing callback reaching uncaughtException, and callbacks surviving GC while the job is pending.

test/js/node/crypto/pbkdf2.test.ts (51 tests), Node's test-crypto-pbkdf2.js, and the AsyncLocalStorage crypto-pbkdf2 case pass on the debug build. Waiting on CI.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

@dylan-conway

Copy link
Copy Markdown
Member Author

@robobun sync with main and address all reviews

@robobun

robobun commented Aug 21, 2026 •

Copy link
Copy Markdown
Collaborator

Synced with main (merge fe26e9a, no conflicts). All review threads are addressed and resolved.

  • 95ba32a: Pbkdf2Job::then no longer propagates a create_buffer failure past the callback. It runs the callback with the error, as the derivation failure branch does. This branch has no test: with keylen capped at i32::MAX, create_buffer can only fail on allocation failure.
  • c343a82: dropped the termination check from that branch. Bun__JSValue__call already makes run_callback a no-op on a stopping VM.
  • The three earlier bot threads were addressed in 9d3dc4d.

On the debug build at c343a82: test/js/node/crypto/pbkdf2.test.ts (51 pass), Node's test-crypto-pbkdf2.js, test-domain-crypto.js, test-crypto-domain.js, and the crypto-pbkdf2 case of AsyncLocalStorage-tracking.test.ts pass.

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

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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:

  • .length was 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_js returns the callback directly.
  • create_buffer failure ?-propagating past the callback → now a match that take_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.

Comment thread src/runtime/crypto/PBKDF2.rs
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.
@dylan-conway
dylan-conway merged commit 614d19f into main Aug 21, 2026
9 of 11 checks passed
@dylan-conway
dylan-conway deleted the claude/crypto-pbkdf2-native-callback branch August 21, 2026 20:46
robobun pushed a commit that referenced this pull request Aug 21, 2026
…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.
robobun added a commit that referenced this pull request Aug 22, 2026
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.
robobun added a commit that referenced this pull request Aug 22, 2026
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.
robobun pushed a commit that referenced this pull request Aug 24, 2026
…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).
robobun pushed a commit that referenced this pull request Aug 24, 2026
…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).
robobun pushed a commit that referenced this pull request Aug 24, 2026
…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.
robobun pushed a commit that referenced this pull request Aug 25, 2026
…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).
robobun pushed a commit that referenced this pull request Aug 25, 2026
…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).
robobun pushed a commit that referenced this pull request Aug 25, 2026
…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.
robobun pushed a commit that referenced this pull request Aug 27, 2026
…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.
robobun pushed a commit that referenced this pull request Aug 27, 2026
…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.
robobun pushed a commit that referenced this pull request Aug 28, 2026
…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.
robobun pushed a commit that referenced this pull request Aug 28, 2026
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants