You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A native function called via Native.register that takes several Structure.ByValue arguments (each a 24-byte struct: two long fields + one pointer field — an AAPCS64 MEMORY-class aggregate, passed by hidden reference) receives corrupted/garbage data in the arguments that spill past the 8 available integer/pointer argument registers. In our reduced repro this manifests as a hard SIGSEGV inside JNA's bundled libffi's ffi_call, invoked from Java_com_sun_jna_Native_registerMethod. The same call works correctly on x86_64 Linux with an identical argument count and struct shape.
We hit this indirectly through mozilla/uniffi-rs, whose generated Kotlin bindings use Native.register and represent every non-primitive argument (records, options, strings) as a RustBuffer.ByValue struct — { capacity: u64, len: u64, data: *mut u8 }, i.e. exactly the 24-byte shape below. A UniFFI-exported Rust function/constructor with 9+ such arguments reliably crashes the JVM on Apple Silicon macOS while working fine on Linux. This report reduces that down to a minimal, uniffi-independent, pure C + JNA repro.
Reproduction
repro.c — compile with clang -dynamiclib -o libjnarepro.dylib repro.c:
Each Buf.of(tag) sets capacity = len = tag, so a correct call prints arg1 capacity=1 ... arg11 capacity=11 with no CORRUPTED markers.
Actual output (macOS arm64)
jna.version = 5.18.1 os.arch=aarch64
arg1 capacity=1 len=1
arg2 capacity=2 len=2
arg3 capacity=3 len=3
arg4 capacity=4 len=4
arg5 capacity=5 len=5
arg6 capacity=6 len=6
arg7 capacity=7 len=7
arg8 capacity=8 len=8
arg9 capacity=9 len=9
#
# A fatal error has been detected by the Java Runtime Environment:
#
# SIGSEGV (0xb) at pc=0x00000001021244f8, pid=41852, tid=5123
# ...
# Problematic frame:
# C [libjnarepro.dylib+0x4f8] dump11+0xe8
Native frames from the hs_err file:
C [libjnarepro.dylib+0x4f8] dump11+0xe8
C [jna...tmp+0x1004c] ffi_prep_closure_loc+0x1970
C [jna...tmp+0xe64c] ffi_call+0x584
C [jna...tmp+0xae3c] Java_com_sun_jna_Native_registerMethod+0xe8c
C [jna...tmp+0xeb48] ffi_prep_closure_loc+0x46c
C [jna...tmp+0x101d4] ffi_prep_closure_loc+0x1af8
j Repro$DirectLib.dump11(...)J+0
j Repro.main([Ljava/lang/String;)V+108
Arguments 1–9 (which fit in the 8 available AAPCS64 integer/pointer argument registers plus one that's actually classified as taken by the hidden return-value/indirect-result register — the exact boundary varies slightly by call shape) print correctly. The crash occurs while the callee reads argument 10 — the second argument that had to spill onto the stack — meaning the pointer JNA/libffi placed on the outgoing call's stack for that argument is invalid. The fault is inside JNA's own bundled libffi's ffi_call, not in the generated dump11 code or in Java-level marshalling.
Reducing further:
With the trailing CallStatus * (by-reference) argument removed entirely — just 9 or 11 Structure.ByValue args and nothing else — we saw no corruption at all in our testing; every argument came back with its expected tag. The corruption/crash only appeared once a trailing struct-by-reference out-parameter was added after the Structure.ByValue run, which is the shape every uniffi-generated call actually uses (a trailing RustCallStatus* out-param). That suggests the bug is specifically in how libffi computes/advances the stack offset once a run of stack-spilled Structure.ByValue arguments is followed by a differently-marshalled (by-reference) argument, not simply "many Structure.ByValue args."
An identical call shape using 11 plain long arguments instead of Structure.ByValue args (still followed by the trailing by-reference CallStatus) does not crash or corrupt.
I have not yet run this exact minimal repro on x86_64 Linux myself, but the originating uniffi-rs-generated code (same argument count and struct shape, called the same way) passes reliably in our Linux x86_64 CI and only fails on macOS arm64 — happy to run the minimal repro on Linux too if that's useful before this is investigated.
Expected behavior
Structure.ByValue arguments passed via Native.register should marshal correctly regardless of how many of them are needed to spill from registers onto the outgoing call's stack, on all supported architectures including macOS arm64.
Notes / related issues
This looks related in spirit to #1259 ("Passing structs by value on the stack is incorrect on arm64"), but is not obviously the same bug: #1259's repro struct is 8 bytes (a single int64_t field), which AAPCS64 classifies as a register-class/INTEGER aggregate passed directly in one register or one stack slot. The struct here is 24 bytes, which AAPCS64 always classifies as MEMORY-class and passes by hidden pointer regardless of position — a different code path in libffi's arm64 support. #1259 was reported fixed upstream as a side effect of an unrelated libffi refactor; this issue reproduces on the current release (5.19.1), so if it is the same underlying class of bug, that fix does not cover this code path.
It may also be related to the struct-by-value/callback crashes on Darwin arm64 acknowledged in #1238 and tracked in #1323 (partially addressed for direct callbacks in 5.11.0 per the changelog), though those predate the current libffi bundled in 5.18.1+ and it's unclear whether they cover plain (non-callback) direct-mapped function arguments.
Note
AI was used to analyze this bug and create a minimal test case.
Environment
aarch64(Apple Silicon,Mac15,11)Native.register(direct mapping), notNative.load/interface mappingSummary
A native function called via
Native.registerthat takes severalStructure.ByValuearguments (each a 24-byte struct: twolongfields + one pointer field — an AAPCS64 MEMORY-class aggregate, passed by hidden reference) receives corrupted/garbage data in the arguments that spill past the 8 available integer/pointer argument registers. In our reduced repro this manifests as a hard SIGSEGV inside JNA's bundledlibffi'sffi_call, invoked fromJava_com_sun_jna_Native_registerMethod. The same call works correctly on x86_64 Linux with an identical argument count and struct shape.We hit this indirectly through mozilla/uniffi-rs, whose generated Kotlin bindings use
Native.registerand represent every non-primitive argument (records, options, strings) as aRustBuffer.ByValuestruct —{ capacity: u64, len: u64, data: *mut u8 }, i.e. exactly the 24-byte shape below. A UniFFI-exported Rust function/constructor with 9+ such arguments reliably crashes the JVM on Apple Silicon macOS while working fine on Linux. This report reduces that down to a minimal, uniffi-independent, pure C + JNA repro.Reproduction
repro.c— compile withclang -dynamiclib -o libjnarepro.dylib repro.c:Repro.java— compile/run with the JNA jar on the classpath,-Djna.library.path=.pointing at the directory containinglibjnarepro.dylib:Each
Buf.of(tag)setscapacity = len = tag, so a correct call printsarg1 capacity=1 ... arg11 capacity=11with noCORRUPTEDmarkers.Actual output (macOS arm64)
Native frames from the
hs_errfile:Arguments 1–9 (which fit in the 8 available AAPCS64 integer/pointer argument registers plus one that's actually classified as taken by the hidden return-value/indirect-result register — the exact boundary varies slightly by call shape) print correctly. The crash occurs while the callee reads argument 10 — the second argument that had to spill onto the stack — meaning the pointer JNA/libffi placed on the outgoing call's stack for that argument is invalid. The fault is inside JNA's own bundled
libffi'sffi_call, not in the generateddump11code or in Java-level marshalling.Reducing further:
CallStatus *(by-reference) argument removed entirely — just 9 or 11Structure.ByValueargs and nothing else — we saw no corruption at all in our testing; every argument came back with its expected tag. The corruption/crash only appeared once a trailing struct-by-reference out-parameter was added after theStructure.ByValuerun, which is the shape every uniffi-generated call actually uses (a trailingRustCallStatus*out-param). That suggests the bug is specifically in how libffi computes/advances the stack offset once a run of stack-spilledStructure.ByValuearguments is followed by a differently-marshalled (by-reference) argument, not simply "manyStructure.ByValueargs."longarguments instead ofStructure.ByValueargs (still followed by the trailing by-referenceCallStatus) does not crash or corrupt.Expected behavior
Structure.ByValuearguments passed viaNative.registershould marshal correctly regardless of how many of them are needed to spill from registers onto the outgoing call's stack, on all supported architectures including macOS arm64.Notes / related issues
This looks related in spirit to #1259 ("Passing structs by value on the stack is incorrect on arm64"), but is not obviously the same bug: #1259's repro struct is 8 bytes (a single
int64_tfield), which AAPCS64 classifies as a register-class/INTEGER aggregate passed directly in one register or one stack slot. The struct here is 24 bytes, which AAPCS64 always classifies as MEMORY-class and passes by hidden pointer regardless of position — a different code path in libffi's arm64 support. #1259 was reported fixed upstream as a side effect of an unrelated libffi refactor; this issue reproduces on the current release (5.19.1), so if it is the same underlying class of bug, that fix does not cover this code path.It may also be related to the struct-by-value/callback crashes on Darwin arm64 acknowledged in #1238 and tracked in #1323 (partially addressed for direct callbacks in 5.11.0 per the changelog), though those predate the current libffi bundled in 5.18.1+ and it's unclear whether they cover plain (non-callback) direct-mapped function arguments.