crypto, zlib, zstd: copy resizable ArrayBuffer inputs before queuing to the threadpool

Closed
RO
2mo ago
+182-42

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_async returns an owned EncodedSlice for resizable non-shared inputs on the async path. Covers crypto.pbkdf2, crypto.scrypt, Bun.zstdCompress, Bun.zstdDecompress.
  • node:zlib processChunk copies a resizable-backed chunk once before storing it on the handle, so the chunk and any continuation writes in processCallback read stable bytes. The native CompressionStream::write also copies as defence in depth for direct handle callers.
  • JSC__JSValue__borrowBytesForOffThread returns 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