Native.register (direct mapping) corrupts/crashes on stack-spilled Structure.ByValue arguments on macOS arm64
Chưa có ai nhận issue này.
Đánh giá
- Độ khó
- 4/5
- Thời gian dự kiến
- 3-5 ngày
- Mức phù hợp với người mới
- 45/100
Hướng nghiên cứu
Tái hiện lỗi bằng repro.c và Repro.java trên macOS arm64, sau đó lần theo Native_registerMethod đến đường dẫn ffi_call của libffi đi kèm dành cho các đối số Structure.ByValue bị tràn ra stack. Được xem là hoàn tất khi lệnh gọi hoàn thành mà không bị hỏng dữ liệu hoặc SIGSEGV và in ra các giá trị capacity và len mong đợi cho cả 11 đối số, bao gồm cả con trỏ CallStatus ở cuối.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
[!NOTE]
AI was used to analyze this bug and create a minimal test case.
Environment
- JNA version: 5.18.1 (also reproduces on 5.19.1, the latest release as of this report)
- OS: macOS 26.5.2 (25F84), Darwin 25.5.0
- Arch:
aarch64(Apple Silicon,Mac15,11) - JDK: OpenJDK 17.0.16 (Homebrew build) — also reproduces on OpenJDK 26.0.1
- Invocation style:
Native.register(direct mapping), notNative.load/interface mapping
Summary
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:
#include <stdint.h>
#include <string.h>
#include <stdio.h>
typedef struct {
uint64_t capacity;
uint64_t len;
void *data;
} Buf; /* 24 bytes: MEMORY-class aggregate on AAPCS64, passed by hidden pointer */
typedef struct {
int8_t code;
Buf error_buf;
} CallStatus;
uint64_t dump11(Buf a1, Buf a2, Buf a3, Buf a4, Buf a5, Buf a6, Buf a7, Buf a8,
Buf a9, Buf a10, Buf a11, CallStatus *out_status) {
Buf *args[11] = {&a1,&a2,&a3,&a4,&a5,&a6,&a7,&a8,&a9,&a10,&a11};
for (int i = 0; i < 11; i++) {
fprintf(stdout, "arg%d capacity=%llu len=%llu%s\n", i + 1,
(unsigned long long)args[i]->capacity,
(unsigned long long)args[i]->len,
args[i]->capacity == (uint64_t)(i + 1) ? "" : " <-- CORRUPTED");
}
memset(out_status, 0, sizeof(*out_status));
return 0;
}
Repro.java — compile/run with the JNA jar on the classpath, -Djna.library.path=. pointing at the directory containing libjnarepro.dylib:
import com.sun.jna.Native;
import com.sun.jna.Structure;
public class Repro {
@Structure.FieldOrder({"capacity", "len", "data"})
public static class Buf extends Structure {
public long capacity;
public long len;
public com.sun.jna.Pointer data;
public static class ByValue extends Buf implements Structure.ByValue {}
static ByValue of(long tag) {
ByValue b = new ByValue();
b.capacity = tag;
b.len = tag;
b.data = null;
return b;
}
}
@Structure.FieldOrder({"code", "error_buf"})
public static class CallStatus extends Structure {
public byte code;
public Buf error_buf;
}
static class DirectLib {
static {
Native.register(DirectLib.class, "jnarepro");
}
static native long dump11(Buf.ByValue a1, Buf.ByValue a2, Buf.ByValue a3, Buf.ByValue a4,
Buf.ByValue a5, Buf.ByValue a6, Buf.ByValue a7, Buf.ByValue a8,
Buf.ByValue a9, Buf.ByValue a10, Buf.ByValue a11, CallStatus outStatus);
}
public static void main(String[] args) {
System.out.println("jna.version = " + Native.VERSION + " os.arch=" + System.getProperty("os.arch"));
CallStatus s = new CallStatus();
DirectLib.dump11(
Buf.of(1), Buf.of(2), Buf.of(3), Buf.of(4), Buf.of(5),
Buf.of(6), Buf.of(7), Buf.of(8), Buf.of(9), Buf.of(10), Buf.of(11), s);
}
}
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 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." - An identical call shape using 11 plain
longarguments instead ofStructure.ByValueargs (still followed by the trailing by-referenceCallStatus) 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.
- Ngôn ngữ chính
- Java
- Star
- 8.9k
- Fork
- 1.7k
- Merge trung bình
- 1 ngày 13 giờ
- Pull request đã merge (30 ngày)
- 1
Chuẩn bị môi trường
Dự án này không cung cấp dev container, Dockerfile hay hướng dẫn đóng góp, nên bạn cần tự thiết lập môi trường: hãy bắt đầu từ README và xem hướng dẫn đóng góp lần đầu của chúng tôi để biết các bước chung.
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của java-native-access/jna
-
Độ khó 4/5 1-2 ngày Mức phù hợp với người mới 48/100
java-native-access/jna#1742 ·
-
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 66/100
java-native-access/jna#1738 ·
-
Độ khó 3/5 1-2 ngày Mức phù hợp với người mới 65/100
java-native-access/jna#1717 ·
-
Windows32Exception - The parameter is incorrectCó thể đã có người làm @marktech0813 đã nhận 336 ngày trước. Đang mở
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 35/100
java-native-access/jna#1700 · 11 bình luận ·
-
feature request
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 35/100
java-native-access/jna#1698 · 1 bình luận ·
Tất cả issue của java-native-access/jna
Issue tương tự
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 77/100
apache/rocketmq-dashboard#6610 ·
Maintainer thường phản hồi trong vòng 3 ngày
-
Citationcount column sorts alphabetically instead of numericallyCó thể đã có người làm @kpunia306 đã nhận hôm nay. Đang mởstatus: needs-triage
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 68/100
JabRef/jabref#17459 · 4 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
`RedisList.reversed()` view returns an empty `range` and `trim` removes all elementsCó thể đã có người làm @CCCLL đã nhận hôm nay. Đang mởstatus: waiting-for-triage
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
spring-projects/spring-data-redis#3461 ·
Maintainer thường phản hồi trong vòng 2 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 73/100
javapathfinder/jpf-core#649 ·
-
bug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
devops-thiago/ThrillhouseBot#1093 ·
Maintainer thường phản hồi trong vòng 1 ngày