Repository navigation
Description
Summary
bun:ffi cannot invoke a C function whose signature takes a struct by value
on Linux x64. Any such call — including the primary query API of mainstream
libraries like DuckDB — segfaults at address 0x0 when declared through
dlopen().
This is the second of the two FFI bugs in #29346. The first (number as
'ptr' argument corruption, Fix 1) was resolved in v1.3.3 / #25045. The
second (struct-by-value, Fix 2) is still broken in v1.3.12 — re-verified
today, Apr 18, 2026.
Related: #6139 (enhancement, add ctypes-style struct construction — open
since Sep 2023). This issue is narrower and more immediate: existing C
functions with struct-by-value signatures should be callable through FFI
at all, regardless of whether Bun ever grows struct construction.
Current status
| Target | Result |
|---|---|
Bun v1.3.12 (700fc117), Linux x64, kernel 6.17.0, glibc 2.39 |
Segfault at 0x0 |
| Bun v1.3.12, macOS ARM64 | Works — by ABI accident (AAPCS64 passes large structs via hidden pointer, matches 'ptr') |
| Deno FFI, Linux x64 | Works — structs-by-value added in denoland/deno#15060 (2023) |
Minimal reproduction
// test-lib.c — gcc -shared -fPIC -o libtest.so test-lib.c
#include <stdint.h>
typedef struct {
uint64_t a, b, c, d, e, f; // 48 bytes → MEMORY class on SysV AMD64
} big_struct;
uint64_t sum_struct(big_struct s) {
return s.a + s.b + s.c + s.d + s.e + s.f;
}// repro.mjs
import { dlopen, ptr } from 'bun:ffi';
// bun:ffi has no type that expresses "struct by value". 'ptr' is the
// closest approximation and happens to work on ARM64 by ABI accident.
const { symbols } = dlopen('./libtest.so', {
sum_struct: { args: ['ptr'], returns: 'u64' },
});
const buf = new BigUint64Array([1n, 2n, 3n, 4n, 5n, 6n]);
console.log(symbols.sum_struct(ptr(buf))); // expected: 21nOn Linux x64:
============================================================
Bun v1.3.12 (700fc117) Linux x64
Linux Kernel v6.17.0 | glibc v2.39
panic(main thread): Segmentation fault at address 0x0
On macOS ARM64: 21n.
Real-world impact
DuckDB — a mainstream analytics database used in production across many
projects — has three core chunk-fetching functions that take the 48-byte
duckdb_result by value:
duckdb_fetch_chunk(duckdb_result)— primary chunk-based query APIduckdb_result_get_chunk(duckdb_result, idx_t)duckdb_result_chunk_count(duckdb_result)
Any Bun FFI binding for DuckDB on Linux requires shipping a separate C
shim library that wraps these with struct * signatures. Our production
workaround is live here:
- Commit: shreeve/rip-lang@5f29011
- Shim source: https://github.com/shreeve/rip-lang/blob/main/packages/db/lib/duckdb-shim.c
- Driver call site: https://github.com/shreeve/rip-lang/blob/main/packages/db/lib/duckdb.mjs#L181
We re-verified today (Apr 18, 2026) that this workaround is still required:
parking the shim .so outside its search path causes an immediate null-deref
segfault; restoring it returns to 99/99 FFI tests passing. Raw output from
the experiment in the comment below.
Struct-by-value is the default for "return a small result with error detail"
in idiomatic C API design. This pattern is common enough that every mature
FFI layer handles it — Python's ctypes, Deno's FFI, Node's node-ffi-napi,
Rust's libloading. bun:ffi is the outlier.
Proposed minimum fix
Even without full composite-struct support (#6139), Bun could accept a
"struct by size + alignment" descriptor so the marshaler can lay the
argument out per-ABI:
dlopen('./libtest.so', {
sum_struct: {
args: [{ type: 'struct', size: 48, align: 8 }],
returns: 'u64',
},
});The caller supplies a pointer or a Uint8Array/BigUint64Array of the
right size; Bun copies it into the argument location expected by the ABI
(stack for MEMORY-class on SysV AMD64, registers for smaller cases, X8 for
hidden pointer on AAPCS64). Full ctypes-style field descriptors can
follow later — this unblocks real bindings immediately.
Environment
Bun v1.3.12 (700fc117) Linux x64
Linux Kernel v6.17.0 | glibc v2.39
CPU: sse42 popcnt avx avx2 avx512
DuckDB 1.5.2
Activity
shreeve commentedon Apr 18, 2026
Raw output from today's re-verification (Apr 18, 2026), for the record.
Setup: Fresh Linux x64 environment, Bun 1.3.12, DuckDB 1.5.2 installed at
/home/shreeve/.duckdb/1.5.2/libduckdb.so, shim available at/home/shreeve/.duckdb/1.5.2/libduckdb-shim.so.With shim in place (fallback not taken):
Shim parked outside the search path (forces direct FFI call to
duckdb_fetch_chunk):Crashes mid-suite on the first query that triggers
duckdb_fetch_chunk. Same signature on the standalone 100k-row stress query:Restoring the shim returns to 99/99 passing with no code change:
The shim is genuinely load-bearing on Bun 1.3.12 + Linux x64. Happy to rerun on a future release when this is addressed.
github-actions commentedon Apr 18, 2026
Found 1 possible duplicate issue:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
shreeve commentedon Apr 18, 2026
Thanks for the check, but #21441 and this issue are sister bugs of the same root cause, not duplicates. They hit different ABI positions and different code paths in the FFI marshaler.
#21441 — large struct return values. Function signature like
Foo Bar(...)whereFoois 32 bytes. The caller-side ABI allocates a hidden out-pointer (SRET slot on SysV AMD64 /X8on AAPCS64); the callee writes the struct there andRAX/X0is the same out-pointer. Bun'sreturns: 'ptr'doesn't describe this, so the caller reads whatever happens to be in the return register while the real data landed in the out-slot. Symptom: corrupted data, no crash.#29464 (this issue) — large struct arguments. Function signature like
duckdb_fetch_chunk(duckdb_result)where the struct is 48 bytes. SysV AMD64 classifies this as MEMORY and requires the caller to copy the struct onto the stack before the call; Bun'sargs: ['ptr']puts a pointer in a register instead, so the callee reads garbage from the stack location the ABI expects. Symptom: hard null-deref segfault.Different ABI rules, different marshaler paths, different symptoms. Fixing the SRET-return case doesn't fix the MEMORY-class argument case, and vice versa — though a single struct-descriptor API in
dlopen()could address both at once.Both bugs are subordinate to #6139 as the umbrella feature. Keeping them as sibling issues rather than merging them means the argument-side and return-side fixes can land independently, and anyone searching for "FFI crash passing struct" vs "FFI garbage returning struct" lands on the right bug.
Requesting this stay open.
Jarred-Sumner commentedon Apr 18, 2026
Not supported yet
Mte90 commentedon May 6, 2026
I think that is the same issue on #18742
That is generating issues with OpenCode only on linux 64 (but some old reports are also for OSX) so right now is unusable.
gretel commentedon May 6, 2026
ran into this just yesterday on macos. regards