From f808b89e557d40cfc163e30efb7f0bec3d37e7cd Mon Sep 17 00:00:00 2001 From: Aayush Srivastava Date: Fri, 14 Aug 2026 19:47:58 +0530 Subject: [PATCH] Do not close cached JarFile obtained from JarURLConnection Closing the shared cached instance breaks concurrent readers and classloading, e.g. Spring Boot nested jars. Closes #4541. --- CHANGELOG.next-release.md | 2 ++ .../sdk/bytebuddy/CustomElementMatchers.java | 8 ++++-- .../bytebuddy/CustomElementMatchersTest.java | 26 +++++++++++++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.next-release.md b/CHANGELOG.next-release.md index 91ab0d62d03..343bbfbad86 100644 --- a/CHANGELOG.next-release.md +++ b/CHANGELOG.next-release.md @@ -10,6 +10,8 @@ This file contains all changes which are not released yet. # Fixes +* Fix `JarFile` obtained from a caching `JarURLConnection` being closed while still shared/in-use, which could crash Spring Boot executable jar startups - [#4541](https://github.com/elastic/apm-agent-java/issues/4541) + # Features and enhancements diff --git a/apm-agent-plugin-sdk/src/main/java/co/elastic/apm/agent/sdk/bytebuddy/CustomElementMatchers.java b/apm-agent-plugin-sdk/src/main/java/co/elastic/apm/agent/sdk/bytebuddy/CustomElementMatchers.java index c2a07a593db..6ba59f94999 100644 --- a/apm-agent-plugin-sdk/src/main/java/co/elastic/apm/agent/sdk/bytebuddy/CustomElementMatchers.java +++ b/apm-agent-plugin-sdk/src/main/java/co/elastic/apm/agent/sdk/bytebuddy/CustomElementMatchers.java @@ -233,6 +233,7 @@ public boolean matches(@Nullable ProtectionDomain protectionDomain) { private static Version readImplementationVersion(@Nullable ProtectionDomain protectionDomain, @Nullable String mavenGroupId, @Nullable String mavenArtifactId) throws IOException, URISyntaxException { Version version = null; JarFile jarFile = null; + boolean closeJarFile = true; if (protectionDomain == null) { logger.info("Cannot read implementation version - got null ProtectionDomain"); @@ -247,7 +248,10 @@ private static Version readImplementationVersion(@Nullable ProtectionDomain prot // does not yet establish an actual connection URLConnection urlConnection = jarUrl.openConnection(); if (urlConnection instanceof JarURLConnection) { - jarFile = ((JarURLConnection) urlConnection).getJarFile(); + JarURLConnection jarURLConnection = (JarURLConnection) urlConnection; + // JarURLConnection may return a shared cached JarFile that must not be closed by the caller + jarFile = jarURLConnection.getJarFile(); + closeJarFile = !jarURLConnection.getUseCaches(); } else { jarFile = new JarFile(new File(jarUrl.toURI())); } @@ -291,7 +295,7 @@ private static Version readImplementationVersion(@Nullable ProtectionDomain prot } } } finally { - if (jarFile != null) { + if (jarFile != null && closeJarFile) { try { jarFile.close(); } catch (IOException e) { diff --git a/apm-agent-plugin-sdk/src/test/java/co/elastic/apm/agent/sdk/bytebuddy/CustomElementMatchersTest.java b/apm-agent-plugin-sdk/src/test/java/co/elastic/apm/agent/sdk/bytebuddy/CustomElementMatchersTest.java index 20398a47bd3..4a6165767ad 100644 --- a/apm-agent-plugin-sdk/src/test/java/co/elastic/apm/agent/sdk/bytebuddy/CustomElementMatchersTest.java +++ b/apm-agent-plugin-sdk/src/test/java/co/elastic/apm/agent/sdk/bytebuddy/CustomElementMatchersTest.java @@ -25,6 +25,8 @@ import java.io.File; import java.io.IOException; +import java.io.InputStream; +import java.net.JarURLConnection; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; @@ -41,6 +43,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; import static co.elastic.apm.agent.sdk.bytebuddy.CustomElementMatchers.classLoaderCanLoadClass; import static co.elastic.apm.agent.sdk.bytebuddy.CustomElementMatchers.implementationVersionGte; @@ -66,6 +70,28 @@ void testSemVerLteWithJarFileUrl() throws MalformedURLException { testSemVerLteMatcher(new ProtectionDomain(new CodeSource(jarFileUrl, new CodeSigner[0]), null)); } + @Test + void testSemVerLteWithJarFileUrlDoesNotCloseSharedCachedJarFile() throws IOException { + // JarURLConnection#getJarFile() returns a shared cached JarFile instance by default (getUseCaches() == true). + // Reading the implementation version must not close that shared instance, as other code may still be using it. + URL originalUrl = HttpClient.class.getProtectionDomain().getCodeSource().getLocation(); + URL jarFileUrl = new URL("jar:" + originalUrl.toString() + "!/"); + + JarURLConnection jarURLConnection = (JarURLConnection) jarFileUrl.openConnection(); + assertThat(jarURLConnection.getUseCaches()).isTrue(); + JarFile cachedJarFile = jarURLConnection.getJarFile(); + + ProtectionDomain protectionDomain = new ProtectionDomain(new CodeSource(jarFileUrl, new CodeSigner[0]), null); + assertThat(implementationVersionLte("5").matches(protectionDomain)).isTrue(); + + // the shared cached JarFile must still be usable after the matcher ran + JarEntry manifestEntry = cachedJarFile.getJarEntry("META-INF/MANIFEST.MF"); + assertThat(manifestEntry).isNotNull(); + try (InputStream input = cachedJarFile.getInputStream(manifestEntry)) { + assertThat(input.read()).isNotEqualTo(-1); + } + } + @Test void testSemVerLteWithEncodedFileUrl() throws MalformedURLException, URISyntaxException { String jarFileUrl = new File("src/test/resources/lib/version##2/test-module.jar").toURI().toASCIIString();