Skip to content

Commit 379803a

Browse files
committed
Add a jpms opener for HostNameResolver cache
1 parent c72f067 commit 379803a

8 files changed

Lines changed: 174 additions & 21 deletions

File tree

dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/java/net/HostNameResolver.java

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,39 +7,47 @@
77
import java.net.InetAddress;
88

99
public final class HostNameResolver {
10-
private static final MethodHandle HOLDER_GET;
11-
private static final MethodHandle HOSTNAME_GET;
10+
private static volatile MethodHandle HOLDER_GET;
11+
private static volatile MethodHandle HOSTNAME_GET;
1212

1313
private static final DDCache<String, String> HOSTNAME_CACHE = DDCaches.newFixedSizeCache(64);
1414

15-
static {
16-
MethodHandle holderTmp = null, hostnameTmp = null;
17-
try {
18-
final ClassLoader cl = HostNameResolver.class.getClassLoader();
19-
final MethodHandles methodHandles = new MethodHandles(cl);
15+
private HostNameResolver() {}
16+
17+
public static void tryInitialize() {
18+
if (HOLDER_GET != null) {
19+
return; // fast path: already initialized
20+
}
21+
synchronized (HostNameResolver.class) {
22+
if (HOLDER_GET != null) {
23+
return; // double-check: another thread just succeeded
24+
}
25+
MethodHandle holderTmp = null, hostnameTmp = null;
26+
try {
27+
final ClassLoader cl = HostNameResolver.class.getClassLoader();
28+
final MethodHandles methodHandles = new MethodHandles(cl);
2029

21-
final Class<?> holderClass =
22-
Class.forName("java.net.InetAddress$InetAddressHolder", false, cl);
23-
holderTmp = methodHandles.method(InetAddress.class, "holder");
24-
if (holderTmp != null) {
25-
hostnameTmp = methodHandles.method(holderClass, "getHostName");
30+
final Class<?> holderClass =
31+
Class.forName("java.net.InetAddress$InetAddressHolder", false, cl);
32+
holderTmp = methodHandles.method(InetAddress.class, "holder");
33+
if (holderTmp != null) {
34+
hostnameTmp = methodHandles.method(holderClass, "getHostName");
35+
}
36+
} catch (Throwable ignored) {
37+
holderTmp = null;
2638
}
27-
} catch (Throwable ignored) {
28-
holderTmp = null;
29-
} finally {
39+
// volatile writes ensure visibility to other threads
3040
if (holderTmp != null && hostnameTmp != null) {
31-
HOLDER_GET = holderTmp;
3241
HOSTNAME_GET = hostnameTmp;
33-
} else {
34-
HOLDER_GET = null;
35-
HOSTNAME_GET = null;
42+
HOLDER_GET = holderTmp; // written last: signals successful initialization
3643
}
3744
}
3845
}
3946

40-
private HostNameResolver() {}
41-
4247
static String getAlreadyResolvedHostName(InetAddress address) {
48+
if (HOLDER_GET == null) {
49+
tryInitialize();
50+
}
4351
if (HOLDER_GET == null) {
4452
return null;
4553
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package datadog.trace.bootstrap.instrumentation.java.net;
2+
3+
import java.util.concurrent.atomic.AtomicBoolean;
4+
5+
public class JpmsInetAddressHelper {
6+
public static final AtomicBoolean OPENED = new AtomicBoolean(false);
7+
8+
private JpmsInetAddressHelper() {}
9+
}

dd-java-agent/agent-tooling/src/main/resources/datadog/trace/agent/tooling/bytebuddy/matcher/ignored_class_name.trie

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
0 java.lang.VirtualThread
6060
0 java.net.http.*
6161
0 java.net.HttpURLConnection
62+
0 java.net.InetAddress
6263
0 java.net.Socket
6364
0 java.net.URL
6465
0 java.nio.DirectByteBuffer
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package datadog.trace.instrumentation.httpclient;
2+
3+
import static net.bytebuddy.matcher.ElementMatchers.isConstructor;
4+
5+
import com.google.auto.service.AutoService;
6+
import datadog.environment.JavaVirtualMachine;
7+
import datadog.trace.agent.tooling.Instrumenter;
8+
import datadog.trace.agent.tooling.InstrumenterModule;
9+
10+
@AutoService(InstrumenterModule.class)
11+
public class JpmsInetAddressInstrumentation extends InstrumenterModule.Tracing
12+
implements Instrumenter.HasMethodAdvice, Instrumenter.ForSingleType {
13+
14+
public JpmsInetAddressInstrumentation() {
15+
super("java-net");
16+
}
17+
18+
@Override
19+
public boolean isEnabled() {
20+
return super.isEnabled() && JavaVirtualMachine.isJavaVersionAtLeast(9);
21+
}
22+
23+
@Override
24+
public String instrumentedType() {
25+
return "java.net.InetAddress";
26+
}
27+
28+
@Override
29+
public void methodAdvice(MethodTransformer transformer) {
30+
// it does not work with typeInitializer()
31+
transformer.applyAdvice(isConstructor(), packageName + ".JpmsInetAddressClearanceAdvice");
32+
}
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package datadog.trace.instrumentation.httpclient;
2+
3+
import datadog.trace.bootstrap.instrumentation.java.net.HostNameResolver;
4+
import datadog.trace.bootstrap.instrumentation.java.net.JpmsInetAddressHelper;
5+
import java.net.InetAddress;
6+
import net.bytebuddy.asm.Advice;
7+
8+
public class JpmsInetAddressClearanceAdvice {
9+
@Advice.OnMethodExit(suppress = Throwable.class)
10+
public static void openOnReturn() {
11+
if (JpmsInetAddressHelper.OPENED.compareAndSet(false, true)) {
12+
// This call needs imperatively to be done from the same module we're adding opens to,
13+
// because the JDK checks that the caller belongs to the same module.
14+
// The code of this advice is inlined into the constructor of InetAddress (java.base),
15+
// so it will work. Moving the same call to a helper class won't.
16+
InetAddress.class.getModule().addOpens("java.net", HostNameResolver.class.getModule());
17+
// Now that java.net is open for deep reflection, initialize the HostNameResolver handles
18+
HostNameResolver.tryInitialize();
19+
}
20+
}
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package datadog.trace.instrumentation.httpclient
2+
3+
import datadog.trace.agent.test.InstrumentationSpecification
4+
import datadog.trace.bootstrap.instrumentation.java.net.HostNameResolver
5+
6+
class JpmsInetAddressDisabledForkedTest extends InstrumentationSpecification {
7+
8+
@Override
9+
protected void configurePreAgent() {
10+
super.configurePreAgent()
11+
// Disable the JPMS instrumentation so java.net is NOT opened for deep reflection.
12+
// HostNameResolver will be unable to bypass the IP→hostname cache and will fall back
13+
// to the cache keyed by IP address.
14+
injectSysConfig("dd.trace.java-net.enabled", "false")
15+
}
16+
17+
/**
18+
* Verifies the fallback behaviour when the JPMS instrumentation is disabled:
19+
* HostNameResolver cannot reflectively read the pre-set hostname from InetAddress and
20+
* falls back to a cache keyed by IP address. As a result, once a hostname has been
21+
* cached for a given IP, every subsequent lookup for that IP returns the first cached
22+
* value, even when the InetAddress object carries a different hostname.
23+
*
24+
* This is the broken behaviour that the JPMS instrumentation is designed to fix.
25+
*/
26+
def "without JPMS instrumentation, IP cache causes stale hostname to be returned"() {
27+
given:
28+
def ip = [192, 0, 2, 2] as byte[] // different subnet from the enabled-test to avoid cross-test cache pollution
29+
def addr1 = InetAddress.getByAddress("service1.example.com", ip)
30+
// Prime the IP→hostname cache with service1's hostname
31+
HostNameResolver.hostName(addr1, "192.0.2.2")
32+
33+
when: "a second service with the same IP but a different hostname is resolved"
34+
def addr2 = InetAddress.getByAddress("service2.example.com", ip)
35+
def result = HostNameResolver.hostName(addr2, "192.0.2.2")
36+
37+
then: "the stale cached hostname of service1 is returned instead of service2's"
38+
result == "service1.example.com"
39+
}
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package datadog.trace.instrumentation.httpclient
2+
3+
import datadog.trace.agent.test.InstrumentationSpecification
4+
import datadog.trace.bootstrap.instrumentation.java.net.HostNameResolver
5+
6+
class JpmsInetAddressForkedTest extends InstrumentationSpecification {
7+
8+
/**
9+
* Verifies that the JPMS instrumentation opens java.base/java.net so that
10+
* HostNameResolver can bypass its IP→hostname cache and return the correct
11+
* peer.hostname even when multiple services share a single IP address
12+
* (e.g. services behind a reverse proxy).
13+
*
14+
* Without the fix, HostNameResolver cannot reflectively access
15+
* InetAddress$InetAddressHolder on Java 9+ and falls back to a cache keyed
16+
* by IP, causing the first service's hostname to be returned for all
17+
* subsequent services on the same IP.
18+
*/
19+
def "instrumentation opens java.net so hostname is resolved correctly when IP is shared"() {
20+
given:
21+
def ip = [192, 0, 2, 1] as byte[] // TEST-NET, will never appear in real DNS cache
22+
def addr1 = InetAddress.getByAddress("service1.example.com", ip)
23+
// Warm the IP→hostname cache with service1's hostname
24+
HostNameResolver.hostName(addr1, "192.0.2.1")
25+
26+
when: "a second service with the same IP but different hostname is resolved"
27+
def addr2 = InetAddress.getByAddress("service2.example.com", ip)
28+
def result = HostNameResolver.hostName(addr2, "192.0.2.1")
29+
30+
then: "the hostname of addr2 is returned, not the cached hostname of addr1"
31+
result == "service2.example.com"
32+
}
33+
}

metadata/supported-configurations.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6737,6 +6737,14 @@
67376737
"aliases": ["DD_TRACE_INTEGRATION_JAVA_HTTP_CLIENT_ENABLED", "DD_INTEGRATION_JAVA_HTTP_CLIENT_ENABLED"]
67386738
}
67396739
],
6740+
"DD_TRACE_JAVA_NET_ENABLED": [
6741+
{
6742+
"version": "A",
6743+
"type": "boolean",
6744+
"default": "true",
6745+
"aliases": ["DD_TRACE_INTEGRATION_JAVA_NET_ENABLED", "DD_INTEGRATION_JAVA_NET_ENABLED"]
6746+
}
6747+
],
67406748
"DD_TRACE_TRACE_FFM_ANALYTICS_ENABLED": [
67416749
{
67426750
"version": "A",

0 commit comments

Comments
 (0)