Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.next-release.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ This file contains all changes which are not released yet.
# Fixes
<!--FIXES-START-->

* 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)

<!--FIXES-END-->
# Features and enhancements
<!--ENHANCEMENTS-START-->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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()));
}
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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();
Expand Down
Loading