CleanerJava25 --initialize-at-build-time bypasses isSupported() probe on GraalVM 25 without -H:+SharedArenaSupport
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 35.1k
- Forks
- 16.3k
- Avg merge
- 1d 5h
- Merged PRs (30d)
- 143
Description
Problem
At runtime, a GraalVM 25 native image binary crashes with:
com.oracle.svm.core.jdk.UnsupportedFeatureError: Support for Arena.ofShared is not active
at com.oracle.svm.core.jdk.ScopedMemoryAccess.closeScope0(ScopedMemoryAccess.java)
at jdk.internal.misc.ScopedMemoryAccess.closeScope0(ScopedMemoryAccess.java)
at io.netty.util.internal.CleanerJava25$CleanableDirectBufferImpl.clean(CleanerJava25.java)
at io.netty.buffer.AdaptivePoolingAllocator$Chunk.deallocate(...)
This crash happens during ByteBuf deallocation when AdaptivePoolingAllocator is in low-memory mode (IS_LOW_MEM = true, triggered when heap ≤ 512 MB).
Root Cause
The probe in PR #15877 is bypassed on GraalVM 25
PR #15877 added a probe in CleanerJava25's static initializer that tries Arena.ofShared().close() at startup, catching failure to null out INVOKE_ALLOCATOR:
static {
try {
Object shared = ofShared.invoke();
((AutoCloseable) shared).close(); // probe
INVOKE_ALLOCATOR = ofShared;
} catch (Throwable t) {
INVOKE_ALLOCATOR = null; // disable if unsupported
}
}
This is the correct design. However, it is silently bypassed because of the line added in commit [42d83b80] :
# META-INF/native-image/io.netty/netty-common/native-image.properties
Args = ... --initialize-at-build-time=io.netty.util.internal.CleanerJava25
Because CleanerJava25 is initialized at build time, the static initializer — including the probe — runs on the HotSpot JDK 25 during the native image build. On HotSpot, Arena.ofShared().close() succeeds. The probe passes, INVOKE_ALLOCATOR is set to a non-null MethodHandle, and this value is frozen into the native image heap.
At runtime on the GraalVM substrate (without -H:+SharedArenaSupport), the static initializer never runs again. INVOKE_ALLOCATOR is already non-null, isSupported() returns true, and Arena.close() is called — hitting GraalVM's closeScope0Unsupported stub and crashing.
Why Arena.ofShared() succeeds but Arena.close() fails
Arena.ofShared() is pure Java object construction — no intrinsic required, works in any environment. Arena.close() calls ScopedMemoryAccess.closeScope0(), a JVM intrinsic that performs a cross-thread memory scope handshake. GraalVM substitutes this with a stub that throws UnsupportedFeatureError when -H:+SharedArenaSupport is not active.
The original commit already acknowledged this
Commit 42d83b80 (PR #15325), which introduced the --initialize-at-build-time line, was explicitly written for GraalVM 19–24. On those versions, the version check (javaVersion >= 25) evaluates to false during the HotSpot build, INVOKE_ALLOCATOR is null, and Arena is dead-code-eliminated — fixing the undefined reference to closeScope0 linker error. For GraalVM 19–24, this works correctly.
The commit author's own note at the time:
"Note that native image 25 is broken even with this change for what appears to be an unrelated reason related to Unsafe. I've forwarded this to the native image team."
The GraalVM 25 breakage was acknowledged and deferred to the GraalVM team. No Netty-side fix was shipped for the case of GraalVM 25 without SharedArenaSupport.
-H:+SharedArenaSupport is not usable in our build config
On GraalVM 25.0.0 with --static --libc=musl, enabling -H:+SharedArenaSupport causes a build crash in SubstrateOptimizeSharedArenaAccessPhase (Exception. cannot be inlined). This appears to be a separate GraalVM bug.
graalvm-reachability-metadata does not help
Netty's native-image.properties carries the comment # Note: This file is overridden by graalvm-reachability-metadata. However, the community metadata for io.netty:netty-common (latest entry: 4.1.115.Final) only contains reachability-metadata.json (reflection/JNI registrations). It does not carry a native-image.properties with initialization args, so it does not suppress the --initialize-at-build-time declaration from the jar.
Workaround
We implemented a GraalVM Feature that hooks into the constant-folding phase (BeforeAnalysisAccess.registerFieldValueTransformer) to force INVOKE_ALLOCATOR = null in the native image heap at analysis time, causing isSupported() to constant-fold to false and the Arena code path to be dead-code-eliminated:
public class NettyCleanerJava25Feature implements Feature {
@Override
public void beforeAnalysis(BeforeAnalysisAccess access) {
try {
Class<?> c = Class.forName("io.netty.util.internal.CleanerJava25");
Field f = c.getDeclaredField("INVOKE_ALLOCATOR");
f.setAccessible(true);
access.registerFieldValueTransformer(f, (receiver, originalValue) -> null);
} catch (Exception e) {
System.err.println("[NettyCleanerJava25Feature] failed: " + e);
}
}
}
This is a user-space workaround. The correct fix should be in Netty itself.
Proposed Fix
Ship a GraalVM @TargetClass substitution inside netty-common:
@TargetClass(className = "io.netty.util.internal.CleanerJava25")
final class Target_CleanerJava25 {
@RecomputeFieldValue(kind = RecomputeFieldValue.Kind.Reset)
static MethodHandle INVOKE_ALLOCATOR;
}
This resets INVOKE_ALLOCATOR to null in the native image heap automatically, giving the same result without changing initialization policy.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with io.netty.util.internal.CleanerJava25 and META-INF/native-image/io.netty/netty-common/native-image.properties, then inspect the existing native-image integration for field substitutions. Reproduce the GraalVM 25 native-image case without -H:+SharedArenaSupport and verify that low-memory ByteBuf deallocation no longer reaches the unsupported Arena.close path.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java
- Domain
- backend, build-system
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100