Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/jsc/JSValue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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_<T: JsClass>(self) -> Option<*mut T> {
Expand Down Expand Up @@ -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(
Expand Down
21 changes: 21 additions & 0 deletions src/jsc/bindings/bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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::JSArrayBufferView>(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
Expand Down
9 changes: 9 additions & 0 deletions src/runtime/ffi/FFIObject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,15 @@ fn ptr_(global_this: &JSGlobalObject, value: JSValue, byte_offset: Option<JSValu
return JSValue::NULL;
}

// The returned address outlives this call, so the view's storage must be
// made permanently immovable first: a FastTypedArray's vector is
// relocated by the engine itself (e.g. at DFG tier-up), which would leave
// the captured pointer dangling.
// https://github.com/oven-sh/bun/issues/32054
if !value.ensure_stable_typed_array_vector() {
return global_this.throw_out_of_memory_value();
}

let Some(array_buffer) = value.as_array_buffer(global_this) else {
return global_this.to_invalid_arguments(format_args!(
"Expected ArrayBufferView but received {:?}",
Expand Down
44 changes: 44 additions & 0 deletions test/js/bun/ffi/ffi.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,50 @@ it("read", () => {
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);
Expand Down
Loading