MUN JEONGHO created COMPRESS-730:
------------------------------------

             Summary: SevenZFile.Builder.get() leaks the file channel it opens 
when the archive headers cannot be read
                 Key: COMPRESS-730
                 URL: https://issues.apache.org/jira/browse/COMPRESS-730
             Project: Commons Compress
          Issue Type: Bug
          Components: Archivers
    Affects Versions: 1.27.1
            Reporter: MUN JEONGHO
         Attachments: SevenZFileBuilderLeak.java

When a {{SevenZFile}} is created with {{SevenZFile.builder().setFile(...)}} (or 
{{setPath(...)}}) and {{get()}} fails while reading the archive headers 
(corrupted/truncated archive, header-encrypted archive opened without a 
password or with a wrong one, ...), the {{SeekableByteChannel}} opened by the 
builder is never closed.

Because the exception is thrown from {{get()}}, the caller never receives a 
{{SevenZFile}} instance, so try-with-resources cannot close it either. One file 
descriptor leaks per failed call. NIO channels are not closed by GC on Java 8, 
so in a long-running server that receives broken 7z files the process 
eventually fails with "Too many open files".

h3. Cause

In {{SevenZFile.Builder.get()}}:
{code:java}
final boolean closeOnError = seekableByteChannel != null;
{code}
{{closeOnError}} is {{true}} only when the caller supplied the channel and 
{{false}} when the builder opened the file itself, which looks inverted. The 
constructor only closes the channel on failure when {{closeOnError}} is 
{{true}}:
{code:java}
if (!succeeded && closeOnError) {
    this.channel.close();
}
{code}
The deprecated {{SevenZFile(File)}} constructor does not leak in the same 
scenario.

The same line appears in the current source published on the project site:
https://commons.apache.org/proper/commons-compress/jacoco/org.apache.commons.compress.archivers.sevenz/SevenZFile.java.html

h3. Steps to reproduce (Linux, counts entries in /proc/self/fd)

See attached SevenZFileBuilderLeak.java:
{code:java}
// 7z signature followed by garbage
Path broken = Files.createTempFile("broken", ".7z");
byte[] data = new byte[64];
System.arraycopy(new byte[] {'7', 'z', (byte) 0xBC, (byte) 0xAF, 0x27, 0x1C}, 
0, data, 0, 6);
for (int i = 6; i < data.length; i++) {
    data[i] = (byte) i;
}
Files.write(broken, data);

for (int i = 0; i < 100; i++) {
    try (SevenZFile f = SevenZFile.builder().setFile(broken.toFile()).get()) {
        // not reached
    } catch (IOException expected) {
        // ignore
    }
}
{code}

h3. Actual result

Open file descriptors grow by about one per iteration:
* Java 8: before=24, after=126
* Java 21: before=7, after=109

The same loop with the deprecated {{new SevenZFile(broken.toFile())}}: 
before=24, after=26 (no growth).

h3. Expected result

When {{get()}} throws, the channel opened by the builder is closed, as the 
deprecated {{File}} constructors already do.

h3. Possible fix

Close the channel on error when the builder opened it, e.g.:
{code:java}
final boolean closeOnError = seekableByteChannel == null;
{code}
Whether a caller-supplied channel should still be closed on error is left to 
the maintainers. Closing it would change the behaviour for callers who pass 
their own channel.

h3. Workaround

Open the channel yourself, pass it with {{setSeekableByteChannel(...)}}, and 
close it if {{get()}} throws.



--
This message was sent by Atlassian Jira
(v10.3.26#10030026)

Reply via email to