Skip to content

Commit c85918b

Browse files
committed
Create new JarFile instance for URL connections
Update `JarURLConnection` to ensure that when connections are opened a new copy of the JarFile is provided. Prior to this commit, a single `JarFile` instance was shared which meant that it could be accidental closed if accessed via `JarURLConnection.getJarFile()`. If the underlying jar file is closed then it's possible for a `NoClassDefFoundError` to be thrown if running on JDK 11 with an active `SecurityManager`. Closes gh-17796
1 parent 6011470 commit c85918b

File tree

4 files changed

+60
-19
lines changed

4 files changed

+60
-19
lines changed

spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFile.java

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2019 the original author or authors.
2+
* Copyright 2012-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -60,6 +60,8 @@ public class JarFile extends java.util.jar.JarFile {
6060

6161
private static final AsciiBytes SIGNATURE_FILE_EXTENSION = new AsciiBytes(".SF");
6262

63+
private final JarFile parent;
64+
6365
private final RandomAccessDataFile rootFile;
6466

6567
private final String pathFromRoot;
@@ -100,6 +102,27 @@ public JarFile(File file) throws IOException {
100102
this(file, "", file, JarFileType.DIRECT);
101103
}
102104

105+
/**
106+
* Create a new JarFile copy based on a given parent.
107+
* @param parent the parent jar
108+
* @throws IOException if the file cannot be read
109+
*/
110+
JarFile(JarFile parent) throws IOException {
111+
super(parent.rootFile.getFile());
112+
this.parent = parent;
113+
this.rootFile = parent.rootFile;
114+
this.pathFromRoot = parent.pathFromRoot;
115+
this.data = parent.data;
116+
this.type = parent.type;
117+
this.url = parent.url;
118+
this.urlString = parent.urlString;
119+
this.entries = parent.entries;
120+
this.manifestSupplier = parent.manifestSupplier;
121+
this.manifest = parent.manifest;
122+
this.signed = parent.signed;
123+
this.comment = parent.comment;
124+
}
125+
103126
/**
104127
* Private constructor used to create a new {@link JarFile} either directly or from a
105128
* nested entry.
@@ -111,12 +134,13 @@ public JarFile(File file) throws IOException {
111134
*/
112135
private JarFile(RandomAccessDataFile rootFile, String pathFromRoot, RandomAccessData data, JarFileType type)
113136
throws IOException {
114-
this(rootFile, pathFromRoot, data, null, type, null);
137+
this(null, rootFile, pathFromRoot, data, null, type, null);
115138
}
116139

117-
private JarFile(RandomAccessDataFile rootFile, String pathFromRoot, RandomAccessData data, JarEntryFilter filter,
118-
JarFileType type, Supplier<Manifest> manifestSupplier) throws IOException {
140+
private JarFile(JarFile parent, RandomAccessDataFile rootFile, String pathFromRoot, RandomAccessData data,
141+
JarEntryFilter filter, JarFileType type, Supplier<Manifest> manifestSupplier) throws IOException {
119142
super(rootFile.getFile());
143+
this.parent = parent;
120144
this.rootFile = rootFile;
121145
this.pathFromRoot = pathFromRoot;
122146
CentralDirectoryParser parser = new CentralDirectoryParser();
@@ -166,6 +190,10 @@ public void visitEnd() {
166190
};
167191
}
168192

193+
JarFile getParent() {
194+
return this.parent;
195+
}
196+
169197
protected final RandomAccessDataFile getRootJarFile() {
170198
return this.rootFile;
171199
}
@@ -277,8 +305,9 @@ private JarFile createJarFileFromDirectoryEntry(JarEntry entry) throws IOExcepti
277305
}
278306
return null;
279307
};
280-
return new JarFile(this.rootFile, this.pathFromRoot + "!/" + entry.getName().substring(0, name.length() - 1),
281-
this.data, filter, JarFileType.NESTED_DIRECTORY, this.manifestSupplier);
308+
return new JarFile(this, this.rootFile,
309+
this.pathFromRoot + "!/" + entry.getName().substring(0, name.length() - 1), this.data, filter,
310+
JarFileType.NESTED_DIRECTORY, this.manifestSupplier);
282311
}
283312

