This is an automated email from the ASF dual-hosted git repository. rmaucher pushed a commit to branch 11.0.x in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 959efc91f2093e92a6759ef5df69236d443a6579 Author: opencode <[email protected]> AuthorDate: Fri Oct 16 13:22:07 2026 +0200 Ignore extra close() calls in ReferenceCountedJar An extra close() call (more closes than opens) drove the reference count below zero. Since the wrapped Jar is only closed when a balanced close returns the count to exactly zero, once the count went negative no subsequent open()/close() cycle would close the wrapped Jar again, leaking it and its file handle until the garbage collector ran the cleaner. Extra close() calls are now ignored, saturating the count at zero. Current callers all balance their references, so this hardens the public API rather than fixing an active leak. --- .../tomcat/util/scan/ReferenceCountedJar.java | 6 +++++ .../tomcat/util/scan/TestReferenceCountedJar.java | 31 ++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/java/org/apache/tomcat/util/scan/ReferenceCountedJar.java b/java/org/apache/tomcat/util/scan/ReferenceCountedJar.java index 83f3dc1bce..6330ae3c24 100644 --- a/java/org/apache/tomcat/util/scan/ReferenceCountedJar.java +++ b/java/org/apache/tomcat/util/scan/ReferenceCountedJar.java @@ -61,6 +61,12 @@ public class ReferenceCountedJar implements Jar { @Override public synchronized void close() { + if (referenceCount <= 0) { + // More closes than opens have been requested. Ignore the extra + // close rather than letting the count go negative, which would + // prevent the wrapped Jar from ever being closed again. + return; + } referenceCount--; if (referenceCount == 0) { wrappedJar.close(); diff --git a/test/org/apache/tomcat/util/scan/TestReferenceCountedJar.java b/test/org/apache/tomcat/util/scan/TestReferenceCountedJar.java index 77a6c8a117..33775f1815 100644 --- a/test/org/apache/tomcat/util/scan/TestReferenceCountedJar.java +++ b/test/org/apache/tomcat/util/scan/TestReferenceCountedJar.java @@ -21,6 +21,7 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.lang.reflect.Field; import java.net.URI; import java.nio.charset.StandardCharsets; import java.util.jar.Attributes; @@ -179,4 +180,34 @@ public class TestReferenceCountedJar { Assert.assertEquals(CONTENT, readFully(is)); } } + + + /* + * Extra close() calls (more closes than opens) must not drive the reference count below zero. If the count were to + * go negative, subsequent open()/close() cycles would never return it to zero and the wrapped Jar would never be + * closed again, leaking it. + */ + @Test + public void testExtraCloseDoesNotGoNegative() throws Exception { + File file = createJar(); + URI jarUri = URI.create("jar:" + file.toURI().toURL().toExternalForm() + "!/"); + + ReferenceCountedJar jar = new ReferenceCountedJar(jarUri.toURL()); + Field countField = ReferenceCountedJar.class.getDeclaredField("referenceCount"); + countField.setAccessible(true); + + jar.close(); + Assert.assertEquals(0, countField.getInt(jar)); + + // Extra closes must be ignored rather than decrementing below zero. + jar.close(); + jar.close(); + Assert.assertEquals(0, countField.getInt(jar)); + + // A later open()/close() cycle must still be able to return to zero and release the wrapped Jar. + try (InputStream is = jar.getInputStream(ENTRY_NAME)) { + Assert.assertEquals(CONTENT, readFully(is)); + } + Assert.assertEquals(0, countField.getInt(jar)); + } } --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
