Skip to content

[Native Image][25] Tracing agent misses field-level reachability metadata (Ktor CIO / InterestSuspensionsMap) on GraalVM 25 #12650

@rotilho

Description

@rotilho

Describe the issue
After upgrading to GraalVM for JDK 25, a native-image built with the tracing agent (-agentlib:native-image-agent / -Pagent) fails at runtime with a MissingReflectionRegistrationError for an internal Ktor CIO class, even though the agent has been run with extensive tests that exercise the failing code path.

The problematic type is:

io.ktor.network.selector.InterestSuspensionsMap

At runtime, GraalVM 25 expects field-level reachability metadata for fields like readHandlerReference (accessed via AtomicReferenceFieldUpdater / Unsafe.objectFieldOffset), but the agent-generated reachability-metadata.json only contains a bare "type" entry for this class and no "fields" / "allDeclaredFields" information.

The same application and the same use of the tracing agent work on GraalVM 21 and GraalVM 24, where the generated metadata correctly includes the fields. On GraalVM 25, the fields are no longer emitted, which suggests a regression or behaviour change in the tracing agent / exact reachability handling in 25.

There is already some discussion about agent issues on JDK 25 in #10352 which I suspect may be related but this report is specifically about the agent not emitting field-level metadata for InterestSuspensionsMap even when the code path is exercised under the agent.

Steps to reproduce the issue
Please include both build steps as well as run steps

  1. Clone the reproducer:

    git clone --depth 1 https://github.com/rotilho/node-reproducer.git
    cd node-reproducer
  2. Run tests with the tracing agent enabled to generate reachability metadata:

    ./gradlew test -Pagent
  3. Build the native image:

    ./gradlew nativeCompile
  4. Run the native binary (path as produced by the Gradle native-image plugin, e.g.):

    ./build/native/nativeCompile/node-reproducer
  5. The application fails at startup with a MissingReflectionRegistrationError pointing to io.ktor.network.selector.InterestSuspensionsMap.readHandlerReference.

Example run (just the metadata generation):
https://github.com/rotilho/node-reproducer/actions/runs/19893636797

Describe GraalVM and your environment:

  • GraalVM version: GraalVM Runtime Environment Oracle GraalVM 25.0.1+8.1 (build 25.0.1+8-LTS-jvmci-b01)

  • JDK major version: 25

  • OS: Ubuntu (GitHub Actions ubuntu-latest runner)

  • Architecture: AMD64

  • Frameworks / libraries:

    • Spring Boot 3.5.6
    • Kotlin 2.3.0-RC
    • Ktor client CIO engine (io.ktor:ktor-client-cio)
    • org.graalvm.buildtools.native Gradle plugin

More details
At runtime, the native image fails during Spring context initialization with an UnsatisfiedDependencyException caused by a MissingReflectionRegistrationError. The interesting part of the log is:

{
"type": "io.ktor.network.selector.InterestSuspensionsMap",
"fields": [
{
"name": "readHandlerReference"
}
]
}
{}
The 'reachability-metadata.json' file should be located in 'META-INF/native-image/<group-id>/<artifact-id>/' of your project. For further help, see https://www.graalvm.org/latest/reference-manual/native-image/metadata/#reflection
at com.oracle.svm.core.reflect.MissingReflectionRegistrationUtils.reportAccessedField(MissingReflectionRegistrationUtils.java:79)
at com.oracle.svm.core.reflect.UnsafeFieldUtil.getFieldOffset(UnsafeFieldUtil.java:46)
at jdk.internal.misc.Unsafe.objectFieldOffset(Unsafe.java:44)
at java.util.concurrent.atomic.AtomicReferenceFieldUpdater$AtomicReferenceFieldUpdaterImpl.<init>(AtomicReferenceFieldUpdater.java:111)
at java.util.concurrent.atomic.AtomicReferenceFieldUpdater.newUpdater(AtomicReferenceFieldUpdater.java:112)
at io.ktor.network.selector.InterestSuspensionsMap.<clinit>(InterestSuspensionsMap.kt:66)
at io.ktor.network.selector.SelectableBase.<init>(SelectableBase.kt:15)
at io.ktor.network.sockets.SocketBase.<init>(SocketBase.kt:15)
at io.ktor.network.sockets.NIOSocketImpl.<init>(NIOSocketImpl.kt:15)
at io.ktor.network.sockets.SocketImpl.<init>(SocketImpl.kt:10)
at io.ktor.network.sockets.ConnectUtilsJvmKt.tcpConnect(ConnectUtilsJvm.kt:20)
at io.ktor.network.sockets.TcpSocketBuilder.connect(TcpSocketBuilder.kt:48)
at io.ktor.client.engine.cio.ConnectionFactory.connect(ConnectionFactory.kt:30)
...

However, the agent-generated metadata differs across GraalVM versions for the exact same project and agent flags:

GraalVM 21 — reflect-config.json

The agent produces a proper reflect-config.json with field-level metadata:

{
  "name": "io.ktor.network.selector.InterestSuspensionsMap",
  "fields": [
    { "name": "acceptHandlerReference" },
    { "name": "connectHandlerReference" },
    { "name": "readHandlerReference" },
    { "name": "writeHandlerReference" }
  ]
},

GraalVM 24 — reachability-metadata.json

On GraalVM 24, the new reachability-metadata.json is generated and still correctly includes the fields:

{
  "type": "io.ktor.network.selector.InterestSuspensionsMap",
  "fields": [
    {
      "name": "acceptHandlerReference"
    },
    {
      "name": "connectHandlerReference"
    },
    {
      "name": "readHandlerReference"
    },
    {
      "name": "writeHandlerReference"
    }
  ]
},

With this metadata, the native image runs successfully.

GraalVM 25 — reachability-metadata.json

On GraalVM 25, with the same application code and the same agent setup, the generated reachability-metadata.json only contains a bare type entry and no field information:

{
  "type": "io.ktor.network.selector.InterestSuspensionsMap"
},

There is no "fields" and no "allDeclaredFields" for this type. At runtime, GraalVM 25 then reports that it needs field-level metadata for readHandlerReference (and the other handler fields) when AtomicReferenceFieldUpdater / Unsafe.objectFieldOffset is used, and throws a MissingReflectionRegistrationError.

Metadata

Metadata

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions