JDK-8193682 : Infinite loop in ZipOutputStream.close()
  • Type: Bug
  • Component: core-libs
  • Sub-Component: java.util.jar
  • Affected Version: 8,9,10,11,17
  • Priority: P4
  • Status: Closed
  • Resolution: Fixed
  • OS: linux
  • CPU: x86_64
  • Submitted: 2017-12-13
  • Updated: 2025-06-27
  • Resolved: 2021-12-08
The Version table provides details related to the release that this issue/RFE will be addressed.

Unresolved : Release in which this issue/RFE will be addressed.
Resolved: Release in which this issue/RFE has been resolved.
Fixed : Release in which this issue/RFE has been fixed. The release containing this fix may be available for download as an Early Access Release or a General Availability Release.

To download the current JDK release, click here.
JDK 11 JDK 13 JDK 15 JDK 17 JDK 18 JDK 7 JDK 8 Other
11.0.16-oracleFixed 13.0.12Fixed 15.0.8Fixed 17.0.4-oracleFixed 18 b26Fixed 7u351Fixed 8u341Fixed openjdk8u432,shenandoah8u432Fixed
Related Reports
CSR :  
Relates :  
Relates :  
Sub Tasks
JDK-8278386 :  
Description
FULL PRODUCT VERSION :
openjdk version "1.8.0_151"
OpenJDK Runtime Environment (build 1.8.0_151-b12)
OpenJDK 64-Bit Server VM (build 25.151-b12, mixed mode)

ADDITIONAL OS VERSION INFORMATION :
Linux 2.6.32-696.13.2.el6.x86_64 #1 SMP Thu Oct 5 21:22:16 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux

A DESCRIPTION OF THE PROBLEM :
ZipOutputStream.close() has the potential for falling into an infinite loop. We've seen this stall out request processing theads on our production tomcat server.

Our app has code similar to 

void generateZipFileResponse(OutputStream clientOut) {
    try(ZipOutputStream zos = new ZipOutputStream(clientOut);
         PrintWriter pw = new PrintWriter(zos)) {

         // start zip entry
         zos.putNextEntry(new ZipEntry("entry.txt"));

         // generate the entry contents
         generateEntry(pw);
         
         // close entry
         zos.closeEntry();
    }
}

Sometimes, when the client disconnects or the socket write times out, the underlying output stream is closed before the zip file is completely written. We've seen this cause the ARM block call of ZipOutputStream.close() to fall into an infinite loop

A stack trace of the stuck threads looks like

   java.lang.Thread.State: RUNNABLE
	at java.util.zip.Deflater.deflateBytes(Native Method)
	at java.util.zip.Deflater.deflate(Deflater.java:444)
        - locked <0x000000040b42b308> (a java.util.zip.ZStreamRef)
	at java.util.zip.Deflater.deflate(Deflater.java:366)
	at java.util.zip.DeflaterOutputStream.deflate(DeflaterOutputStream.java:251)
	at java.util.zip.ZipOutputStream.closeEntry(ZipOutputStream.java:255)
	at java.util.zip.ZipOutputStream.finish(ZipOutputStream.java:360)
	at java.util.zip.DeflaterOutputStream.close(DeflaterOutputStream.java:238)
	at java.util.zip.ZipOutputStream.close(ZipOutputStream.java:377)
        ...

I cannot reproduce this issue outside our production machines, however I believe I know what is happening.

ZipOutputStream.closeEntry() will be called 3 times by generateZipFileResponse() when the underlying SocketOutputStream is closed while it is executing. Since closeEntry() writes to the underlying closed SocketOutputStream, an exception will be generated during the first explicit call. However, the ARM block guarantees two calls to ZipOutputStream.close() which calls closeEntry() because the first call has not completed successfully

This while loop inside closeEntry() is never terminating

                def.finish();
                while (!def.finished()) {
                    deflate();
                }

DeflaterOutputStream.deflate() looks like

    protected void deflate() throws IOException {
        int len = def.deflate(buf, 0, buf.length);
        if (len > 0) {
            out.write(buf, 0, len);
        }
    }

