Skip to content

Close the JarFile when JarAnalyzer's constructor fails - #166

Open
kratos0718 wants to merge 1 commit into
apache:masterfrom
kratos0718:fix/139-close-jarfile-on-constructor-failure
Open

Close the JarFile when JarAnalyzer's constructor fails#166
kratos0718 wants to merge 1 commit into
apache:masterfrom
kratos0718:fix/139-close-jarfile-on-constructor-failure

Conversation

@kratos0718

Copy link
Copy Markdown

Fixes #139

Problem

JarAnalyzer's constructor opens the JarFile, then lists and sorts its entries, and only afterwards reads the manifest:

this.jarFile = new JarFile(file);

List<JarEntry> entries = Collections.list(jarFile.entries());
entries.sort(Comparator.comparing(ZipEntry::getName));

Manifest manifest;
try {
    manifest = jarFile.getManifest();
} catch (IOException e) {
    closeQuietly();          // only this path cleans up
    throw e;
}

Only the manifest read is guarded. If the listing or the sort throws, the constructor exits with the JarFile still open — and because construction failed, the caller has no instance on which to call closeQuietly(), so the handle leaks. The class javadoc already promises the file "will be closed if this occurs".

Comparator.comparing(ZipEntry::getName) is one way in: it throws NullPointerException on an entry with a null name, which a malformed archive can carry.

Fix

Wrap everything between opening the file and assigning jarData, and release the handle on any IOException or RuntimeException before rethrowing. That covers the sort, the entry listing, and anything else added to this region later, rather than enumerating individual failure modes.

It also folds in the manifest cleanup that was already there, so there is one exit path instead of two — net effect is +11/-9 in a single method.

Note on #148

This is adjacent to your #148, which removes the ZipException re-wrap in the block immediately above. The two are semantically independent — that one changes how the open fails, this one changes what happens after a successful open — so they compose cleanly. Happy to rebase on top of it if it lands first, or to fold both into one change if you'd prefer.

Testing

mvn test — 78 tests across the module, all passing.

I did not add a test for the leak itself. Reproducing it needs an entry with a null name, which standard tooling won't produce, and asserting the descriptor was released needs the JarFile to be injectable — the class constructs it internally. On POSIX a "can the file be deleted afterwards" assertion passes whether or not the handle leaked, so it would be a test that only means something on Windows. I'd rather not add a misleading one, but if you want a specific approach here I'm happy to write it.

The constructor opened the JarFile and then listed and sorted its
entries before reading the manifest. Only the manifest read was guarded:
if listing or sorting threw, the constructor exited with the JarFile
still open, and the caller had no reference on which to call
closeQuietly(), so the handle leaked.

Comparator.comparing(ZipEntry::getName) is one way in - it throws a
NullPointerException on an entry with a null name, which a malformed
archive can carry.

Wrap everything between opening the file and assigning jarData, and
release the handle on any IOException or RuntimeException before
rethrowing. This also folds in the existing manifest cleanup, so there
is a single exit path rather than two.

Fixes apache#139
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Resource leak in JarAnalyzer constructor if entries.sort() throws

1 participant