From caf1beed1c297f2ad1a162489691e3f3bae5e610 Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Mon, 4 May 2026 16:08:23 +0200 Subject: [PATCH 1/5] Improve JPMS module opener --- .../java/module/JpmsHelper.java | 25 +++++++++- .../java/net/JpmsInetAddressHelper.java | 9 ---- .../trace/agent/tooling/AgentInstaller.java | 17 ++++--- .../agent/tooling/JavaModuleOpenProvider.java | 3 +- .../tooling/InstrumenterIndexTest.groovy | 22 +++++++++ .../module/JpmsClearanceInstrumentation.java | 28 +++++------ .../JpmsInetAddressInstrumentation.java | 4 +- .../JpmsInetAddressDisabledForkedTest.groovy | 45 ----------------- .../JpmsInetAddressForkedTest.groovy | 40 ---------------- .../JpmsInetAddressDisabledForkedTest.java | 48 +++++++++++++++++++ .../httpclient/JpmsInetAddressForkedTest.java | 42 ++++++++++++++++ .../mule4/JpmsMuleInstrumentation.java | 20 ++++++-- docs/how_instrumentations_work.md | 8 ++-- 13 files changed, 183 insertions(+), 128 deletions(-) delete mode 100644 dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/net/JpmsInetAddressHelper.java delete mode 100644 dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/test/groovy/datadog/trace/instrumentation/httpclient/JpmsInetAddressDisabledForkedTest.groovy delete mode 100644 dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/test/groovy/datadog/trace/instrumentation/httpclient/JpmsInetAddressForkedTest.groovy create mode 100644 dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/test/java/datadog/trace/instrumentation/httpclient/JpmsInetAddressDisabledForkedTest.java create mode 100644 dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/test/java/datadog/trace/instrumentation/httpclient/JpmsInetAddressForkedTest.java diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/module/JpmsHelper.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/module/JpmsHelper.java index f926a3500e4..490d7d8130e 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/module/JpmsHelper.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/module/JpmsHelper.java @@ -17,11 +17,17 @@ private JpmsHelper() {} private static final Set TRIGGERS = new HashSet<>(); + private static final Set TRIGGERS_VIEW = unmodifiableSet(TRIGGERS); + private static final ClassValue HAS_FIRED = GenericClassValue.constructing(AtomicBoolean.class); - public static final Logger LOGGER = LoggerFactory.getLogger(JpmsHelper.class); + private static final Logger LOGGER = LoggerFactory.getLogger(JpmsHelper.class); + /** + * Registers trigger class names whose constructors will open their enclosing module. Must be + * called at agent startup before instrumentation is applied; not thread-safe. + */ public static void addTriggers(Collection classes) { if (classes == null) { return; @@ -29,11 +35,26 @@ public static void addTriggers(Collection classes) { TRIGGERS.addAll(classes); } + /** Returns an unmodifiable view of all registered trigger class names. */ public static Set getAllTriggers() { - return unmodifiableSet(TRIGGERS); + return TRIGGERS_VIEW; } + /** + * Returns {@code true} and atomically marks {@code cls} as opened the first time this is called + * for a given class; returns {@code false} on all subsequent calls for the same class. + */ public static boolean shouldBeOpened(Class cls) { return HAS_FIRED.get(cls).compareAndSet(false, true); } + + /** Called from inlined ByteBuddy advice; logs when module opening fails. */ + public static void logFailedToOpen(String pkg, Throwable t) { + LOGGER.debug("Unable to open package {} to the agent module or unnamed module", pkg, t); + } + + /** Called from inlined ByteBuddy advice; logs when a class has no named module. */ + public static void logNullModule(Class cls) { + LOGGER.debug("Class {} has no named module; skipping module open", cls.getName()); + } } diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/net/JpmsInetAddressHelper.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/net/JpmsInetAddressHelper.java deleted file mode 100644 index 62b937b824d..00000000000 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/net/JpmsInetAddressHelper.java +++ /dev/null @@ -1,9 +0,0 @@ -package datadog.trace.bootstrap.instrumentation.java.net; - -import java.util.concurrent.atomic.AtomicBoolean; - -public class JpmsInetAddressHelper { - public static final AtomicBoolean OPENED = new AtomicBoolean(false); - - private JpmsInetAddressHelper() {} -} diff --git a/dd-java-agent/agent-installer/src/main/java/datadog/trace/agent/tooling/AgentInstaller.java b/dd-java-agent/agent-installer/src/main/java/datadog/trace/agent/tooling/AgentInstaller.java index bf5e01b8446..3a8c7065362 100644 --- a/dd-java-agent/agent-installer/src/main/java/datadog/trace/agent/tooling/AgentInstaller.java +++ b/dd-java-agent/agent-installer/src/main/java/datadog/trace/agent/tooling/AgentInstaller.java @@ -193,19 +193,22 @@ public static ClassFileTransformer installBytebuddyAgent( // immediately and we don't have the ability to express dependencies between different // instrumentations to control the load order. for (InstrumenterModule module : instrumenterModules) { - boolean filterAdded = false; if (module instanceof ExcludeFilterProvider) { ExcludeFilterProvider provider = (ExcludeFilterProvider) module; ExcludeFilter.add(provider.excludedClasses()); - filterAdded = true; + if (DEBUG) { + log.debug( + "Registering exclude-filter module - instrumentation.class={}", + module.getClass().getName()); + } } if (javaModuleSupported && module instanceof JavaModuleOpenProvider) { JpmsHelper.addTriggers(((JavaModuleOpenProvider) module).triggerClasses()); - filterAdded = true; - } - if (DEBUG && filterAdded) { - log.debug( - "Adding filtered classes - instrumentation.class={}", module.getClass().getName()); + if (DEBUG) { + log.debug( + "Registering JPMS trigger module - instrumentation.class={}", + module.getClass().getName()); + } } } diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/JavaModuleOpenProvider.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/JavaModuleOpenProvider.java index 3f506ea8a51..142164d7aff 100644 --- a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/JavaModuleOpenProvider.java +++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/JavaModuleOpenProvider.java @@ -3,7 +3,8 @@ import java.util.Collection; /** - * Allows an {@link InstrumenterModule} to possibly open a java module to the unnamed module. + * Allows an {@link InstrumenterModule} to open a java module to the agent module and to the unnamed + * module of the trigger class's class loader. * *

This is typically used when reflective operations need to be done and the agent cannot assume * that the host application has permitted them. diff --git a/dd-java-agent/agent-tooling/src/test/groovy/datadog/trace/agent/tooling/InstrumenterIndexTest.groovy b/dd-java-agent/agent-tooling/src/test/groovy/datadog/trace/agent/tooling/InstrumenterIndexTest.groovy index 4f220451c38..77492474dd2 100644 --- a/dd-java-agent/agent-tooling/src/test/groovy/datadog/trace/agent/tooling/InstrumenterIndexTest.groovy +++ b/dd-java-agent/agent-tooling/src/test/groovy/datadog/trace/agent/tooling/InstrumenterIndexTest.groovy @@ -83,4 +83,26 @@ class InstrumenterIndexTest extends DDSpecification { index.instrumentationId(unknownInstrumentation) == -1 index.transformationId(unknownTransformation) == -1 } + + def "JavaModuleOpenProvider-only module gets NEEDS_EARLY_LOAD_FLAG"() { + given: + def jpmsOnlyModule = new TestJpmsOnlyModule() + + when: + byte flags = InstrumenterIndex.encodeModuleFlags(jpmsOnlyModule, false) + + then: + InstrumenterIndex.decodeModuleNeedsEarlyLoad(flags) + } +} + +class TestJpmsOnlyModule extends InstrumenterModule implements JavaModuleOpenProvider { + TestJpmsOnlyModule() { + super('jpms-test-only') + } + + @Override + Collection triggerClasses() { + return [] + } } diff --git a/dd-java-agent/instrumentation/java/java-lang/java-lang-9.0/src/main/java/datadog/trace/instrumentation/java/lang/module/JpmsClearanceInstrumentation.java b/dd-java-agent/instrumentation/java/java-lang/java-lang-9.0/src/main/java/datadog/trace/instrumentation/java/lang/module/JpmsClearanceInstrumentation.java index de52d2188ed..b8b1b8dba67 100644 --- a/dd-java-agent/instrumentation/java/java-lang/java-lang-9.0/src/main/java/datadog/trace/instrumentation/java/lang/module/JpmsClearanceInstrumentation.java +++ b/dd-java-agent/instrumentation/java/java-lang/java-lang-9.0/src/main/java/datadog/trace/instrumentation/java/lang/module/JpmsClearanceInstrumentation.java @@ -13,10 +13,10 @@ import net.bytebuddy.implementation.bytecode.assign.Assigner; /** - * Generic instrumenter module that will advice the constructor of each known class in order to open - * once their module. This is marked for bootstrap even if it's not for sure, but we cannot know in - * advance (depends to the instrumented types and today we are instrumenting InetAddress that's in - * the bootstrap). + * Generic instrumenter module that advises the constructor of each registered trigger class to open + * its enclosing module once. Marked {@link Instrumenter.ForBootstrap} because some trigger classes + * (e.g. {@code InetAddress}) reside in the bootstrap classloader; the annotation is applied + * conservatively since the set of trigger classes is not known until runtime. */ @AutoService(InstrumenterModule.class) public class JpmsClearanceInstrumentation extends InstrumenterModule @@ -34,7 +34,7 @@ public boolean isEnabled() { @Override public boolean isApplicable(Set enabledSystems) { - return true; // not directly linked ot a target system + return true; // not directly linked to a target system } @Override @@ -53,22 +53,22 @@ public static void onExit(@Advice.This(typing = Assigner.Typing.DYNAMIC) Object final Class cls = self.getClass(); if (JpmsHelper.shouldBeOpened(cls)) { final Module module = cls.getModule(); + final String pkg = cls.getPackageName(); if (module != null) { try { - // This call needs imperatively to be done from the same module we're adding exports - // because the jdk is checking that the caller belongs to the same module. - // The code of this advice is getting inlined into the constructor of the class - // belonging - // to that package so it will work. Moving the same to a helper won't. - module.addOpens(cls.getPackageName(), JpmsHelper.class.getModule()); + // This call must be inlined into the constructor of the class belonging to that + // package because the JDK verifies the caller belongs to the module being opened. + // Moving this to a helper method will not work. + module.addOpens(pkg, JpmsHelper.class.getModule()); final ClassLoader loader = cls.getClassLoader(); if (loader != null) { - module.addOpens(cls.getPackageName(), loader.getUnnamedModule()); + module.addOpens(pkg, loader.getUnnamedModule()); } } catch (Throwable t) { - JpmsHelper.LOGGER.debug( - "Unable to open package {} to the unnamed module", cls.getPackageName(), t); + JpmsHelper.logFailedToOpen(pkg, t); } + } else { + JpmsHelper.logNullModule(cls); } } } diff --git a/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/main/java/datadog/trace/instrumentation/httpclient/JpmsInetAddressInstrumentation.java b/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/main/java/datadog/trace/instrumentation/httpclient/JpmsInetAddressInstrumentation.java index 1a0c6a62a31..b79f2c4268b 100644 --- a/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/main/java/datadog/trace/instrumentation/httpclient/JpmsInetAddressInstrumentation.java +++ b/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/main/java/datadog/trace/instrumentation/httpclient/JpmsInetAddressInstrumentation.java @@ -11,12 +11,14 @@ public class JpmsInetAddressInstrumentation extends InstrumenterModule implements JavaModuleOpenProvider { + private static final Collection TRIGGER_CLASSES = singleton("java.net.InetAddress"); + public JpmsInetAddressInstrumentation() { super("java-net"); } @Override public Collection triggerClasses() { - return singleton("java.net.InetAddress"); + return TRIGGER_CLASSES; } } diff --git a/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/test/groovy/datadog/trace/instrumentation/httpclient/JpmsInetAddressDisabledForkedTest.groovy b/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/test/groovy/datadog/trace/instrumentation/httpclient/JpmsInetAddressDisabledForkedTest.groovy deleted file mode 100644 index fc38a05e100..00000000000 --- a/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/test/groovy/datadog/trace/instrumentation/httpclient/JpmsInetAddressDisabledForkedTest.groovy +++ /dev/null @@ -1,45 +0,0 @@ -package datadog.trace.instrumentation.httpclient - -import datadog.environment.JavaVirtualMachine -import datadog.trace.agent.test.InstrumentationSpecification -import datadog.trace.bootstrap.instrumentation.java.net.HostNameResolver -import spock.lang.IgnoreIf - -@IgnoreIf(reason = "--illegal-access=deny is only enforced from java 16", value = { - !JavaVirtualMachine.isJavaVersionAtLeast(16) -}) -class JpmsInetAddressDisabledForkedTest extends InstrumentationSpecification { - - @Override - protected void configurePreAgent() { - super.configurePreAgent() - // Disable the JPMS instrumentation so java.net is NOT opened for deep reflection. - // HostNameResolver will be unable to bypass the IP→hostname cache and will fall back - // to the cache keyed by IP address. - injectSysConfig("dd.trace.java-module.enabled", "false") - } - - /** - * Verifies the fallback behaviour when the JPMS instrumentation is disabled: - * HostNameResolver cannot reflectively read the pre-set hostname from InetAddress and - * falls back to a cache keyed by IP address. As a result, once a hostname has been - * cached for a given IP, every subsequent lookup for that IP returns the first cached - * value, even when the InetAddress object carries a different hostname. - * - * This is the broken behaviour that the JPMS instrumentation is designed to fix. - */ - def "without JPMS instrumentation, IP cache causes stale hostname to be returned"() { - given: - def ip = [192, 0, 2, 2] as byte[] // different subnet from the enabled-test to avoid cross-test cache pollution - def addr1 = InetAddress.getByAddress("service1.example.com", ip) - // Prime the IP→hostname cache with service1's hostname - HostNameResolver.hostName(addr1, "192.0.2.2") - - when: "a second service with the same IP but a different hostname is resolved" - def addr2 = InetAddress.getByAddress("service2.example.com", ip) - def result = HostNameResolver.hostName(addr2, "192.0.2.2") - - then: "the stale cached hostname of service1 is returned instead of service2's" - result == "service1.example.com" - } -} diff --git a/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/test/groovy/datadog/trace/instrumentation/httpclient/JpmsInetAddressForkedTest.groovy b/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/test/groovy/datadog/trace/instrumentation/httpclient/JpmsInetAddressForkedTest.groovy deleted file mode 100644 index e5019969e5a..00000000000 --- a/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/test/groovy/datadog/trace/instrumentation/httpclient/JpmsInetAddressForkedTest.groovy +++ /dev/null @@ -1,40 +0,0 @@ -package datadog.trace.instrumentation.httpclient - -import datadog.environment.JavaVirtualMachine -import datadog.trace.agent.test.InstrumentationSpecification -import datadog.trace.bootstrap.instrumentation.java.net.HostNameResolver -import spock.lang.IgnoreIf - -@IgnoreIf(reason = 'Does not work on J9', value = { - JavaVirtualMachine.isJ9() -}) -class JpmsInetAddressForkedTest extends InstrumentationSpecification { - - /** - * Verifies that the JPMS instrumentation opens java.base/java.net so that - * HostNameResolver can bypass its IP→hostname cache and return the correct - * peer.hostname even when multiple services share a single IP address - * (e.g. services behind a reverse proxy). - * - * Without the fix, HostNameResolver cannot reflectively access - * InetAddress$InetAddressHolder on Java 9+ and falls back to a cache keyed - * by IP, causing the first service's hostname to be returned for all - * subsequent services on the same IP. - */ - def "instrumentation opens java.net so hostname is resolved correctly when IP is shared"() { - given: - // emulate an early initialisation - HostNameResolver.hostName(null, "192.0.2.1") - def ip = [192, 0, 2, 1] as byte[] // TEST-NET, will never appear in real DNS cache - def addr1 = InetAddress.getByAddress("service1.example.com", ip) - // Warm the IP→hostname cache with service1's hostname - HostNameResolver.hostName(addr1, "192.0.2.1") - - when: "a second service with the same IP but different hostname is resolved" - def addr2 = InetAddress.getByAddress("service2.example.com", ip) - def result = HostNameResolver.hostName(addr2, "192.0.2.1") - - then: "the hostname of addr2 is returned, not the cached hostname of addr1" - result == "service2.example.com" - } -} diff --git a/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/test/java/datadog/trace/instrumentation/httpclient/JpmsInetAddressDisabledForkedTest.java b/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/test/java/datadog/trace/instrumentation/httpclient/JpmsInetAddressDisabledForkedTest.java new file mode 100644 index 00000000000..6c5f58917dc --- /dev/null +++ b/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/test/java/datadog/trace/instrumentation/httpclient/JpmsInetAddressDisabledForkedTest.java @@ -0,0 +1,48 @@ +package datadog.trace.instrumentation.httpclient; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import datadog.trace.agent.test.AbstractInstrumentationTest; +import datadog.trace.bootstrap.instrumentation.java.net.HostNameResolver; +import java.net.InetAddress; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledForJreRange; +import org.junit.jupiter.api.condition.JRE; + +// Forked test: runs in an isolated JVM with JPMS instrumentation DISABLED. +// --illegal-access=deny is only enforced from Java 16 onward. +@EnabledForJreRange(min = JRE.JAVA_16) +class JpmsInetAddressDisabledForkedTest extends AbstractInstrumentationTest { + + static { + // Disable the JPMS instrumentation so java.net is NOT opened for deep reflection. + // HostNameResolver will be unable to bypass the IP→hostname cache and will fall back + // to the cache keyed by IP address. + System.setProperty("dd.trace.java-module.enabled", "false"); + } + + /** + * Verifies the fallback behaviour when the JPMS instrumentation is disabled: HostNameResolver + * cannot reflectively read the pre-set hostname from InetAddress and falls back to a cache keyed + * by IP address. As a result, once a hostname has been cached for a given IP, every subsequent + * lookup for that IP returns the first cached value, even when the InetAddress object carries a + * different hostname. + * + *

This is the broken behaviour that the JPMS instrumentation is designed to fix. + */ + @Test + void withoutJpmsInstrumentationIpCausesStaleHostnameToBeReturned() throws Exception { + // different subnet from the enabled-test to avoid cross-test cache pollution + byte[] ip = {(byte) 192, 0, 2, 2}; + InetAddress addr1 = InetAddress.getByAddress("service1.example.com", ip); + // Prime the IP→hostname cache with service1's hostname + HostNameResolver.hostName(addr1, "192.0.2.2"); + + // a second service with the same IP but a different hostname is resolved + InetAddress addr2 = InetAddress.getByAddress("service2.example.com", ip); + String result = HostNameResolver.hostName(addr2, "192.0.2.2"); + + // the stale cached hostname of service1 is returned instead of service2's + assertEquals("service1.example.com", result); + } +} diff --git a/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/test/java/datadog/trace/instrumentation/httpclient/JpmsInetAddressForkedTest.java b/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/test/java/datadog/trace/instrumentation/httpclient/JpmsInetAddressForkedTest.java new file mode 100644 index 00000000000..bcbaf580dbf --- /dev/null +++ b/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/test/java/datadog/trace/instrumentation/httpclient/JpmsInetAddressForkedTest.java @@ -0,0 +1,42 @@ +package datadog.trace.instrumentation.httpclient; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assumptions.assumeFalse; + +import datadog.environment.JavaVirtualMachine; +import datadog.trace.agent.test.AbstractInstrumentationTest; +import datadog.trace.bootstrap.instrumentation.java.net.HostNameResolver; +import java.net.InetAddress; +import org.junit.jupiter.api.Test; + +// Forked test: runs in an isolated JVM with JPMS instrumentation ENABLED (default). +class JpmsInetAddressForkedTest extends AbstractInstrumentationTest { + + /** + * Verifies that the JPMS instrumentation opens java.base/java.net so that HostNameResolver can + * bypass its IP→hostname cache and return the correct peer.hostname even when multiple services + * share a single IP address (e.g. services behind a reverse proxy). + * + *

Without the fix, HostNameResolver cannot reflectively access InetAddress$InetAddressHolder + * on Java 9+ and falls back to a cache keyed by IP, causing the first service's hostname to be + * returned for all subsequent services on the same IP. + */ + @Test + void instrumentationOpensJavaNetSoHostnameIsResolvedCorrectlyWhenIpIsShared() throws Exception { + assumeFalse(JavaVirtualMachine.isJ9(), "Does not work on J9"); + + // emulate an early initialisation + HostNameResolver.hostName(null, "192.0.2.1"); + byte[] ip = {(byte) 192, 0, 2, 1}; // TEST-NET, will never appear in real DNS cache + InetAddress addr1 = InetAddress.getByAddress("service1.example.com", ip); + // Warm the IP→hostname cache with service1's hostname + HostNameResolver.hostName(addr1, "192.0.2.1"); + + // a second service with the same IP but different hostname is resolved + InetAddress addr2 = InetAddress.getByAddress("service2.example.com", ip); + String result = HostNameResolver.hostName(addr2, "192.0.2.1"); + + // the hostname of addr2 is returned, not the cached hostname of addr1 + assertEquals("service2.example.com", result); + } +} diff --git a/dd-java-agent/instrumentation/mule-4.5/src/main/java/datadog/trace/instrumentation/mule4/JpmsMuleInstrumentation.java b/dd-java-agent/instrumentation/mule-4.5/src/main/java/datadog/trace/instrumentation/mule4/JpmsMuleInstrumentation.java index b2f2c720b23..10f9e010b64 100644 --- a/dd-java-agent/instrumentation/mule-4.5/src/main/java/datadog/trace/instrumentation/mule4/JpmsMuleInstrumentation.java +++ b/dd-java-agent/instrumentation/mule-4.5/src/main/java/datadog/trace/instrumentation/mule4/JpmsMuleInstrumentation.java @@ -1,20 +1,32 @@ package datadog.trace.instrumentation.mule4; import static java.util.Arrays.asList; +import static java.util.Collections.unmodifiableList; import com.google.auto.service.AutoService; import datadog.trace.agent.tooling.InstrumenterModule; import datadog.trace.agent.tooling.JavaModuleOpenProvider; import datadog.trace.agent.tooling.muzzle.Reference; import java.util.Collection; +import java.util.Set; @AutoService(InstrumenterModule.class) -public class JpmsMuleInstrumentation extends InstrumenterModule.Tracing - implements JavaModuleOpenProvider { +public class JpmsMuleInstrumentation extends InstrumenterModule implements JavaModuleOpenProvider { + private static final Collection TRIGGER_CLASSES = + unmodifiableList( + asList( + "org.mule.runtime.tracer.customization.impl.info.ExecutionInitialSpanInfo", + "org.mule.runtime.tracer.customization.impl.provider.LazyInitialSpanInfo")); + public JpmsMuleInstrumentation() { super("mule", "mule-jpms"); } + @Override + public boolean isApplicable(Set enabledSystems) { + return true; + } + @Override public Reference[] additionalMuzzleReferences() { return new Reference[] { @@ -32,8 +44,6 @@ public Reference[] additionalMuzzleReferences() { @Override public Collection triggerClasses() { - return asList( - "org.mule.runtime.tracer.customization.impl.info.ExecutionInitialSpanInfo", - "org.mule.runtime.tracer.customization.impl.provider.LazyInitialSpanInfo"); + return TRIGGER_CLASSES; } } diff --git a/docs/how_instrumentations_work.md b/docs/how_instrumentations_work.md index 0a619b701c7..3c4aa7a9053 100644 --- a/docs/how_instrumentations_work.md +++ b/docs/how_instrumentations_work.md @@ -866,7 +866,7 @@ application has not passed the corresponding `--add-opens` flag), the reflection Any `InstrumenterModule` can additionally implement [`JavaModuleOpenProvider`](../dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/JavaModuleOpenProvider.java) to declare _trigger classes_. The first time a trigger class is instantiated, the agent opens the class's enclosing -package to its own module so that subsequent reflective operations succeed. +package to the agent module and to the unnamed module of the class's class loader so that subsequent reflective operations succeed. ```java import static java.util.Collections.singleton; @@ -884,18 +884,18 @@ public class JpmsInetAddressInstrumentation extends InstrumenterModule } @Override - public Iterable triggerClasses() { + public Collection triggerClasses() { return singleton("java.net.InetAddress"); } } ``` -This module has no `adviceTransformations()` — its only purpose is to register the trigger class. +This module has no `methodAdvice()` — its only purpose is to register the trigger class. ### How it works 1. During startup, `AgentInstaller` collects every `InstrumenterModule` that implements `JavaModuleOpenProvider` - and registers their trigger classes with `JpmsHelper.addAllTriggers()`. + and registers their trigger classes with `JpmsHelper.addTriggers()`. 2. `JpmsClearanceInstrumentation` (a built-in module) instruments the **constructors** of all registered trigger classes. 3. On the first constructor call of a trigger class, its advice (inlined via ByteBuddy) calls From 300ab901503cde59597ebeb7842f174313c5a2b3 Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Tue, 5 May 2026 11:09:45 +0200 Subject: [PATCH 2/5] Use WithConfig and skip J9 --- .../JpmsInetAddressDisabledForkedTest.java | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/test/java/datadog/trace/instrumentation/httpclient/JpmsInetAddressDisabledForkedTest.java b/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/test/java/datadog/trace/instrumentation/httpclient/JpmsInetAddressDisabledForkedTest.java index 6c5f58917dc..9bcc47d5f4f 100644 --- a/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/test/java/datadog/trace/instrumentation/httpclient/JpmsInetAddressDisabledForkedTest.java +++ b/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/test/java/datadog/trace/instrumentation/httpclient/JpmsInetAddressDisabledForkedTest.java @@ -1,9 +1,12 @@ package datadog.trace.instrumentation.httpclient; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assumptions.assumeFalse; +import datadog.environment.JavaVirtualMachine; import datadog.trace.agent.test.AbstractInstrumentationTest; import datadog.trace.bootstrap.instrumentation.java.net.HostNameResolver; +import datadog.trace.junit.utils.config.WithConfig; import java.net.InetAddress; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledForJreRange; @@ -12,15 +15,9 @@ // Forked test: runs in an isolated JVM with JPMS instrumentation DISABLED. // --illegal-access=deny is only enforced from Java 16 onward. @EnabledForJreRange(min = JRE.JAVA_16) +@WithConfig(key = "trace.java-module.enabled", value = "false") class JpmsInetAddressDisabledForkedTest extends AbstractInstrumentationTest { - static { - // Disable the JPMS instrumentation so java.net is NOT opened for deep reflection. - // HostNameResolver will be unable to bypass the IP→hostname cache and will fall back - // to the cache keyed by IP address. - System.setProperty("dd.trace.java-module.enabled", "false"); - } - /** * Verifies the fallback behaviour when the JPMS instrumentation is disabled: HostNameResolver * cannot reflectively read the pre-set hostname from InetAddress and falls back to a cache keyed @@ -32,6 +29,7 @@ class JpmsInetAddressDisabledForkedTest extends AbstractInstrumentationTest { */ @Test void withoutJpmsInstrumentationIpCausesStaleHostnameToBeReturned() throws Exception { + assumeFalse(JavaVirtualMachine.isJ9(), "Does not work on J9"); // different subnet from the enabled-test to avoid cross-test cache pollution byte[] ip = {(byte) 192, 0, 2, 2}; InetAddress addr1 = InetAddress.getByAddress("service1.example.com", ip); From d2f0ae67b57023a6a6f05fa9b8c9beacf0745d8e Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Tue, 5 May 2026 14:35:42 +0200 Subject: [PATCH 3/5] Update dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/module/JpmsHelper.java Co-authored-by: Stuart McCulloch --- .../trace/bootstrap/instrumentation/java/module/JpmsHelper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/module/JpmsHelper.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/module/JpmsHelper.java index 490d7d8130e..6e0271ccb70 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/module/JpmsHelper.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/module/JpmsHelper.java @@ -54,7 +54,7 @@ public static void logFailedToOpen(String pkg, Throwable t) { } /** Called from inlined ByteBuddy advice; logs when a class has no named module. */ - public static void logNullModule(Class cls) { + public static void logNoNamedModule(Class cls) { LOGGER.debug("Class {} has no named module; skipping module open", cls.getName()); } } From 9a5e84348f03e9a5030705b646e0c2adafb3ccea Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Tue, 5 May 2026 14:35:54 +0200 Subject: [PATCH 4/5] Update dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/module/JpmsHelper.java Co-authored-by: Stuart McCulloch --- .../trace/bootstrap/instrumentation/java/module/JpmsHelper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/module/JpmsHelper.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/module/JpmsHelper.java index 6e0271ccb70..1954b51637f 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/module/JpmsHelper.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/module/JpmsHelper.java @@ -55,6 +55,6 @@ public static void logFailedToOpen(String pkg, Throwable t) { /** Called from inlined ByteBuddy advice; logs when a class has no named module. */ public static void logNoNamedModule(Class cls) { - LOGGER.debug("Class {} has no named module; skipping module open", cls.getName()); + LOGGER.debug("{} has no named module; skipping module open", cls); } } From 28e91264ec81ac4c5654228a30a25b11765f9ca0 Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Tue, 5 May 2026 14:41:47 +0200 Subject: [PATCH 5/5] human suggestions --- .../lang/module/JpmsClearanceInstrumentation.java | 9 ++++++--- .../httpclient/JpmsInetAddressInstrumentation.java | 4 +--- .../mule4/JpmsMuleInstrumentation.java | 11 +++-------- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/dd-java-agent/instrumentation/java/java-lang/java-lang-9.0/src/main/java/datadog/trace/instrumentation/java/lang/module/JpmsClearanceInstrumentation.java b/dd-java-agent/instrumentation/java/java-lang/java-lang-9.0/src/main/java/datadog/trace/instrumentation/java/lang/module/JpmsClearanceInstrumentation.java index b8b1b8dba67..c3997003d73 100644 --- a/dd-java-agent/instrumentation/java/java-lang/java-lang-9.0/src/main/java/datadog/trace/instrumentation/java/lang/module/JpmsClearanceInstrumentation.java +++ b/dd-java-agent/instrumentation/java/java-lang/java-lang-9.0/src/main/java/datadog/trace/instrumentation/java/lang/module/JpmsClearanceInstrumentation.java @@ -1,5 +1,8 @@ package datadog.trace.instrumentation.java.lang.module; +import static datadog.trace.bootstrap.instrumentation.java.module.JpmsHelper.logFailedToOpen; +import static datadog.trace.bootstrap.instrumentation.java.module.JpmsHelper.logNoNamedModule; +import static datadog.trace.bootstrap.instrumentation.java.module.JpmsHelper.shouldBeOpened; import static net.bytebuddy.matcher.ElementMatchers.isConstructor; import com.google.auto.service.AutoService; @@ -51,7 +54,7 @@ public static class OpenModuleAdvice { @Advice.OnMethodExit(suppress = Throwable.class) public static void onExit(@Advice.This(typing = Assigner.Typing.DYNAMIC) Object self) { final Class cls = self.getClass(); - if (JpmsHelper.shouldBeOpened(cls)) { + if (shouldBeOpened(cls)) { final Module module = cls.getModule(); final String pkg = cls.getPackageName(); if (module != null) { @@ -65,10 +68,10 @@ public static void onExit(@Advice.This(typing = Assigner.Typing.DYNAMIC) Object module.addOpens(pkg, loader.getUnnamedModule()); } } catch (Throwable t) { - JpmsHelper.logFailedToOpen(pkg, t); + logFailedToOpen(pkg, t); } } else { - JpmsHelper.logNullModule(cls); + logNoNamedModule(cls); } } } diff --git a/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/main/java/datadog/trace/instrumentation/httpclient/JpmsInetAddressInstrumentation.java b/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/main/java/datadog/trace/instrumentation/httpclient/JpmsInetAddressInstrumentation.java index b79f2c4268b..1a0c6a62a31 100644 --- a/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/main/java/datadog/trace/instrumentation/httpclient/JpmsInetAddressInstrumentation.java +++ b/dd-java-agent/instrumentation/java/java-net/java-net-11.0/src/main/java/datadog/trace/instrumentation/httpclient/JpmsInetAddressInstrumentation.java @@ -11,14 +11,12 @@ public class JpmsInetAddressInstrumentation extends InstrumenterModule implements JavaModuleOpenProvider { - private static final Collection TRIGGER_CLASSES = singleton("java.net.InetAddress"); - public JpmsInetAddressInstrumentation() { super("java-net"); } @Override public Collection triggerClasses() { - return TRIGGER_CLASSES; + return singleton("java.net.InetAddress"); } } diff --git a/dd-java-agent/instrumentation/mule-4.5/src/main/java/datadog/trace/instrumentation/mule4/JpmsMuleInstrumentation.java b/dd-java-agent/instrumentation/mule-4.5/src/main/java/datadog/trace/instrumentation/mule4/JpmsMuleInstrumentation.java index 10f9e010b64..171181670f8 100644 --- a/dd-java-agent/instrumentation/mule-4.5/src/main/java/datadog/trace/instrumentation/mule4/JpmsMuleInstrumentation.java +++ b/dd-java-agent/instrumentation/mule-4.5/src/main/java/datadog/trace/instrumentation/mule4/JpmsMuleInstrumentation.java @@ -1,7 +1,6 @@ package datadog.trace.instrumentation.mule4; import static java.util.Arrays.asList; -import static java.util.Collections.unmodifiableList; import com.google.auto.service.AutoService; import datadog.trace.agent.tooling.InstrumenterModule; @@ -12,12 +11,6 @@ @AutoService(InstrumenterModule.class) public class JpmsMuleInstrumentation extends InstrumenterModule implements JavaModuleOpenProvider { - private static final Collection TRIGGER_CLASSES = - unmodifiableList( - asList( - "org.mule.runtime.tracer.customization.impl.info.ExecutionInitialSpanInfo", - "org.mule.runtime.tracer.customization.impl.provider.LazyInitialSpanInfo")); - public JpmsMuleInstrumentation() { super("mule", "mule-jpms"); } @@ -44,6 +37,8 @@ public Reference[] additionalMuzzleReferences() { @Override public Collection triggerClasses() { - return TRIGGER_CLASSES; + return asList( + "org.mule.runtime.tracer.customization.impl.info.ExecutionInitialSpanInfo", + "org.mule.runtime.tracer.customization.impl.provider.LazyInitialSpanInfo"); } }