Race on first use of a by-value Structure type: FFIType is shared per class with size 0 and libffi initialises it in place without a lock
Dieses Issue hat noch niemand übernommen.
Bewertung
- Schwierigkeit
- 4/5
- Geschätzter Aufwand
- 3-5 Tage
- Anfängerfreundlichkeit
- 43/100
Rechercherichtung
Start by reading Structure.java around FFIType, typeInfoMap, and cacheStructureLock, then trace getStructureType() into native/dispatch.c and the Native.initialize_ffi_type binding. Confirm the shared descriptor can be initialized safely before publication, and add a concurrent first-use regression test for a by-value structure. Done means concurrent calls preserve the correct structure size and values without crashes.
Vom Indexierungsmodell aus dem Issue-Text verfasst.
Beschreibung
Summary
The libffi ffi_type that JNA creates for a Structure class is shared per class and is created with size = 0 and alignment = 0. libffi fills it in lazily inside ffi_prep_cif, in place and without a lock. When two threads pass a by-value struct of a class that has not been used before, at the same time, they race on that initialisation. One thread can read a partial size, marshal the struct with the wrong size, and corrupt the native stack or pass wrong data to the callee. The shared descriptor can also be left with a wrong size for the rest of the process.
Observed on JNA 5.18.1, JDK 21, macOS arm64, and in production on Android arm32 (POS app) where it crashes with SIGSEGV on return from the native call.
Mechanism
Structure.FFIType(Structure ref)writeselementsbut leavessizeandalignmentat 0 (Structure.java,FFITypeconstructor andinit). The instance is cached per class intypeInfoMapand shared across threads.Native.initialize_ffi_typeexists but is not called.dispatch()innative/dispatch.cbuilds a newffi_cifon every call and passes the sharedffi_type*fromgetStructureType()toffi_prep_cif, with no synchronisation.- libffi
ffi_prep_cif_corerunsinitialize_aggregatewhenarg->size == 0.initialize_aggregate(src/prep_cif.c) doesarg->size = 0; arg->alignment = 0;and then accumulates element sizes intoarg->sizeon the shared object. - Two threads in
initialize_aggregateat once interleave the reset and the accumulation. Each thread then computescif->bytesfrom whateversizeit reads, whileffi_prep_argslater copiesty->sizebytes. Abytesthat is too small overflows the argument block into libffi's call frame; asizethat is too small on aarch64 makes libffi pass a large struct in registers instead of by pointer.
Only the first use of each struct class is exposed. After one successful initialisation, size != 0 and the path is never taken again.
Reproduction
Two files, no dependencies beyond jna-5.18.1.jar and a C compiler.
sink.c:
#include <stdint.h>
typedef struct { uint64_t a; uint64_t b; void *p; } inner_t; /* 24 bytes */
typedef struct { inner_t first; int8_t code; inner_t second; } outer_t; /* 56 bytes */
uint64_t sink(uint64_t handle, outer_t v) {
return (handle << 48) | ((uint64_t)(uint8_t)v.code << 40) | (v.first.a << 20) | v.first.b;
}
FfiTypeRace.java:
import com.sun.jna.*;
import java.lang.reflect.Method;
import java.util.concurrent.atomic.*;
public class FfiTypeRace {
@Structure.FieldOrder({"a", "b", "p"})
public static class Inner extends Structure {
public long a; public long b; public Pointer p;
public static class ByValue extends Inner implements Structure.ByValue {}
}
@Structure.FieldOrder({"first", "code", "second"})
public static class Outer extends Structure {
public Inner.ByValue first = new Inner.ByValue(); public byte code; public Inner.ByValue second = new Inner.ByValue();
public static class ByValue extends Outer implements Structure.ByValue {}
}
// Test hook only: JNA's native ffi_type for a Structure class (size_t size @0, short alignment @8).
static Pointer ffiType(Structure s) throws Exception {
Method m = Structure.class.getDeclaredMethod("getTypeInfo"); m.setAccessible(true);
return (Pointer) m.invoke(s);
}
static long expected(long h, long a, long b) { return (h << 48) | (a << 20) | b; }
public static void main(String[] args) throws Exception {
int threads = Integer.parseInt(args[0]), rounds = Integer.parseInt(args[1]);
boolean resetEachRound = args.length > 2 && args[2].equals("reset");
Function sink = NativeLibrary.getInstance("sink").getFunction("sink");
Outer.ByValue warm = new Outer.ByValue(); warm.first.a = 1; warm.first.b = 2;
if (sink.invokeLong(new Object[]{9L, warm}) != expected(9, 1, 2)) throw new AssertionError("warm-up");
Pointer outer = ffiType(warm), inner = ffiType(new Inner.ByValue());
System.out.println("after single-threaded first use: outer.size=" + outer.getLong(0) + " inner.size=" + inner.getLong(0));
AtomicLong gen = new AtomicLong(); AtomicInteger arrived = new AtomicInteger(), done = new AtomicInteger();
AtomicInteger wrongSize = new AtomicInteger(), wrongChecksum = new AtomicInteger(); AtomicBoolean stop = new AtomicBoolean();
for (int t = 0; t < threads; t++) {
final long id = t;
Thread th = new Thread(() -> {
long seen = 0;
while (!stop.get()) {
while (gen.get() == seen && !stop.get()) Thread.onSpinWait();
if (stop.get()) break;
seen = gen.get(); long a = (seen % 1000) + 1, b = id + 1;
Outer.ByValue v = new Outer.ByValue(); v.first.a = a; v.first.b = b;
arrived.incrementAndGet(); while (arrived.get() < threads) Thread.onSpinWait();
if (sink.invokeLong(new Object[]{id, v}) != expected(id, a, b)) wrongChecksum.incrementAndGet();
done.incrementAndGet();
}
});
th.setDaemon(true); th.start();
}
for (int i = 0; i < 20000; i++) sink.invokeLong(new Object[]{1L, warm}); // JIT the call path
for (int r = 0; r < rounds; r++) {
// Re-create "first use of this class in the process" for every round.
if (resetEachRound) { outer.setLong(0, 0); outer.setShort(8, (short) 0); inner.setLong(0, 0); inner.setShort(8, (short) 0); }
arrived.set(0); done.set(0); gen.set(r + 1);
while (done.get() < threads) Thread.onSpinWait();
if (outer.getLong(0) != 56) wrongSize.incrementAndGet();
}
stop.set(true);
System.out.println("threads=" + threads + " rounds=" + rounds + " reset_each_round=" + resetEachRound
+ " rounds_with_wrong_final_size=" + wrongSize + " wrong_struct_values_seen_by_native=" + wrongChecksum);
}
}
cc -shared -o libsink.dylib sink.c # or libsink.so
javac -cp jna-5.18.1.jar FfiTypeRace.java
java -Djna.library.path=. -cp .:jna-5.18.1.jar FfiTypeRace 4 20000 # control
java -Djna.library.path=. -cp .:jna-5.18.1.jar FfiTypeRace 4 20000 reset # race
Results on JDK 21.0.10, macOS 15 arm64, JNA 5.18.1:
| mode | rounds | rounds with wrong final size |
wrong struct values seen by native | crash |
|---|---|---|---|---|
| control (descriptor initialised once, then 4 threads) | 20000 | 0 | 0 | no |
| race (descriptor reset before each round, 4 threads) | 20000 | 247 | 13 | no |
| race, second run | stopped early | - | - | SIGSEGV in sink+0x4, si_addr 0x18 |
| race, third run | stopped early | - | - | SIGSEGV in sink+0x4 |
The reset through getTypeInfo() only re-creates the state a brand new Structure class has on its first use. Without it the race happens once per class per process, which is how it shows up in production: rare per device, but tied to the first concurrent use of a struct type.
Production impact
Shopify POS on Android passes a 56-byte struct by value from two threads at the same time, once per process, through UniFFI generated bindings. On 32-bit ARM devices the thread that computed a too-small cif->bytes overflows libffi's call frame and crashes with SIGSEGV on return, with PC in no code mapping and the callee having received correct data. The same race is a plausible cause for the foreign-future completion segfaults discussed in mozilla/uniffi-rs#2733.
Possible fixes
- Compute
sizeandalignmentin Java when constructingFFIType(Structure ref)and write them with theelements, so libffi never seessize == 0. JNA already knows both values from theStructurelayout. - Or call the existing
Native.initialize_ffi_typeon the newFFITypeinsidecacheStructureLockinFFIType.get, before the type is published.
Either removes the only unsynchronised write to the shared descriptor.
Workaround
Pass one instance of each by-value struct class through a native call, or create one JNA closure for a callback method that takes it, on a single thread before any concurrent use. We do this at application startup.
- Vorherrschende Sprache
- Java
- Sterne
- 8.9k
- Forks
- 1.7k
- Ø Merge
- 7 T. 18 Std.
- Gemergte PRs (30 T.)
- 2
Entwicklungsumgebung
Dieses Projekt bietet weder Dev-Container noch Dockerfile noch Beitragsleitfaden – die Einrichtung liegt bei Ihnen. Beginnen Sie mit der README; die allgemeinen Schritte stehen in unserem Leitfaden für den ersten Beitrag.
Erste Schritte
- Lesen Sie das ganze Issue und danach den Beitragsleitfaden des Projekts.
- Schreiben Sie ins Issue, dass Sie es übernehmen — das erspart doppelte Arbeit.
- Forken Sie das Repository und arbeiten Sie in einem Branch.
- Öffnen Sie einen Pull Request, der die Issue-Nummer nennt.
Mehr aus java-native-access/jna
-
MacFileUtils: FSPathMakeRef passes an int where the C function takes its FSRef output pointerEvtl. wieder frei @VelpulaGangavardhan hat das vor 13 Tagen übernommen, und es ist kein Pull Request offen. Offen
Schwierigkeit 1/5 Unter einer Stunde Anfängerfreundlichkeit 79/100
java-native-access/jna#1746 · 1 Kommentar ·
-
User32: SetCursorPos is declared (long, long), but Windows declares (int, int)Evtl. vergeben @OKAN-SOFTWARE hat das vor 7 Tagen übernommen. Offen
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 72/100
java-native-access/jna#1745 ·
-
Update libffi to v3.8.0?Offen
Schwierigkeit 4/5 3-5 Tage Anfängerfreundlichkeit 44/100
java-native-access/jna#1751 · 2 Kommentare ·
-
Schwierigkeit 4/5 3-5 Tage Anfängerfreundlichkeit 48/100
java-native-access/jna#1747 · 1 Kommentar ·
-
`Native.typeOptions` never releases library interfaces, leaking classloadersEvtl. vergeben @curforever hat das vor 25 Tagen übernommen. Offen
Schwierigkeit 4/5 1-2 Tage Anfängerfreundlichkeit 48/100
java-native-access/jna#1742 ·
Alle Issues in java-native-access/jna
Ähnliche Issues
-
getRawTextContent() returns the lines of every page in reverse order (bottom to top) since 4.0.3Offen
Schwierigkeit 1/5 1-3 Stunden Anfängerfreundlichkeit 90/100
-
bug subsystem:agent-harness triage
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 72/100
ShaftHQ/SHAFT_ENGINE#7797 ·
Maintainer antworten meist innerhalb von 1 Tag
-
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 73/100
NationalSecurityAgency/ghidra#10155 ·
Maintainer antworten meist innerhalb von 2 Tagen
-
area:data coverage
Schwierigkeit 1/5 Unter einer Stunde Anfängerfreundlichkeit 67/100
reacher-z/Awesome-CUA#4864 ·
Maintainer antworten meist innerhalb von 19 Tagen
-
bug web-ui
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 67/100
jenkinsci/jenkins#27705 · 3 Kommentare ·
Maintainer antworten meist innerhalb von 1 Tag