From 08cc62a6b602506425da3829ef25e6278a86b00e Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Thu, 10 Jul 2025 17:59:46 -0700 Subject: [PATCH] Simplify JarFileUrlKey to prevent issues with Cortex XDR agent Replace the string based `JarFileUrlKey` and cache with a simple key built directly from URL components. This should allow cache lookups to remain fast whilst removing any `URLStreamHandler.getHostAddress` calls. Prior to this commit, we assumed that it was safe to use the URL directly as a cache key as long as it had an empty or null `host`. This assumption isn't correct when Palo Alto Network Cortex XDR agent is present as it appears to intercept calls to `getHostAddress` and, unlike the JDK code, always perform a DNS lookup. Fixes gh-46401 --- .../boot/loader/net/protocol/jar/Handler.java | 1 - .../net/protocol/jar/JarFileUrlKey.java | 82 ++++++++----------- .../loader/net/protocol/jar/UrlJarFiles.java | 8 +- .../net/protocol/jar/JarFileUrlKeyTests.java | 64 +++++++++------ 4 files changed, 79 insertions(+), 76 deletions(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/Handler.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/Handler.java index 780f3685676..11cc23301fc 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/Handler.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/Handler.java @@ -183,7 +183,6 @@ public class Handler extends URLStreamHandler { * Clear any internal caches. */ public static void clearCache() { - JarFileUrlKey.clearCache(); JarUrlConnection.clearCache(); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/JarFileUrlKey.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/JarFileUrlKey.java index 404de257d4c..3d310b7e1f2 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/JarFileUrlKey.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/JarFileUrlKey.java @@ -16,68 +16,56 @@ package org.springframework.boot.loader.net.protocol.jar; -import java.lang.ref.SoftReference; import java.net.URL; -import java.util.Locale; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; +import java.util.Objects; /** - * Utility to generate a string key from a jar file {@link URL} that can be used as a - * cache key. + * A fast cache key for a jar file {@link URL} that doesn't trigger DNS lookups. * * @author Phillip Webb */ final class JarFileUrlKey { - private static volatile SoftReference> cache; + private final String protocol; - private JarFileUrlKey() { + private final String host; + + private final int port; + + private final String file; + + private final boolean runtimeRef; + + JarFileUrlKey(URL url) { + this.protocol = url.getProtocol(); + this.host = url.getHost(); + this.port = (url.getPort() != -1) ? url.getPort() : url.getDefaultPort(); + this.file = url.getFile(); + this.runtimeRef = "runtime".equals(url.getRef()); } - /** - * Get the {@link JarFileUrlKey} for the given URL. - * @param url the source URL - * @return a {@link JarFileUrlKey} instance - */ - static String get(URL url) { - if (!isCachableUrl(url)) { - return create(url); - } - Map cache = (JarFileUrlKey.cache != null) ? JarFileUrlKey.cache.get() : null; - if (cache == null) { - cache = new ConcurrentHashMap<>(); - JarFileUrlKey.cache = new SoftReference<>(cache); - } - return cache.computeIfAbsent(url, JarFileUrlKey::create); + @Override + public int hashCode() { + return Objects.hashCode(this.file); } - private static boolean isCachableUrl(URL url) { - // Don't cache URL that have a host since equals() will perform DNS lookup - return url.getHost() == null || url.getHost().isEmpty(); + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + JarFileUrlKey other = (JarFileUrlKey) obj; + // We check file first as case sensitive and the most likely item to be different + return Objects.equals(this.file, other.file) && equalsIgnoringCase(this.protocol, other.protocol) + && equalsIgnoringCase(this.host, other.host) && (this.port == other.port) + && (this.runtimeRef == other.runtimeRef); } - private static String create(URL url) { - StringBuilder value = new StringBuilder(); - String protocol = url.getProtocol(); - String host = url.getHost(); - int port = (url.getPort() != -1) ? url.getPort() : url.getDefaultPort(); - String file = url.getFile(); - value.append(protocol.toLowerCase(Locale.ROOT)); - value.append(":"); - if (host != null && !host.isEmpty()) { - value.append(host.toLowerCase(Locale.ROOT)); - value.append((port != -1) ? ":" + port : ""); - } - value.append((file != null) ? file : ""); - if ("runtime".equals(url.getRef())) { - value.append("#runtime"); - } - return value.toString(); - } - - static void clearCache() { - cache = null; + private boolean equalsIgnoringCase(String s1, String s2) { + return (s1 == s2) || (s1 != null && s1.equalsIgnoreCase(s2)); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/UrlJarFiles.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/UrlJarFiles.java index 5892d46eae8..e2dbb3a29e9 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/UrlJarFiles.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/UrlJarFiles.java @@ -144,7 +144,7 @@ class UrlJarFiles { */ private static final class Cache { - private final Map jarFileUrlToJarFile = new HashMap<>(); + private final Map jarFileUrlToJarFile = new HashMap<>(); private final Map jarFileToJarFileUrl = new HashMap<>(); @@ -154,7 +154,7 @@ class UrlJarFiles { * @return the cached {@link JarFile} or {@code null} */ JarFile get(URL jarFileUrl) { - String urlKey = JarFileUrlKey.get(jarFileUrl); + JarFileUrlKey urlKey = new JarFileUrlKey(jarFileUrl); synchronized (this) { return this.jarFileUrlToJarFile.get(urlKey); } @@ -180,7 +180,7 @@ class UrlJarFiles { * they were already there */ boolean putIfAbsent(URL jarFileUrl, JarFile jarFile) { - String urlKey = JarFileUrlKey.get(jarFileUrl); + JarFileUrlKey urlKey = new JarFileUrlKey(jarFileUrl); synchronized (this) { JarFile cached = this.jarFileUrlToJarFile.get(urlKey); if (cached == null) { @@ -200,7 +200,7 @@ class UrlJarFiles { synchronized (this) { URL removedUrl = this.jarFileToJarFileUrl.remove(jarFile); if (removedUrl != null) { - this.jarFileUrlToJarFile.remove(JarFileUrlKey.get(removedUrl)); + this.jarFileUrlToJarFile.remove(new JarFileUrlKey(removedUrl)); } } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/JarFileUrlKeyTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/JarFileUrlKeyTests.java index 03c64199b3e..575630819aa 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/JarFileUrlKeyTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/JarFileUrlKeyTests.java @@ -16,6 +16,7 @@ package org.springframework.boot.loader.net.protocol.jar; +import java.net.MalformedURLException; import java.net.URL; import org.junit.jupiter.api.BeforeAll; @@ -38,51 +39,66 @@ class JarFileUrlKeyTests { } @Test - void getCreatesKey() throws Exception { - URL url = new URL("jar:nested:/my.jar/!mynested.jar!/my/path"); - assertThat(JarFileUrlKey.get(url)).isEqualTo("jar:nested:/my.jar/!mynested.jar!/my/path"); + void equalsAndHashCode() throws Exception { + JarFileUrlKey k1 = key("jar:nested:/my.jar/!mynested.jar!/my/path"); + JarFileUrlKey k2 = key("jar:nested:/my.jar/!mynested.jar!/my/path"); + JarFileUrlKey k3 = key("jar:nested:/my.jar/!mynested.jar!/my/path2"); + assertThat(k1.hashCode()).isEqualTo(k2.hashCode()) + .isEqualTo("nested:/my.jar/!mynested.jar!/my/path".hashCode()); + assertThat(k1).isEqualTo(k1).isEqualTo(k2).isNotEqualTo(k3); } @Test - void getWhenUppercaseProtocolCreatesKey() throws Exception { - URL url = new URL("JAR:nested:/my.jar/!mynested.jar!/my/path"); - assertThat(JarFileUrlKey.get(url)).isEqualTo("jar:nested:/my.jar/!mynested.jar!/my/path"); + void equalsWhenUppercaseAndLowercaseProtocol() throws Exception { + JarFileUrlKey k1 = key("JAR:nested:/my.jar/!mynested.jar!/my/path"); + JarFileUrlKey k2 = key("jar:nested:/my.jar/!mynested.jar!/my/path"); + assertThat(k1).isEqualTo(k2); } @Test - void getWhenHasHostAndPortCreatesKey() throws Exception { - URL url = new URL("https://example.com:1234/test"); - assertThat(JarFileUrlKey.get(url)).isEqualTo("https:example.com:1234/test"); + void equalsWhenHasHostAndPort() throws Exception { + JarFileUrlKey k1 = key("https://example.com:1234/test"); + JarFileUrlKey k2 = key("https://example.com:1234/test"); + assertThat(k1).isEqualTo(k2); } @Test - void getWhenHasUppercaseHostCreatesKey() throws Exception { - URL url = new URL("https://EXAMPLE.com:1234/test"); - assertThat(JarFileUrlKey.get(url)).isEqualTo("https:example.com:1234/test"); + void equalsWhenHasUppercaseAndLowercaseHost() throws Exception { + JarFileUrlKey k1 = key("https://EXAMPLE.com:1234/test"); + JarFileUrlKey k2 = key("https://example.com:1234/test"); + assertThat(k1).isEqualTo(k2); } @Test - void getWhenHasNoPortCreatesKeyWithDefaultPort() throws Exception { - URL url = new URL("https://EXAMPLE.com/test"); - assertThat(JarFileUrlKey.get(url)).isEqualTo("https:example.com:443/test"); + void equalsWhenHasNoPortUsesDefaultPort() throws Exception { + JarFileUrlKey k1 = key("https://EXAMPLE.com/test"); + JarFileUrlKey k2 = key("https://example.com:443/test"); + assertThat(k1).isEqualTo(k2); } @Test - void getWhenHasNoFileCreatesKey() throws Exception { - URL url = new URL("https://EXAMPLE.com"); - assertThat(JarFileUrlKey.get(url)).isEqualTo("https:example.com:443"); + void equalsWhenHasNoFile() throws Exception { + JarFileUrlKey k1 = key("https://EXAMPLE.com"); + JarFileUrlKey k2 = key("https://example.com:443"); + assertThat(k1).isEqualTo(k2); } @Test - void getWhenHasRuntimeRefCreatesKey() throws Exception { - URL url = new URL("jar:nested:/my.jar/!mynested.jar!/my/path#runtime"); - assertThat(JarFileUrlKey.get(url)).isEqualTo("jar:nested:/my.jar/!mynested.jar!/my/path#runtime"); + void equalsWhenHasRuntimeRef() throws Exception { + JarFileUrlKey k1 = key("jar:nested:/my.jar/!mynested.jar!/my/path#runtime"); + JarFileUrlKey k2 = key("jar:nested:/my.jar/!mynested.jar!/my/path#runtime"); + assertThat(k1).isEqualTo(k2); } @Test - void getWhenHasOtherRefCreatesKeyWithoutRef() throws Exception { - URL url = new URL("jar:nested:/my.jar/!mynested.jar!/my/path#example"); - assertThat(JarFileUrlKey.get(url)).isEqualTo("jar:nested:/my.jar/!mynested.jar!/my/path"); + void equalsWhenHasOtherRefIgnoresRefs() throws Exception { + JarFileUrlKey k1 = key("jar:nested:/my.jar/!mynested.jar!/my/path#example"); + JarFileUrlKey k2 = key("jar:nested:/my.jar/!mynested.jar!/my/path"); + assertThat(k1).isEqualTo(k2); + } + + private JarFileUrlKey key(String spec) throws MalformedURLException { + return new JarFileUrlKey(new URL(spec)); } }