jsc: replace MarkedArrayBuffer::from_bytes with an owning constructor - #32031
jsc: replace MarkedArrayBuffer::from_bytes with an owning constructor#32031alii wants to merge 2 commits into
Conversation
|
Rebased onto current main (head 2efb507: the src fix plus the leak-test budget fix alii asked for). Neither change has landed on main independently; |
alii
left a comment
There was a problem hiding this comment.
can we use rust tests for this rather than an entirely new separate crate that is built from within bun:test? seems wasteful. we have some other rust tests you could likely follow for reference. just make sure that they do still run in ci otherwise not useful. @robobun
|
This PR may be a duplicate of:
🤖 Generated with Claude Code |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughRefactors ChangesMarkedArrayBuffer ownership safety and migration
Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/jsc/array_buffer.rs`:
- Around line 1036-1045: The method to_node_buffer currently only clears
owns_buffer but leaves self.buffer.ptr/byte_len live — change it to consume the
backing buffer by taking/replacing self.buffer with ArrayBuffer::EMPTY (or
equivalent take/null) before calling JSValue::create_buffer so the original
MarkedArrayBuffer no longer holds the allocation; apply the same take/replace
pattern in the sibling method to_js (and any other handoff sites) so ownership
is transferred exactly once and the source handle is neutralized.
- Around line 962-969: MarkedArrayBuffer::from_string currently uses
Box::from(str) which is infallible and will OOM-abort instead of returning
bun_alloc::AllocError; replace the infallible allocation with a fallible path
that returns Err(bun_alloc::AllocError) on OOM (e.g., allocate into a Vec<u8>
using try_reserve/try_reserve_exact then extend_from_slice and convert to
Box<[u8]> or call the in-tree fallible allocator helper such as allocator.dupe
for u8), then pass the owned bytes to MarkedArrayBuffer::from_owned_bytes so the
function honors its Result<..., bun_alloc::AllocError> contract and keeps
existing semantics with MarkedArrayBuffer_deallocator/default_alloc::free.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 3052f0c9-4b3c-42d4-baa8-ed10e6fb9acd
⛔ Files ignored due to path filters (1)
test/regression/marked-array-buffer-ownership-soundness-fixture/Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (10)
src/jsc/array_buffer.rssrc/runtime/api/bun/Terminal.rssrc/runtime/api/bun/subprocess/Readable.rssrc/runtime/api/bun/subprocess/SubprocessPipeReader.rssrc/runtime/node/node_fs.rssrc/runtime/shell/subproc.rstest/regression/marked-array-buffer-ownership-soundness-fixture/.gitignoretest/regression/marked-array-buffer-ownership-soundness-fixture/Cargo.tomltest/regression/marked-array-buffer-ownership-soundness-fixture/src/lib.rstest/regression/marked-array-buffer-ownership-soundness.test.ts
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
test/regression/marked-array-buffer-ownership-soundness.test.ts (1)
23-26:⚠️ Potential issue | 🔴 Critical | ⚡ Quick winAdd missing imports from harness.
The test uses
bunExe()(lines 82, 115) andbunEnv(lines 99, 132) but never imports them. This will fail at runtime withReferenceError.🐛 Proposed fix
-import { spawn, which } from "bun"; +import { bunEnv, bunExe } from "harness"; +import { spawn, which } from "bun"; import { expect, test } from "bun:test"; import { existsSync } from "node:fs"; import { join } from "node:path";🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@test/regression/marked-array-buffer-ownership-soundness.test.ts` around lines 23 - 26, The test references bunExe() and bunEnv but never imports them; add missing imports for bunExe and bunEnv from the test harness module at the top of test/regression/marked-array-buffer-ownership-soundness.test.ts so the functions are available to the tests (import the named exports bunExe and bunEnv from the harness module used by other tests).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@test/regression/marked-array-buffer-ownership-soundness.test.ts`:
- Around line 74-139: Delete the duplicated test block that repeats the test
titled "empty buffers round-trip without freeing the dangling sentinel" so only
one instance remains; locate the two identical test(...) declarations (the
blocks starting with test("empty buffers round-trip without freeing the dangling
sentinel", async () => { ... })) and remove one of them (keep the other intact)
to eliminate the copy-paste duplicate while preserving the assertions and spawn
logic.
---
Outside diff comments:
In `@test/regression/marked-array-buffer-ownership-soundness.test.ts`:
- Around line 23-26: The test references bunExe() and bunEnv but never imports
them; add missing imports for bunExe and bunEnv from the test harness module at
the top of test/regression/marked-array-buffer-ownership-soundness.test.ts so
the functions are available to the tests (import the named exports bunExe and
bunEnv from the harness module used by other tests).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: de0c920e-5573-41b0-a2b5-45bff4568055
📒 Files selected for processing (1)
test/regression/marked-array-buffer-ownership-soundness.test.ts
|
@robobun? adopt plz |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/rust-test.yml:
- Line 42: The action metadata for setup-bun uses unsupported `type:` keys on
inputs which breaks workflow parsing; open the action metadata for the setup-bun
local action and remove the `type:` entries from the inputs definitions (for
inputs named `bun-version`, `baseline`, and `download-url`) so the inputs remain
declared without `type` fields and the workflow can parse the action correctly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 39531e4c-ccd2-4776-a299-8a1f4580c2f5
📒 Files selected for processing (4)
.github/workflows/rust-test.ymlsrc/jsc/JSGlobalObject.rssrc/jsc/array_buffer.rssrc/jsc/host_object.rs
20e2596 to
299c27b
Compare
|
Updated 3:08 PM PT - Aug 16th, 2026
✅ @robobun, your commit 2efb50787ecce0f427b69aa6828ab4d7c2e8c03b passed in 🧪 To try this PR locally: bunx bun-pr 32031That installs a local version of the PR into your bun-32031 --bun |
|
Rebased onto main and force-pushed (299c27b). One conflict, in the Re-verified on the rebased tree: |
|
@robobun get this mergeable — or close it if the changes are already covered on main / too stale to salvage. |
|
On it. Checked main first: it is not covered there. Main has since picked up the peripheral hardening from this PR (the |
a4b44ed to
aecad2d
Compare
|
Re-targeted onto current main and force-pushed as a single commit (aecad2d), replacing the stale stack. Main had independently picked up the peripheral parts of the old branch ( Verified on the rebuilt diff under ASAN: doctest, the zero-length readFile worker-terminate test (exercises Drop on an unconverted empty buffer), terminal (98), spawnSync (14), the spawn buffer paths, and fs readFile/encoding/readlink all pass. Description updated; CI is running on the new head. |
There was a problem hiding this comment.
I re-reviewed after the force-push to aecad2d and found no bugs. Both earlier notes are resolved: destroy() now neutralizes self.buffer after the free, and the stale .github/workflows/CLAUDE.md / rust-test.yml hunk is gone (the diff is now the 5 source files only). Because this rewires allocator/FFI ownership at the JSC boundary — the repo's most-scrutinized category — a maintainer sign-off is still worthwhile.
What was reviewed:
from_owned_bytes: empty-box →owns_buffer=falsepreserved;Bufferalias innode_fs.rsresolves toMarkedArrayBuffer, so all threeBuffer::from_owned_bytessites hit the new constructor.to_js/to_node_buffer/destroyhandle-neutralization: grepped everyto_node_buffer/to_jscaller — none readsself.bufferafter the handoff, and thefrom_jsearly-return into_jsstill fires before themem::replace.Readable.rsstruct-literal → constructor: the old literal setowns_buffer=trueunconditionally; the new path sets it to!bytes.is_empty(). Checked the empty case —to_node_bufferruns immediately either way, so no behavior change (and the new path is strictly safer for the dangling empty-box pointer).- No remaining
MarkedArrayBuffer::from_bytescallers outside thecompile_faildoctest.
Extended reasoning...
Overview
The PR replaces the unsound MarkedArrayBuffer::from_bytes(&mut [u8], JSType) constructor in src/jsc/array_buffer.rs with from_owned_bytes(Box<[u8]>, JSType), migrates the six in-tree callers (from_string, Terminal.rs, SubprocessPipeReader.rs, Readable.rs, and three node_fs.rs sites via the Buffer alias), and neutralizes self.buffer via mem::replace(.., ArrayBuffer::EMPTY) in the three release paths (destroy, to_js, to_node_buffer). Net diff is 5 files, ~+16 lines. Since my last review the branch was force-pushed to a single commit that drops all .github/ changes, resolving the stale rust-test.yml reference I'd flagged.
Security risks
None identified. This is an internal Rust-API soundness fix; no user-controlled input flows through the changed code paths differently than before, and the pointers/deallocators handed to JSC are byte-identical to what the leak-and-reborrow callers produced.
Level of scrutiny
High. src/jsc/array_buffer.rs is FFI glue that hands raw allocations to JavaScriptCore with a GC-time deallocator, and MarkedArrayBuffer now has a Drop impl — exactly the "every allocation has one owner, released once, with the right allocator" territory REVIEW.md calls out as the most-blocked category. The change is mechanically simple, but the domain warrants a maintainer's eyes.
Other factors
- Both of my prior inline notes (the
destroy()dangling-handle nit and the orphanedrust-test.ymldoc reference) are addressed in the current head. - CodeRabbit's earlier "neutralize on handoff" concern is addressed (
mem::replacein all three release paths). - alii is actively engaged and asked for the PR to be made mergeable, so a human review is already expected; this note is to record what the automated pass covered.
- Verified
Bufferinnode_fs.rsaliasesjsc::MarkedArrayBuffer(src/runtime/node/types.rs:17), and grepped for any remainingMarkedArrayBuffer::from_bytescallers (only the intentionalcompile_faildoctest remains).
|
@robobun fix the debian-x64-asan test failure in build #91768. |
|
On it. The debian-x64-asan lane in #91768 had two test files red: |
There was a problem hiding this comment.
I reviewed the re-targeted diff and found no issues. All prior automated-review feedback (the destroy() handle-neutralization nit and the stale .github/workflows/CLAUDE.md reference) is addressed on the current head — the diff is now just the five .rs files. Because this is a memory-ownership change at the Rust↔JSC FFI boundary, a maintainer sign-off is still worthwhile.
What was reviewed:
from_owned_bytescomputesowns_buffer = !bytes.is_empty()before the move intoArrayBuffer::from_owned_bytes, matching main's empty-box rule; theReadable.rsstruct-literal that hard-codedowns_buffer: truenow goes through it, so the empty case is handled.destroy()/to_js()/to_node_buffer()all take/null the buffer (mem::replace→ArrayBuffer::EMPTY), so a repeated handoff or post-destroy accessor cannot reach the freed pointer; the leak-on-throw direction into_jsis unchanged (owns_buffer was already cleared pre-call on main).- Grepped for remaining
MarkedArrayBuffer::from_bytescallers andowns_buffer: truestruct literals — none survive outside thecompile_faildoctest; the remainingArrayBuffer::from_bytessites are the separate #31970 hole the PR scopes out. - Confirmed
node_fs.rs'sBufferalias resolves toMarkedArrayBuffer(src/runtime/node/types.rs:17), so the threeBuffer::from_owned_bytesmigrations bind to the new constructor.
Extended reasoning...
Overview
The PR replaces the unsound MarkedArrayBuffer::from_bytes(&mut [u8], JSType) constructor in src/jsc/array_buffer.rs with from_owned_bytes(Box<[u8]>, JSType), routes from_string through it, migrates the six in-tree callers (Terminal, SubprocessPipeReader, Readable, node_fs ×3), and hardens the three release paths (destroy, to_js, to_node_buffer) to take the buffer out of self via mem::replace(.., ArrayBuffer::EMPTY) so a later Drop or repeated handoff cannot double-release. Net +16 lines across 5 .rs files; the earlier .github changes were dropped.
Security risks
This is a soundness hardening at the Rust↔JSC ownership boundary — the class of bug it closes is double-free / free-of-borrowed-storage. The change strictly narrows the API surface (Box-only) and neutralizes handles on transfer, so it reduces risk rather than adding it. No new user-facing surface, no auth/crypto/permissions.
Level of scrutiny
High. src/jsc/array_buffer.rs is the layer that hands raw allocations to JavaScriptCore with a deallocator; a mistake here is a UAF or double-free reachable from every readFile/spawn/terminal data path. That said, the diff itself is small and mechanical: a constructor rename that lets the type system enforce what callers were already doing manually (Box::leak + reborrow), plus the take/null pattern applied uniformly to all three release paths. I traced each migrated call site and confirmed the allocation being handed over was already a default-allocator Box<[u8]> in every case, so the pointers/deallocators reaching JSC are byte-identical to before.
Other factors
All prior automated feedback on this PR is resolved on the current head: CodeRabbit's "consume the backing buffer on handoff" and my destroy() neutralization nit are both applied; the stale rust-test.yml/CLAUDE.md references are gone (no .github changes remain). The bug-hunting pass on this revision found nothing. The maintainer (alii) is actively shepherding and today asked about a debian-x64-asan lane failure — robobun's analysis that inspect-error-leak/require-cache reproduce on main without this diff is consistent with the change (nothing here touches those paths), but that thread is still open. Given the FFI-memory-ownership subject matter and the active maintainer engagement, deferring rather than auto-approving.
|
Fixed in d3ed391 (rebased onto your latest merge of main). What was actually failing on the x64-asan lane in #91768: The fix keeps leak detection as it was and fixes the clocks: the budgets scale under ASAN or debug builds (same |
There was a problem hiding this comment.
I re-reviewed after the retarget and the test-budget commit; no bugs found. Both earlier nits are resolved (destroy() now neutralizes self.buffer, and the .github/workflows/CLAUDE.md hunk is gone). The Rust side looks correct — the constructor swap, the six caller migrations, and the take-on-handoff pattern all check out, and the Readable.rs struct-literal → from_owned_bytes change also fixes the empty-box owns_buffer=true edge case that was there before.
Given this touches FFI ownership transfer plus the new leak-test budget tuning (the esm-fixture-leak-small iteration/threshold scaling under instrumentation in particular), a maintainer sign-off on the final shape would still be worthwhile.
What was reviewed:
from_owned_bytesempty-box handling and themem::replaceneutralization indestroy/to_js/to_node_buffer— allocator/ownership pairing is unchanged from the pre-PR call sites.- Fixture
isASANdetection matchesharness.ts'sdetectASAN();isASANEnabledexists inbun:internal-for-testing. esm-fixture-leak-small.mjsthreshold formula at 100k iterations reduces to the previous 100 MB / 500 MB bounds, so release-lane sensitivity is unchanged.
Extended reasoning...
Overview
Eleven files. Five Rust files close the #31969 soundness hole by replacing MarkedArrayBuffer::from_bytes(&mut [u8]) with from_owned_bytes(Box<[u8]>), migrating six call sites (from_string, Terminal, SubprocessPipeReader, Readable, node_fs ×3 via the Buffer alias), and neutralizing self.buffer in destroy/to_js/to_node_buffer so a repeated handoff or post-destroy access cannot observe the freed pointer. Six test files (new since my 2026-08-10 review) address pre-existing debian-x64-asan failures alii asked robobun to fix: the four leak fixtures now detect ASAN via bun:internal-for-testing (matching harness.ts) instead of the executable name, require-cache.test.ts adds a leakTimeout() helper and scales esm-fixture-leak-small to 10k iterations under instrumentation via LEAK_ITERATIONS, and inspect-error-leak.test.js widens its clock budget under debug/ASAN.
Security risks
None introduced. The Rust change strictly tightens an API boundary — the old constructor let safe code hand a stack pointer to default_alloc::free; the new one requires Box<[u8]>. The deallocators installed (MarkedArrayBuffer_deallocator / JSValue::create_buffer's deallocator) and the allocator that produced every migrated buffer (.into_boxed_slice() on the global allocator) are unchanged from main. No untrusted-input parsing, auth, or crypto is touched.
Level of scrutiny
High. This is FFI ownership transfer in bun_jsc — exactly the "every allocation has one named owner, released once, with the allocator that allocated it" territory REVIEW.md calls out as most-blocked. Separately, the test changes tune RSS thresholds and iteration counts on leak regression tests, which per REVIEW.md needs care that the property the original assertion protected is still asserted. I verified the release-lane bounds are byte-identical (100 MB / 500 MB at 100k iterations) and only the instrumented lanes are relaxed, which follows the repo convention that ASAN's job is UAF detection while release lanes are the RSS canary — but a maintainer should confirm that trade-off is acceptable.
Other factors
All three of my prior inline comments are resolved in the current diff. alii has been actively shepherding ("get this mergeable", "fix the debian-x64-asan test failure") and is the natural sign-off. CI on the new head (#92381) was still building at review time. The Readable.rs migration incidentally corrects a latent owns_buffer=true on an empty boxed slice (dangling ptr) that the old struct literal set unconditionally — not user-reachable, but worth noting the direction is strictly safer.
MarkedArrayBuffer::from_bytes(&mut [u8], JSType) was a safe constructor that stored a borrowed slice's pointer with owns_buffer set, so destroy() (and, since the Drop impl landed, plain drop) freed memory the caller still owned. Safe code could free a stack buffer with it (#31969). Replace it with from_owned_bytes(Box<[u8]>, JSType), built on ArrayBuffer::from_owned_bytes, so the ownership transfer is enforced by the type system. Empty boxes have no backing allocation, so the constructor keeps the existing rule of not marking them owned. Route from_string through it and migrate the remaining callers (Terminal, SubprocessPipeReader, node_fs x3) off the leak-and-reborrow pattern; the Readable struct literal goes through the constructor too. destroy(), to_js() and to_node_buffer() now take the buffer out of self when they release it, so a later destroy()/Drop or a repeated handoff cannot release the same allocation twice. The contract is pinned by a compile_fail doctest on from_owned_bytes, runnable with cargo test --doc -p bun_jsc (rustdoc checks compile_fail doctests without linking, which is what makes a Rust-level test possible for bun_jsc). The removal itself is enforced by every build: any reintroduced caller of from_bytes fails to compile.
inspect-error-leak.test.js and the require-cache leak tests already use ASAN-aware RSS thresholds (the RSS bound is what detects a leak), but their clock budgets were fixed release-build numbers. On the x64-asan lane the 100k-iteration inspect() loop takes ~13s against a 10s budget and the 100k-iteration import() fixture overruns its 30s budget, so both time out regularly there (inspect-error-leak timed out on the asan lane in 56 of the last 300 builds) and debug builds time out locally. Scale the clock budgets when running under ASAN or a debug build, and have the import() fixture, which ran 10x more iterations than its CJS sibling, take its iteration count from the driving test so instrumented builds run a proportionally scaled loop with a proportionally scaled threshold. Release builds keep the same iteration counts, thresholds and budgets as before. The standalone fixtures detected ASAN by sniffing the executable name for bun-asan, which misses debug builds (bun-debug) that also enable ASAN, so they applied release thresholds to ASAN processes and failed locally. They now ask the runtime, the same way harness.isASAN does, and fall back to the name check.
d3ed391 to
2efb507
Compare
|
Rebased onto current main (07d38c1) and force-pushed; the branch is now the two commits, 0547fed (src) and 2efb507 (leak-test budgets), with the earlier merge commits folded away. Neither change has landed on main in the meantime: One conflict, in Re-verified on the rebased tree under the ASAN debug build: the compile_fail doctest, the worker-terminate zero-length readFile test, spawnSync and the spawn buffer tests, fs readFile/encoding/readlink, and both leak-test files all pass. The terminal suite was flaky on this box today (PTY timing tests), but it fails the same way with this PR's src removed, so that is environmental; the Terminal.rs resolution is the same call that passed the suite on CI before the rebase. |
There was a problem hiding this comment.
I reviewed this PR and didn't find any bugs. Both of my earlier nits are addressed (destroy() now neutralizes self.buffer to ArrayBuffer::EMPTY after the free, and the stale .github/workflows/CLAUDE.md hunk is gone). Because the change rewires ownership/deallocation in MarkedArrayBuffer across the FFI boundary and the second commit adjusts leak-test thresholds and iteration counts, a maintainer sign-off would still be worthwhile.
Checked: no remaining MarkedArrayBuffer::from_bytes / Buffer::from_bytes callers outside the compile_fail doctest; the surviving ArrayBuffer::from_bytes sites are #31970 and out of scope. The Readable.rs struct-literal → from_owned_bytes swap changes owns_buffer from true to !bytes.is_empty(), which is correct for the empty-box dangling-ptr case and irrelevant on the immediate to_node_buffer path. The fixture isASAN detection matches harness.ts's detectASAN() verbatim, and release lanes keep their pre-existing iteration counts / thresholds / budgets (esm-fixture-leak-small at 100k → 100/500 MB as before).
Extended reasoning...
Overview
Two commits on top of current main. The first (0547fedf) replaces the unsound MarkedArrayBuffer::from_bytes(&mut [u8]) constructor in src/jsc/array_buffer.rs with from_owned_bytes(Box<[u8]>), migrates the six callers (from_string, Terminal.rs, SubprocessPipeReader.rs, Readable.rs, node_fs.rs ×3 via the Buffer alias), and neutralizes self.buffer to ArrayBuffer::EMPTY in destroy()/to_js()/to_node_buffer() so a repeated release cannot observe the freed pointer. The second (2efb5078) gives the module-load leak tests instrumentation-aware clock budgets and switches four standalone fixtures from execPath-sniffing to bun:internal-for-testing's isASANEnabled().
Security risks
The change is a strict soundness improvement: it removes a safe constructor that could free a stack address. The migrated callers hand the same default-allocator Box<[u8]> allocations to the same deallocator (default_alloc::free / MarkedArrayBuffer_deallocator), so runtime behavior is unchanged. The new handle-neutralization is defense-in-depth (no in-tree caller reuses a MarkedArrayBuffer after handoff). No new attack surface.
Level of scrutiny
High — this is allocator/deallocator wiring across the Rust↔JSC FFI boundary, which REVIEW.md calls out as the most-blocked category. The change is small and well-argued, but ownership transfer across FFI is exactly where a maintainer's eyes add the most value. The second commit adjusts leak-test thresholds and iteration counts; release lanes are provably unchanged, but the instrumented-lane scaling (10k iterations, 446 MB ASAN threshold in esm-fixture-leak-small.mjs) is a judgment call alii should confirm.
Other factors
alii has been actively driving this PR (adopted it, requested the rebase, requested the x64-asan flake fix). All prior review threads (mine, CodeRabbit's) are resolved and reflected in the current diff. The bug-hunting pass found nothing. Verification per the PR description covers the affected paths under ASAN. Given the memory-safety surface plus the leak-threshold adjustments, deferring for a final maintainer sign-off rather than auto-approving.
What
MarkedArrayBuffer::from_bytes(&mut [u8], JSType)(src/jsc/array_buffer.rs) is a safe, public constructor that stores a borrowed slice's pointer in a lifetime-less struct withowns_bufferset.destroy()then frees that pointer with the default allocator, and sinceMarkedArrayBuffergained aDropimpl (#37075) so does a plain drop. Safe code can free a stack buffer:All in-tree callers upheld the contract manually (
Box::leak/heap::into_rawa default-allocatorBox<[u8]>, then reborrow), so there is no user-reachable corruption today; this closes the API-boundary soundness hole.Fixes #31969
Fix
MarkedArrayBuffer::from_bytes(&mut [u8])withMarkedArrayBuffer::from_owned_bytes(Box<[u8]>, JSType), built on the existingArrayBuffer::from_owned_bytes, so the ownership transfer is enforced by the type system. It keeps main's rule that an empty box (no backing allocation) is not marked owned. The separateArrayBuffer::from_byteshole ([Unsoundness] ArrayBuffer no-copy constructors trust raw backing pointers #31970) is deliberately not touched.from_stringthrough it and migrate the remaining callers off the leak-and-reborrow dance:Terminal.rs,SubprocessPipeReader.rs, andnode_fs.rs(x3, via theBufferalias). TheReadable.rsstruct literal goes through the constructor too.destroy(),to_js()andto_node_buffer()take the buffer out ofself(leavingArrayBuffer::EMPTY) when they release it, so a laterdestroy()/Dropor a repeated handoff cannot release the same allocation twice.No behavior change: the pointers handed to JSC and the deallocators installed are identical before and after (callers were already passing
into_boxed_slice()allocations).Test
The removal is enforced by every build: any reintroduced caller of
from_bytesfails to compile. The contract itself is pinned by acompile_fail,E0599doctest onfrom_owned_bytescontaining the snippet above, runnable withcargo test --doc -p bun_jsc(rustdoc checkscompile_faildoctests without linking, which is what makes a Rust-level test possible for bun_jsc, whose test targets cannot link without the C++ side). Mutation-tested: re-adding a safefrom_bytes(&mut [u8])makes the doctest fail; on this diff it passes. Per review, the earlier attempts at wiring this into CI (a fixture crate driven from bun:test, then a dedicated workflow) were dropped; the doctest is a source-level guard.There is no JS-observable behavior to test: the change is to the Rust API surface, and the migrated call sites produce byte-identical results, which the existing suites below cover.
Also in this PR: the x64-asan leak-test flakes
The x64-asan lane failed on
inspect-error-leak.test.jsandrequire-cache.test.ts. Both reproduce with this PR's src changes removed, andinspect-error-leaktimed out on that lane in 56 of the 300 most recent builds, so this is a pre-existing flake class rather than anything this diff does; fixed here at alii's request (d3ed391). The tests' RSS thresholds were already ASAN-aware but their clock budgets were fixed release-build numbers (the 100k-iterationinspect()loop takes ~13s on the asan lane against a 10s budget; the 100k-iterationimport()fixture overruns 30s). The budgets now scale under ASAN or debug builds, theimport()fixture (which ran 10x more iterations than its CJS sibling) takes its iteration count from the driving test with a proportionally scaled threshold, and the four standalone fixtures now detect ASAN by asking the runtime (asharness.isASANdoes) instead of sniffing the executable name, which missed debug+ASAN builds. Release builds keep the same iteration counts, thresholds and budgets as before.Verification
On this diff rebuilt against current main, under the ASAN debug build:
cargo test --doc -p bun_jsc: the doctest passes, and fails withfrom_bytesreintroduced.test/js/web/workers/worker-terminate-lifetime.test.ts(the zero-length readFile drop path, which exercisesDrop->destroy()on an unconverted empty buffer): pass.test/js/bun/terminal/terminal.test.ts(98 pass),test/js/bun/spawn/spawnSync.test.ts(14 pass),spawn-empty-arrayBufferOrBlob.test.tsand thespawn.test.tsstdout-read case (PipeReader/Readable handoffs),test/js/node/fs/fs.test.ts-t readFile(22),-t encoding(39, includesreaddirwithencoding: "buffer"),-t readlink(10): all pass.cargo fmt --allclean.History: replaces #31986; re-targeted onto current main after main independently picked up the
&mut selfhandoffs, theDropimpl, the empty-box ownership rule, and the doc-comment fences that earlier revisions of this PR carried, so the diff is now just the constructor swap, the caller migration, and the handle neutralization. Rebased again onto main at 07d38c1 (two linear commits); the only conflict was inTerminal.rs, where main had added a landing-frame comment and switched the error arm todispatch::foldaround the same call this PR rewrites, resolved by keeping main's text with thefrom_owned_bytescall.no test proof · iteration 18 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/cli/run/require-cache.test.ts