Skip to content

Commit 1b08eec

Browse files
Unit test for rsrc leakage in unzip
create a temp zip file > create a destination that is a file not a directory (guaranteed exception) -> unzip throws ioexception because it expects a directory not a file -> catch it -> check if the zip file is still open -> if true == leak.
1 parent dac0b65 commit 1b08eec

1 file changed

Lines changed: 81 additions & 0 deletions

File tree

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package processing.app;
2+
3+
import org.junit.jupiter.api.Test;
4+
import org.junit.jupiter.api.Assumptions;
5+
import java.io.File;
6+
import java.io.FileOutputStream;
7+
import java.io.IOException;
8+
import java.nio.file.Files;
9+
import java.nio.file.Path;
10+
import java.nio.file.Paths;
11+
import java.util.stream.Stream;
12+
import java.util.zip.ZipEntry;
13+
import java.util.zip.ZipOutputStream;
14+
15+
import static org.junit.jupiter.api.Assertions.assertFalse;
16+
import static org.junit.jupiter.api.Assertions.assertTrue;
17+
18+
public class UtilTest {
19+
20+
@Test
21+
public void unzipLeaksFileDescriptorsOnException() throws IOException {
22+
// thi only runs on Linux where /proc/self/fd exists otherwise skip
23+
Assumptions.assumeTrue(new File("/proc/self/fd").exists(),
24+
"Skipping test: /proc/self/fd not available (not Linux)");
25+
// create a temporary zip file here with one entry
26+
File zipFile = File.createTempFile("leak-test", ".zip");
27+
zipFile.deleteOnExit();
28+
File destDir = File.createTempFile("dest", "");
29+
destDir.delete(); // turn into a directory
30+
destDir.mkdirs();
31+
destDir.deleteOnExit();
32+
// build a simple zip file
33+
try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) {
34+
ZipEntry entry = new ZipEntry("test.txt");
35+
zos.putNextEntry(entry);
36+
zos.write("hello".getBytes());
37+
zos.closeEntry();
38+
}
39+
// make the destination directory read‑only – this will cause extraction to fail
40+
destDir.setReadOnly();
41+
boolean exceptionThrown = false;
42+
try {
43+
Util.unzip(zipFile, destDir);
44+
} catch (IOException e) {
45+
exceptionThrown = true;
46+
}
47+
assertTrue(exceptionThrown, "Expected an exception because destDir is read‑only");
48+
49+
// check if the file is open by examining /proc/self/fd symlinks
50+
boolean fileStillOpen = isFileOpen(zipFile);
51+
assertFalse(fileStillOpen, "File " + zipFile + " is still open after exception – leak detected");
52+
53+
destDir.setWritable(true);
54+
destDir.delete();
55+
zipFile.delete();
56+
}
57+
58+
/**
59+
* Checks whether the given file is currently open by the current process.
60+
* Works on Linux by reading the symlinks in /proc/self/fd.
61+
*/
62+
private boolean isFileOpen(File file) throws IOException {
63+
Path fdDir = Paths.get("/proc/self/fd");
64+
String targetPath = file.getCanonicalPath();
65+
66+
try (Stream<Path> fdPaths = Files.list(fdDir)) {
67+
return fdPaths
68+
.map(Path::toFile)
69+
.map(File::toPath)
70+
.map(path -> {
71+
try {
72+
return Files.readSymbolicLink(path);
73+
} catch (IOException e) {
74+
return null; // not a symlink or inaccessible
75+
}
76+
})
77+
.filter(resolved -> resolved != null)
78+
.anyMatch(resolved -> resolved.toString().equals(targetPath));
79+
}
80+
}
81+
}

0 commit comments

Comments
 (0)