Skip to content

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

Open
kratos0718 wants to merge 2 commits 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 2 commits 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

// Sorting of list is done by name to ensure a bytecode hash is always consistent.
entries.sort(Comparator.comparing(ZipEntry::getName));
// Sorting of list is done by name to ensure a bytecode hash is always consistent.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorting of list is done by name --> Sort list by name

Manifest manifest = jarFile.getManifest();

this.jarData = new JarData(file, manifest, entries);
} catch (IOException | RuntimeException e) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't catch raw RuntimeException

List<JarEntry> entries = Collections.list(jarFile.entries());
try {
// Obtain entries list.
List<JarEntry> entries = Collections.list(jarFile.entries());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this throw? why is it in the try block?

@kratos0718

Copy link
Copy Markdown
Author

All three addressed, and the middle one changed the shape of the fix for the better.

Catching RuntimeException was me reaching for the wrong tool. What the constructor actually needs is a release-on-failure guarantee, not an exception handler, so it now uses a constructed flag with finally and catches nothing at all. That also covers Error paths the old version silently missed.

On the entries list and the sort being inside the try: they are still in it, but the block is no longer a catch, so the question changes. Nothing in those two lines throws anything worth handling. They sit inside the finally scope only because the JarFile is already open by then, and anything that goes wrong between opening it and assigning jarData leaks the handle, since the constructor never returns and the caller gets no reference to close.

Comment reworded. mvn test is 78 passing.

@elharo elharo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your try block isn't big enough. It should cover mist of the constructor to make finally work

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

2 participants