crypto, zlib, zstd: copy resizable ArrayBuffer inputs before queuing to the threadpool
Repro
import crypto from "node:crypto";
for (let i = 0; i < 20; i++) {
const ab = new ArrayBuffer(256 * 1024, { maxByteLength: 1 << 21 });
new Uint8Array(ab).fill(0x41);
const p = new Promise(r => crypto.pbkdf2(new Uint8Array(ab), "salt", 5000, 32, "sha256", r));
ab.resize(0); // pin stops transfer(), not resize()
await p;
}
panic: Segmentation fault at address 0x78301D348000
The same shape crashes crypto.scrypt, every async node:zlib codec (deflate/gzip/inflate/gunzip/brotliCompress/brotliDecompress), Bun.zstdCompress, and Bun.zstdDecompress: each queues its threadpool job with a borrowed pointer+length into the caller's buffer. The in-flight pin blocks transfer() but not ArrayBuffer.prototype.resize(), so a shrink on the JS thread decommits the pages while the pool thread is still reading the old extent.
Fix
When the backing ArrayBuffer is resizable and non-shared, copy the bytes into the job instead of borrowing (the same approach Node.js uses for pbkdf2/scrypt). Growable SharedArrayBuffer is left alone: grow is in-place within the reserved max, so reading the old extent remains valid. Fixed-length ArrayBuffer (the common case) stays zero-copy.
Two borrow funnels and one shared helper are updated:
StringOrBuffer::from_js_maybe_asyncreturns an ownedEncodedSlicefor resizable non-shared inputs on the async path. Coverscrypto.pbkdf2,crypto.scrypt,Bun.zstdCompress,Bun.zstdDecompress.node:zlibprocessChunkcopies a resizable-backed chunk once before storing it on the handle, so the chunk and any continuation writes inprocessCallbackread stable bytes. The nativeCompressionStream::writealso copies as defence in depth for direct handle callers.JSC__JSValue__borrowBytesForOffThreadreturns the dupe-signal for resizable non-shared buffers, matching its existing FastTypedArray handling.
Scope
Async fs.write/fs.writev/fs.readv share the pin-then-borrow pattern but are intentionally left out of this PR: their threadpool side hands the borrowed range to the kernel (write(2)/pwritev(2)), and a decommitted page inside a syscall returns EFAULT rather than delivering SIGSEGV, so they are not in the crash set. That EFAULT is a correctness wart worth tightening separately.
Verification
test/js/bun/util/async-resizable-arraybuffer-input.test.ts spawns a subprocess for each of the 10 APIs, races resize(0) against the in-flight call 20 times, and compares the result byte-for-byte against a fixed-buffer baseline. All 10 subprocesses segfault on stock bun; all pass with this change.
[review] gate passed · iteration 3 · 7 files touched
fails on main (without fix)
ASAN without fix: BUILD FAILED (no junit output)
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/bun/util/async-resizable-arraybuffer-input.test.ts
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
ninja: Entering directory `/workspace/bun/build/debug'
[1/160] gen JS modules (bundle-modules)
Preprocess modules (22219ms)
Bundle modules (123ms)
Postprocesss modules (33ms)
Bundle Functions (1210ms)
Generate Code (18ms)
[23.62s] Bundled "src/js" for development
2208 kb
165 internal modules
13 native modules
90 internal functions across 19 files
[1/160] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)
nightly-2026-05-06-x86_64-unknown-linux-gnu unchanged - rustc 1.97.0-nightly (e95e73209 2026-05-05)
[2/160] pch pch/root-pch.h.hxx.pch
FAILED: pch/root-pch.h.hxx.pch
/usr/lib/llvm-21/bin/clang++ -march=haswell -O0 -g3 -gz=zstd -glldb -fsanitize=address -fno-exceptions -fno-c++-static-destructor
... (truncated)
release without fix: 1 skipped
bun test v1.4.0-canary.1 (637889fd0)
test/js/bun/util/async-resizable-arraybuffer-input.test.ts:
(skip) async native input survives ArrayBuffer.prototype.resize(0) mid-flight
0 pass
1 skip
0 fail
Ran 1 test across 1 file. [339.00ms]
__F:0:S:1
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/bun/util/async-resizable-arraybuffer-input.test.ts
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
bun test v1.4.0 (637889fd0)
test/js/bun/util/async-resizable-arraybuffer-input.test.ts:
(pass) async native input survives ArrayBuffer.prototype.resize(0) mid-flight [9087.40ms]
1 pass
0 fail
1 expect() calls
Ran 1 test across 1 file. [18.88s]
__F:0:S:0
release with fix: 1 skipped
$ bun scripts/build.ts --profile=release
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
[configured] bun-profile → bun (stripped) in 3064ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/121] gen generated_host_exports.rs
generated_host_exports.rs: 91 exports (host=3, lazy=10, generic=78, rust=0); 244 extern-C blocks audited
[2/121] gen cpp.rs (cppbind)
[3/121] gen JS modules (bundle-modules)
Preprocess modules (18178ms)
Bundle modules (143ms)
Postprocesss modules (58ms)
Bundle Functions (1842ms)
Generate Code (203ms)
[20.51s] Bundled "src/js" for production
2039 kb
165 internal modules
13 native modules
90 internal functions across 19 files
[3/121] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: comp
... (truncated)
diff hotspot
src/js/node/zlib.ts | 6 ++
src/jsc/bindings/bindings.cpp | 18 +++--
src/runtime/image/Image.rs | 12 ++-
src/runtime/node/node_zlib_binding.rs | 43 ++++++++++-
src/runtime/node/types.rs | 50 ++++++------
src/sql_jsc/mysql/MySQLValue.rs | 6 +-
.../util/async-resizable-arraybuffer-input.test.ts | 89 ++++++++++++++++++++++
7 files changed, 182 insertions(+), 42 deletions(-)
gate history · 4 passed · 1 rejected · iteration 3
evidence per changed file
file reads edits tests
src/js/node/zlib.ts 1 1 0
src/jsc/bindings/bindings.cpp 3 2 0
src/runtime/image/Image.rs 2 2 0
src/runtime/node/node_zlib_binding.rs 6 6 0
src/runtime/node/types.rs 5 5 0
src/sql_jsc/mysql/MySQLValue.rs 2 2 0
…t/js/bun/util/async-resizable-arraybuffer-input.test.ts 4 15 0