node:fs: async calls with a Buffer path no longer keep the Buffer alive forever
What does this PR do?
await fs.promises.stat(Buffer.from(path)) (and every other async node:fs call given a Buffer/TypedArray path or data argument) left the Buffer GC-rooted for the life of the process: argument parsing protect()ed it and never released that protect (the argument slice is ManuallyDrop'd on the success path), and the work-pool hand-off (make_thread_isolated) protected it a second time; only the second protect was released when the operation completed. 400 fs.promises.stat(buf) calls leave 400 uncollectable Uint8Arrays on 1.4; the sync variants were unaffected.
Now there is one RAII type for a JS buffer held across a call, bun_jsc::PinnedArrayBuffer: ::pin pins the backing store (sync, stack-scoped uses), ::root additionally GC-roots the cell (work-pool jobs); Drop releases what was taken. The async parsers return the parsed arguments already wrapped as ThreadIsolated<T> (FsArgument::from_js_async, StringOrBuffer::from_js_async, PBKDF2/Scrypt::from_js_async; owned-only constructors like args::Stat::owned for internal callers), so "safe to hand to the pool" is a type-level fact with a crate-private constructor — no unsafe at any call site — and the args simply drop when the operation completes or is rejected before scheduling: exactly one root, one release. Removed in favour of this: the Unprotect trait and its impls, make_thread_isolated/into_thread_isolated, MarkedArrayBuffer's pinned/protected bookkeeping, ArgumentsSlice's protect-tracking and the ManuallyDrop around it, ArrayBufferStrong, the Stdio::ArrayBuffer variant (the shell's > ${buf} targets now travel beside the stdio triple, so no consumer needs an unreachable arm), and the ad-hoc pin guards in the shell, CompressionStream and Bun.markdown. Still zero-copy; sync calls do not gain a GC protect (they lose one: 3 FFI calls instead of 5 for a Buffer path).
Other visible effects: Bun.markdown.* on a detached view renders empty input instead of throwing "Out of memory"; if materializing a small typed array's ArrayBuffer for pinning fails, async fs/shell-redirect now throw an out-of-memory error instead of continuing unpinned; an S3Client/Bun.file("s3://…") TypedArray key is copied into the store instead of rooting the JS buffer forever.
Bun.file(new Uint8Array(...)) has a similar unreleased protect on the Blob store's path; that is a separate change (the store can be dropped inside a GC finalizer or on another thread, so it needs a different release mechanism) and is not touched here.
How did you verify your code works?
New test in test/js/node/fs/fs.test.ts (stat › "async calls do not keep a Buffer path alive after they complete"): 200× stat/readFile/writeFile/rejected-stat with Buffer paths, GC, count live Uint8Arrays — 803 on 1.4, <10 with this change. Also probed appendFile/write/read/readv/writev, zlib, zstd, pbkdf2, scrypt, Transpiler.transform and shell > ${buf} the same way (flat before and after). Drove the debug build through async stat/readFile/writeFile with Buffer path and data, a rejected call (ENOENT) and an aborted readFile — all complete, 3 Uint8Arrays live afterwards. Existing stat tests pass.