If Deflater.deflate() returns 0 but doesn't update Deflater.finished to be true, no write will be made to the underlying SocketOutputStream and the loop in ZipOutputStream will never terminate

Deflater.deflate() ultimately calls Deflater.deflateBytes which it turns out, returns 0 on certain occasions. From Deflater.c


        res = deflate(strm, finish ? Z_FINISH : flush);
        (*env)->ReleasePrimitiveArrayCritical(env, b, out_buf, 0);
        (*env)->ReleasePrimitiveArrayCritical(env, this_buf, in_buf, 0);

        switch (res) {
        case Z_STREAM_END:
            (*env)->SetBooleanField(env, this, finishedID, JNI_TRUE);
            /* fall through */
        case Z_OK:
            this_off += this_len - strm->avail_in;
            (*env)->SetIntField(env, this, offID, this_off);
            (*env)->SetIntField(env, this, lenID, strm->avail_in);
            return len - strm->avail_out;
        case Z_BUF_ERROR:
            return 0;

It turns out, zlib deflate() can return Z_BUF_ERROR under one condition that doesn't mean the output-buffer-full/input-buffer-empty. From deflate.c

    /* User must not provide more input after the first FINISH: */
    if (s->status == FINISH_STATE && strm->avail_in != 0) {
        ERR_RETURN(strm, Z_BUF_ERROR);
    }
    
So here is what I think is happening:
1. The underlying SocketOutputStream is closed
2. The next write(s) to the PrintWriter generate IOExceptions whenever the DeflaterOutputStream flushes it's buffer, but these are discarded by PrintWriter. The last write leaves the Deflater part way through consuming it's input buffer
3. The explicit ZipOutputStream.closeEntry() sets Deflater.finish to true, calls Deflater.deflateBytes for one compression round which ultimately calls deflate() with Z_FINISH as the flush mode. This results in the internal zlib s->status being FINISH_STATE
4. ZipOutputStream.closeEntry() attempts to call write() on the underlying SocketOutputStream which generates an IOException
5. The ARM block calls PrintWriter.close() which ultimately calls ZipOutputStream.close() which calls ZipOutputStream.closeEntry() again
6. ZipOutputStream.closeEntry() once again enters the loop until finished, but the input buffer is still not completely consumed.
7. This time, calls to zlib's deflate() return Z_BUF_ERROR which results in no update to Deflater.finished and a return of 0 from Deflater.deflateBytes()
8. Since Deflater.deflateBytes() returned 0, DeflaterOutputStream.deflate() never attempts to write to the underlying closed SocketOutputStream
9. DeflaterOutputStream.deflate() returns successfully, but Deflater.finished is still false so the loop in ZipOutputStream.closeEntry() continues forever

STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
I have not been able to reproduce this issue outside of our production environment.


REPRODUCIBILITY :
This bug can be reproduced often.

CUSTOMER SUBMITTED WORKAROUND :
The only workaround I know of is to ensure ZipOutputStream.closeEntry() is only called once, since the first call is guaranteed to complete with an IOException.

This can be done for single entry zip files by omitting the explicit call to ZipOutputStream.closeEntry() and allowing ZipOutputStream.close() to handle it.

However, for multi-entry zip files, there is no way to guarantee a single call to ZipOutputStream.closeEntry() 


Comments
Fix request [8u] I backport this fix for parity with Oracle 8u341 and because of encountering the issue in production. CSR for 8-pool is approved. Code as it is in jdk11 applied without changes, with path adjustments only. Tests passed.
13-08-2024

A pull request was submitted for review. Branch: master URL: https://git.openjdk.org/jdk8u-dev/pull/558 Date: 2024-08-13 15:11:57 +0000
13-08-2024

verified
13-04-2022

Fix request [13u,15u] This fix to the infinite loop in ZipOutputStream.close() is applicable to 13u/15u (and the regtest promptly fails here). CSR is required and provided (thank you Goetz!). The nightly tests run OK. I'm using old format for switch: switch expressions are in preview mode in 13 and in prod in 15 but replaced code hasn't them in any case, and other switches in the file.
05-04-2022

A pull request was submitted for review. URL: https://git.openjdk.java.net/jdk15u-dev/pull/192 Date: 2022-04-04 14:06:36 +0000
04-04-2022

A pull request was submitted for review. URL: https://git.openjdk.java.net/jdk13u-dev/pull/337 Date: 2022-04-04 13:54:20 +0000
04-04-2022

Fix request [17u] This is a backport for parity with 17.0.4-oracle. Jonathan Dowland started it, I requested the CSR and finished it up. Clean backport. Test passes. SAP nightly testing passed for PR.
20-03-2022

Approving 17u and 11u backports with reference to the comment regarding non public CSRs for the backports: https://bugs.openjdk.java.net/browse/JDK-8276305?focusedCommentId=14482648&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-14482648
19-03-2022

A pull request was submitted for review. URL: https://git.openjdk.java.net/jdk11u-dev/pull/914 Date: 2022-03-18 08:35:00 +0000
18-03-2022

Fix request [11u] I backport this for parity with 11.0.16-oracle. I had to adapt switch statements that are not supported in 11. Test passes and fails without the patch.
18-03-2022

A pull request was submitted for review. URL: https://git.openjdk.java.net/jdk17u-dev/pull/147 Date: 2022-02-04 12:28:57 +0000
04-02-2022

Changeset: 1e9ed54d Author: Ravi Reddy <rreddy@openjdk.org> Committer: Sean Coffey <coffeys@openjdk.org> Date: 2021-12-01 15:35:00 +0000 URL: https://git.openjdk.java.net/jdk/commit/1e9ed54d362b8c57be5fbbac2de5afbd0f05435f
01-12-2021

Hi Cesar , The same issue is reproducible with 11+ JDKs, you can find the test case here: https://github.com/openjdk/jdk/pull/5522/files#diff-3ec6d67f9694762d024f805fed639bfedbab46530621d9a4dd345de88b73815e
18-11-2021

May I ask a few questions about this issue? - Does this problem affect 11+ JDKs? If it doesn't, do you know know why? - Has anyone been able to create a small reproducible test case for this?
07-10-2021

Additional information from submitter: We are using zlib 1.2.3 on Centos 6 You are correct, the s->status = FINISH_STATE can only happen when deflate_xxx() returns finish_started/finish_done. However, finish_started can be returned when the compression output buffer is full (avail_out == 0) - see https://github.com/madler/zlib/blob/v1.2.3/deflate.c#L1378 Quoting the man page: "If the parameter flush is set to Z_FINISH, pending input is processed, pending output is flushed and deflate returns with Z_STREAM_END if there was enough output space. If deflate returns with Z_OK or Z_BUF_ERROR, this function must be called again with Z_FINISH and more output space (updated avail_out) but no more input data, until it returns with Z_STREAM_END or an error. After deflate has returned Z_STREAM_END, the only possible operations on the stream are deflateReset or deflateEnd."
26-07-2018

Without a test case to reproduce the issue it remains a puzzle for me on how we can get into "finish_state" status when there is still bytes in input buffer. in latest deflate.c/deflate() the s->status = FINISH_STATE is only done when the "deflate_xxx() returns finish_started/done, in which is only true if there no more bytes in the buffer (strm_avail_in == 0). That said, possible "solution" for this particular issue is to wipe the "current" out in closeEntry() regardless if there is an exception thrown during the process, so no re-entry closeEntry() for the same entry for ZOS.
24-05-2018

Additional information from submitter: A workaround that prevents the infinite loop is public class ZipOutputStreamBugfix extends ZipOutputStream { public ZipOutputStreamBugfix(OutputStream out) { super(out); } public ZipOutputStreamBugfix(OutputStream out, Charset charset) { super(out, charset); } @Override protected void deflate() throws IOException { try { super.deflate(); } catch (IOException ex) { // if DeflaterOutputStream.deflate() ever fails (due to writing to // underlying to underlying OutputStream), the Deflater *may* be // left in an inconsistent state (i.e. part way through consuming // it's input buffer, but after having been told to "finish"). Clean // up Deflater here to prevent possible infinite loops later def.reset(); throw ex; } } }
03-01-2018