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);