From 1571a7258deec0e64dac9202ffdf661f94b43b76 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 10 Jun 2026 08:29:31 +0000 Subject: [PATCH 1/2] bun:ffi: pin a FastTypedArray's storage before ptr() captures its address ptr(typedArray) returned the raw vector address of the view without forcing the view out of FastTypedArray mode. JSC relocates that vector when the view transitions to wasteful mode, and the engine triggers the transition on its own: DFG tier-up folds the view into compiled code and registers an ArrayBufferView watchpoint, whose installation calls possiblySharedBuffer() (slowDownAndWasteMemory copies the storage into a fresh ArrayBuffer and repoints m_vector). From that moment the captured pointer dangles: native writes land in the abandoned allocation while JS reads the new one. ptr() now forces the one-time fast-to-wasteful transition before reading the address, so the pointer it hands out can never be invalidated by the engine. Other storage modes already have immovable data and are left untouched. Fixes #32054 --- src/jsc/JSValue.rs | 12 ++++++++++ src/jsc/bindings/bindings.cpp | 21 +++++++++++++++++ src/runtime/ffi/FFIObject.rs | 9 +++++++ test/js/bun/ffi/ffi.test.js | 44 +++++++++++++++++++++++++++++++++++ 4 files changed, 86 insertions(+) diff --git a/src/jsc/JSValue.rs b/src/jsc/JSValue.rs index 033bc3c364f9..a441dd71e3e4 100644 --- a/src/jsc/JSValue.rs +++ b/src/jsc/JSValue.rs @@ -959,6 +959,17 @@ impl JSValue { } self.as_array_buffer(global) } + /// If `self` is a `FastTypedArray` (small view whose vector lives in the + /// GC heap), force it into wasteful mode so the engine can never relocate + /// its data. JSC moves such vectors on its own, e.g. DFG tier-up + /// registers an ArrayBufferView watchpoint whose installation calls + /// `possiblySharedBuffer()` and repoints the vector. Must be called + /// before capturing the data pointer for use beyond the current native + /// call. No-op for every other storage mode (their data never moves). + /// Returns `false` on allocation failure. + pub fn ensure_stable_typed_array_vector(self) -> bool { + JSC__JSValue__ensureStableTypedArrayVector(self) + } /// Generic downcast. Dispatches via [`JsClass::from_js`]. #[inline] pub fn as_(self) -> Option<*mut T> { @@ -2075,6 +2086,7 @@ unsafe extern "C" { out: &mut ArrayBuffer, ) -> bool; safe fn JSC__JSValue__pinArrayBuffer(this: JSValue) -> bool; + safe fn JSC__JSValue__ensureStableTypedArrayVector(this: JSValue) -> bool; safe fn JSC__JSValue__asPromise(this: JSValue) -> *mut JSPromise; safe fn JSC__JSValue__asInternalPromise(this: JSValue) -> *mut JSInternalPromise; safe fn Bun__attachAsyncStackFromPromise( diff --git a/src/jsc/bindings/bindings.cpp b/src/jsc/bindings/bindings.cpp index 1f0a29596548..6af03a5418fd 100644 --- a/src/jsc/bindings/bindings.cpp +++ b/src/jsc/bindings/bindings.cpp @@ -3381,6 +3381,27 @@ CPP_DECL void JSC__JSValue__unpinArrayBuffer(JSC::EncodedJSValue v) } } +// Make a view's data pointer permanently stable before handing the raw +// address out for use beyond the current call (FFI.ptr). A FastTypedArray's +// vector lives in the GC heap and is RELOCATED when the view transitions to +// wasteful mode (slowDownAndWasteMemory copies into a fresh ArrayBuffer and +// repoints m_vector), and the engine triggers that transition on its own: +// DFG tier-up folds the view into compiled code and registers an +// ArrayBufferView watchpoint, whose installation calls +// possiblySharedBuffer() (ArrayBufferViewWatchpointAdaptor::add in +// DFGDesiredWatchpoints.cpp). Forcing the transition up front means the +// address we are about to capture can never be invalidated. Every other mode +// already has stable storage: Oversize is fastMalloc'd and adopted in place, +// Wasteful/DataView already have an ArrayBuffer, and JSArrayBuffer data never +// moves. Returns false only if the transition failed (allocation failure). +CPP_DECL bool JSC__JSValue__ensureStableTypedArrayVector(JSC::EncodedJSValue v) +{ + auto* view = dynamicDowncast(JSC::JSValue::decode(v)); + if (!view || view->mode() != JSC::FastTypedArray) + return true; + return !!view->possiblySharedBuffer(); +} + // Borrow `v`'s byte storage for off-thread reading. Splits out only the // `FastTypedArray` case from `pinArrayBuffer`, because that's the one mode // where `possiblySharedBuffer()` actually COPIES data diff --git a/src/runtime/ffi/FFIObject.rs b/src/runtime/ffi/FFIObject.rs index e96fda5b76c2..4b78ac49dd3c 100644 --- a/src/runtime/ffi/FFIObject.rs +++ b/src/runtime/ffi/FFIObject.rs @@ -428,6 +428,15 @@ fn ptr_(global_this: &JSGlobalObject, value: JSValue, byte_offset: Option { delete globalThis.buffer; }); +// https://github.com/oven-sh/bun/issues/32054 +it("ptr(typedArray) stays valid after DFG tier-up", async () => { + const code = ` + import { ptr, read } from "bun:ffi"; + const out = new Float64Array(1); + out[0] = 1.5; + const outPtr = ptr(out); + function main() { + let sum = 0; + for (let i = 0; i < 10_000; i++) { + sum += out[0]; + } + return sum; + } + main(); + if (ptr(out) !== outPtr) { + console.log("MOVED: the view's storage was relocated after ptr() was taken"); + process.exit(1); + } + out[0] = 42.5; + if (read.f64(outPtr) !== 42.5) { + console.log("STALE: write through the view is not visible at the captured address"); + process.exit(1); + } + console.log("STABLE"); + `; + await using proc = Bun.spawn({ + cmd: [bunExe(), "-e", code], + env: { + ...bunEnv, + // Force deterministic, early tier-up so the loop above reliably + // DFG-compiles (which registers an ArrayBufferView watchpoint on the + // folded view; registration relocates a FastTypedArray's vector). + BUN_JSC_useConcurrentJIT: "false", + BUN_JSC_thresholdForOptimizeAfterWarmUp: "100", + BUN_JSC_thresholdForOptimizeSoon: "100", + }, + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stdout.trim()).toBe("STABLE"); + expect(exitCode).toBe(0); +}); + if (ok) { describe("run ffi", () => { ffiRunner(false); From 87174b51c4919d9d8f751c00a4200969411ea1a6 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 28 Jun 2026 12:14:28 +0000 Subject: [PATCH 2/2] ci: retrigger