bun:ffi: make lib.symbols.<fn>.ptr a numeric native address - #34008
bun:ffi: make lib.symbols.<fn>.ptr a numeric native address#34008robobun wants to merge 2 commits into
Conversation
The .ptr field on dlopen/linkSymbols/cc symbol functions was produced via std::bit_cast<double>(functionPointer), which reinterprets the pointer bits as IEEE-754 bits and yields a denormal (~6.8e-310) instead of an integer address. Passing that to CFunction/linkSymbols truncates to 0 and throws 'missing a "ptr" field'. It was also the wrong pointer: functionPointer is the TCC-compiled JSC-calling-convention trampoline, not the native symbol. Expose symbolFromDynamicLibrary (the dlsym'd / user-provided / cc-compiled native function) encoded the same way as every other bun:ffi pointer (PTR_TO_JSVALUE / JSValue::from_ptr_address), so .ptr -> CFunction round-trips.
WalkthroughChangesFFI pointer round-trip
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
|
Updated 1:04 AM PT - Jul 12th, 2026
❌ @autofix-ci[bot], your commit 741876f has 2 failures in
🧪 To try this PR locally: bunx bun-pr 34008That installs a local version of the PR into your bun-34008 --bun |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@test/js/bun/ffi/ffi.test.js`:
- Around line 730-744: Make the child-process FFI setup and invocation
exception-safe by wrapping the code using linkSymbols, CFunction, and the
callback in try/finally blocks. Ensure rewrapped, lib, and cb are each closed
when successfully created, including when setup or invocation throws, while
preserving the existing logging and call behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 455704f1-808c-4990-85e3-e493abcfeabd
📒 Files selected for processing (2)
src/jsc/bindings/JSFFIFunction.cpptest/js/bun/ffi/ffi.test.js
There was a problem hiding this comment.
LGTM — one-line encoding fix that makes .ptr a usable numeric address instead of a denormal double.
What was reviewed:
- Verified the new encoding (
jsNumber((double)(uintptr_t)ptr)) matchesPTR_TO_JSVALUEinFFI.handJSValue::from_ptr_addressinJSValue.rs. - Confirmed all three
add_ptr_property=truecallers inffi_body.rsalready passsymbol_from_dynamic_library, so switching from the JSC trampoline to the native symbol is safe and requires no signature changes. - Checked
src/js/bun/ffi.ts—wrap.ptr = functionToCall.ptrand.native = functionToCallboth flow from this field, consistent with the test'ssymPtr === nativePtr === cbPtrassertion.
Extended reasoning...
Overview
Single functional line changed in src/jsc/bindings/JSFFIFunction.cpp: the .ptr property on bun:ffi symbol functions was previously jsNumber(std::bit_cast<double>(functionPointer)), which reinterprets the pointer's raw bits as an IEEE-754 double — for any userspace address this yields a denormal in the e-310/e-311 range that truncates to 0 in as_ptr_address(). It also exposed the wrong pointer (the JSC-calling-convention trampoline, not the native symbol). The fix exposes symbolFromDynamicLibrary encoded as jsNumber((double)(uintptr_t)addr), matching the encoding used everywhere else in bun:ffi (PTR_TO_JSVALUE, from_ptr_address, JSCallback.ptr, read.ptr). A new subprocess test in ffi.test.js verifies the integer encoding, equality with the input JSCallback.ptr, and a functional round-trip through CFunction.
Security risks
None. This exposes a native address the caller already has (they passed it via linkSymbols({ptr}) or it's the dlsym result of a library they explicitly loaded). No new capability is granted — the same address was already reachable via JSCallback.ptr and via calling the wrapped function. The previous bit_cast value leaked the trampoline address anyway, just in an unusable encoding.
Level of scrutiny
Low-to-medium. bun:ffi is native/unsafe territory, but this is a pure value-encoding fix on a read-only property, not a change to any call/marshalling path. The old value was provably unusable (denormal → 0 → "missing ptr field" throw), so nothing could have depended on it. I traced all call sites: the three FFI-path callers in ffi_body.rs:1223/1585/1681 all pass add_ptr_property=true with a populated symbol_from_dynamic_library; the non-FFI callers (hw_exports.rs, node_os.rs, generated bindings) pass add_ptr_property=false and don't hit this branch. The JS wrapper in src/js/bun/ffi.ts:410 copies this field onto the outer wrapper unchanged.
Other factors
The test follows the file's established subprocess pattern (matching the neighboring JSCallback exceptions propagate test), drains both pipes concurrently, and asserts a combined {parsed, call, stderr, exitCode} object per repo convention. CodeRabbit's try/finally suggestion was withdrawn after the author explained the subprocess boundary makes it unnecessary. The jsNumber(double) vs jsDoubleNumber distinction (int32 fast path) doesn't matter here since as_ptr_address() = as_number() as usize handles both encodings, and 64-bit userspace addresses exceed int32 range anyway.
|
CI status: the only hard failure in #72077 is Ready for review. |
|
Closing this since #35246 (bun:ffi: use the engine-native FFI when available) merged and covers the same ground. Thank you @robobun for the PR — if there's a piece of this that #35246 didn't pick up, please say so and we'll take another look. (This comment was written by Claude, on behalf of the Bun team.) |
|
Confirmed #35246 fixes One piece it didn't pick up: import { cc, CFunction } from "bun:ffi";
// /tmp/a.c: int add2(int a, int b){ return a + b; }
const lib = cc({ source: "/tmp/a.c", symbols: { add2: { args: ["int","int"], returns: "int" } } });
console.log(lib.symbols.add2.ptr);
// 1.5046817921187e-311
new CFunction({ ptr: lib.symbols.add2.ptr, args: ["int","int"], returns: "int" });
// TypeError: Symbol "CFunction" is missing a "ptr" fieldThe one-line fix in this PR still applies there (it's the only |
Reproduction
Same with
dlopen:lib.symbols.fn.ptris always a denormal in thee-310/e-311range, while every otherbun:ffipointer producer (ptr(),JSCallback.ptr,read.ptr) returns a plain integer address. The documentedsymbols.fn.ptr->CFunction/linkSymbolsround-trip can never have worked on this representation: the denormal truncates to 0 in the consumer'sas_ptr_address()and the constructor throws.Cause
Bun__CreateFFIFunctionValueinsrc/jsc/bindings/JSFFIFunction.cppstoredJSC::jsNumber(std::bit_cast<double>(functionPointer)): the pointer's raw bits reinterpreted as IEEE-754 bits, which for any userspace address is a denormal. It was also the wrong pointer:functionPointeris the TinyCC-compiled trampoline with JSC calling convention ((JSGlobalObject*, CallFrame*) -> EncodedJSValue), not something aCFunctionwrapper could ever call with the declared native signature.Fix
Expose
symbolFromDynamicLibrary(the dlsym'd / user-provided / cc-compiled native function, which everyaddPtrFieldcaller already passes), encoded asjsNumber((double)(uintptr_t)addr), matchingPTR_TO_JSVALUEinFFI.handJSValue::from_ptr_addresseverywhere else.Verification
New test in
test/js/bun/ffi/ffi.test.jsbuilds alinkSymbolswrapper around aJSCallback, assertssymbols.add2.ptr === cb.ptr(integer), then feeds that.ptrback throughCFunctionand calls it. On main the test fails with the denormal value and the "missing a ptr field" throw; with the fix it passes, anddlopen("libc.so.6").symbols.abs.ptrround-trips throughCFunctiontoabs(-7) == 7. The rest oftest/js/bun/ffi/passes unchanged.no test proof · iteration 0 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/bun/ffi/ffi.test.js