284313
private JarFile createJarFileFromFileEntry(JarEntry entry) throws IOException {
@@ -306,7 +335,7 @@ public int size() {
306335
@Override
307336
public void close() throws IOException {
308337
super.close();
309-
if (this.type == JarFileType.DIRECT) {
338+
if (this.type == JarFileType.DIRECT && this.parent == null) {
310339
this.rootFile.close();
311340
}
312341
}
@@ -326,10 +355,9 @@ String getUrlString() throws MalformedURLException {
326355
*/
327356
public URL getUrl() throws MalformedURLException {
328357
if (this.url == null) {
329-
Handler handler = new Handler(this);
330358
String file = this.rootFile.getFile().toURI() + this.pathFromRoot + "!/";
331359
file = file.replace("file:////", "file://"); // Fix UNC paths
332-
this.url = new URL("jar", "", -1, file, handler);
360+
this.url = new URL("jar", "", -1, file, new Handler(this));
333361
}
334362
return this.url;
335363
}

spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarURLConnection.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2019 the original author or authors.
2+
* Copyright 2012-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -266,7 +266,7 @@ static JarURLConnection get(URL url, JarFile jarFile) throws IOException {
266266
&& !jarFile.containsEntry(jarEntryName.toString())) {
267267
return NOT_FOUND_CONNECTION;
268268
}
269-
return new JarURLConnection(url, jarFile, jarEntryName);
269+
return new JarURLConnection(url, new JarFile(jarFile), jarEntryName);
270270
}
271271

272272
private static int indexOfRootSpec(StringSequence file, String pathFromRoot) {

spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -205,10 +205,10 @@ public void getUrl() throws Exception {
205205
URL url = this.jarFile.getUrl();
206206
assertThat(url.toString()).isEqualTo("jar:" + this.rootJarFile.toURI() + "!/");
207207
JarURLConnection jarURLConnection = (JarURLConnection) url.openConnection();
208-
assertThat(jarURLConnection.getJarFile()).isSameAs(this.jarFile);
208+
assertThat(jarURLConnection.getJarFile().getParent()).isSameAs(this.jarFile);
209209
assertThat(jarURLConnection.getJarEntry()).isNull();
210210
assertThat(jarURLConnection.getContentLength()).isGreaterThan(1);
211-
assertThat(jarURLConnection.getContent()).isSameAs(this.jarFile);
211+
assertThat(((JarFile) jarURLConnection.getContent()).getParent()).isSameAs(this.jarFile);
212212
assertThat(jarURLConnection.getContentType()).isEqualTo("x-java/jar");
213213
assertThat(jarURLConnection.getJarFileURL().toURI()).isEqualTo(this.rootJarFile.toURI());
214214
}
@@ -218,7 +218,7 @@ public void createEntryUrl() throws Exception {
218218
URL url = new URL(this.jarFile.getUrl(), "1.dat");
219219
assertThat(url.toString()).isEqualTo("jar:" + this.rootJarFile.toURI() + "!/1.dat");
220220
JarURLConnection jarURLConnection = (JarURLConnection) url.openConnection();
221-
assertThat(jarURLConnection.getJarFile()).isSameAs(this.jarFile);
221+
assertThat(jarURLConnection.getJarFile().getParent()).isSameAs(this.jarFile);
222222
assertThat(jarURLConnection.getJarEntry()).isSameAs(this.jarFile.getJarEntry("1.dat"));
223223
assertThat(jarURLConnection.getContentLength()).isEqualTo(1);
224224
assertThat(jarURLConnection.getContent()).isInstanceOf(InputStream.class);
@@ -273,7 +273,7 @@ public void getNestedJarFile() throws Exception {
273273
URL url = nestedJarFile.getUrl();
274274
assertThat(url.toString()).isEqualTo("jar:" + this.rootJarFile.toURI() + "!/nested.jar!/");
275275
JarURLConnection conn = (JarURLConnection) url.openConnection();
276-
assertThat(conn.getJarFile()).isSameAs(nestedJarFile);
276+
assertThat(conn.getJarFile().getParent()).isSameAs(nestedJarFile);
277277
assertThat(conn.getJarFileURL().toString()).isEqualTo("jar:" + this.rootJarFile.toURI() + "!/nested.jar");
278278
assertThat(conn.getInputStream()).isNotNull();
279279
JarInputStream jarInputStream = new JarInputStream(conn.getInputStream());
@@ -301,7 +301,7 @@ public void getNestedJarDirectory() throws Exception {
301301

302302
URL url = nestedJarFile.getUrl();
303303
assertThat(url.toString()).isEqualTo("jar:" + this.rootJarFile.toURI() + "!/d!/");
304-
assertThat(((JarURLConnection) url.openConnection()).getJarFile()).isSameAs(nestedJarFile);
304+
assertThat(((JarURLConnection) url.openConnection()).getJarFile().getParent()).isSameAs(nestedJarFile);
305305
}
306306

307307
@Test

spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarURLConnectionTests.java

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2019 the original author or authors.
2+
* Copyright 2012-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -20,6 +20,7 @@
2020
import java.io.File;
2121
import java.io.FileNotFoundException;
2222
import java.net.URL;
23+
import java.util.zip.ZipFile;
2324

2425
import org.junit.Before;
2526
import org.junit.Rule;
@@ -28,6 +29,7 @@
2829

2930
import org.springframework.boot.loader.TestJarCreator;
3031
import org.springframework.boot.loader.jar.JarURLConnection.JarEntryName;
32+
import org.springframework.test.util.ReflectionTestUtils;
3133

3234
import static org.assertj.core.api.Assertions.assertThat;
3335
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@@ -58,13 +60,15 @@ public void setup() throws Exception {
5860
@Test
5961
public void connectionToRootUsingAbsoluteUrl() throws Exception {
6062
URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/");
61-
assertThat(JarURLConnection.get(url, this.jarFile).getContent()).isSameAs(this.jarFile);
63+
Object content = JarURLConnection.get(url, this.jarFile).getContent();
64+
assertThat(((JarFile) content).getParent()).isSameAs(this.jarFile);
6265
}
6366

6467
@Test
6568
public void connectionToRootUsingRelativeUrl() throws Exception {
6669
URL url = new URL("jar:file:" + getRelativePath() + "!/");
67-
assertThat(JarURLConnection.get(url, this.jarFile).getContent()).isSameAs(this.jarFile);
70+
Object content = JarURLConnection.get(url, this.jarFile).getContent();
71+
assertThat(((JarFile) content).getParent()).isSameAs(this.jarFile);
6872
}
6973

7074
@Test
@@ -191,6 +195,15 @@ public void jarEntryNameWithMixtureOfEncodedAndUnencodedDoubleByteCharacters() {
191195
.isEqualTo("\u00e1/b/\u00c7.class");
192196
}
193197

198+
@Test
199+
public void openConnectionCanBeClosedWithoutClosingSourceJar() throws Exception {
200+
URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/");
201+
JarURLConnection connection = JarURLConnection.get(url, this.jarFile);
202+
JarFile connectionJarFile = connection.getJarFile();
203+
connectionJarFile.close();
204+
assertThat((Boolean) ReflectionTestUtils.getField(this.jarFile, ZipFile.class, "closeRequested")).isFalse();
205+
}
206+
194207
private String getRelativePath() {
195208
return this.rootJarFile.getPath().replace('\\', '/');
196209
}

0 commit comments

Comments
 (0)