JDK-8184306 : zlib 1.2.11 upgrade triggers j.u.zip.Deflater regression
  • Type: Bug
  • Component: core-libs
  • Sub-Component: java.util.jar
  • Affected Version: 9
  • Priority: P1
  • Status: Closed
  • Resolution: Fixed
  • OS: windows
  • Submitted: 2017-07-12
  • Updated: 2017-10-09
  • Resolved: 2017-07-17
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 10 JDK 9
10Fixed 9 b179Fixed
Related Reports
Relates :  
Relates :  
Description
It appears the j.u.Deflater compression functionality is broken when

 (1) level/strategy is changed (to new value) + (2) reset,()

which is a normal combination/use scenario when the deflater is being cached and reused. The attached test case fails with 1.2.11 (with data size > the DeflaterOutputStream's internal buffer size).

My reading of the 1.2.11 changes suggests the direct trigger is that the internal state "deflate_state.high_water" is not being reset correctly/appropriately in deflateReset/deflateResetKeep/lm_init().

It triggers a regression from 1.2.8 to 1.2.11 in the scenario that the "strategy/levle is changed" AND a followup "reset" is called (withe the assumption that everything should be reset back to initial state and we start a new compression circle, with the new level and strategy. This is how it worked in 1.2.8). Because one of the condition check is changed from "strm->total_in!=0" to "s->high_water" (for the flush last buffer" branch) in deflate.c/deflateParams().

from (1.2.8's)

if ((strategy != s->strategy || func != configuration_table[level].func) &&
    strm->total_in != 0) {
    /* Flush the last buffer: */
    err = deflate(strm, Z_BLOCK);
    if (err == Z_BUF_ERROR && s->pending == 0)
        err = Z_OK;
}

to (1.2.11's)

if ((strategy != s->strategy || func != configuration_table[level].func) &&
    s->high_water) {
    /* Flush the last buffer: */
    int err = deflate(strm, Z_BLOCK);
    if (err == Z_STREAM_ERROR)
        return err;
    if (strm->avail_out == 0)
        return Z_BUF_ERROR;
}

Given the nature of "reset" it seems reasonable to assume the "high_water" should be reset to 0, its initial value as well, inside either deflateResetKeep() or lm_init().

I have logged an issue at

https://github.com/madler/zlib/issues/275

Given jdk9 has been changed to uses the system zlib for all platforms except the windows, this one currently only fails on windows (but I would assume it will fails if the underlying system zlib is 1.2.11)

-------------------------------- test case ----------------------------------------------------
   public static void main(final String[] args) throws IOException {
        Path p = Paths.get("jms.compression.data.large");
        int[] levels = new int[] {
            Deflater.DEFAULT_COMPRESSION,
            0,
            Deflater.BEST_SPEED,
            //            2, 3, 4, 5, 6, 7, 8,                                                                                                
            Deflater.BEST_COMPRESSION,
        };
        try {
	    byte[] src = Files.readAllBytes(p);
            if (src.length == 0) return;

            Deflater def = new Deflater(Deflater.DEFAULT_COMPRESSION); //, true);                                                             
            for (int level : levels) {
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                System.out.println(" -------------- level:" + level + " ------------------");

		def.setLevel(level);
                def.reset();

		try (DeflaterOutputStream defos = new DeflaterOutputStream(baos, def)) {
                    defos.write(src);
                }
                byte[] compressed = baos.toByteArray();

		ByteArrayInputStream bais = new ByteArrayInputStream(compressed);
                Inflater inf = new Inflater();
                try (InflaterInputStream infis = new InflaterInputStream(bais, inf)) {
                    byte[] bytes = infis.readAllBytes();
                    if (!Arrays.equals(src, bytes)) {
                        System.out.println("        ==> NG");
                        new Exception().printStackTrace();
                    } else {
                        System.out.println("        ==> OK");
                    }
                } catch (Exception x) {
                    System.out.println("        ==> NG");
                    x.printStackTrace();
                }
            }
	} catch (IOException x) {
            x.printStackTrace();
	}
}
Comments
verified in b179
24-07-2017

This is approved for JDK 9 as the alternatives (reverting to an older version of zlib or working around the issue at application level) are more risky. We need to keep an eye on zlib/issues/275 to see if the zlib maintainers come up with an alternative fix in the JDK 9 timeframe.
17-07-2017

Fix Request. This bug breaks the basic j.u.zip compression functionality. It's reasonable to assume this regression might have big impact to applications that use the same/similar deflater cache mechanism. The fix is small (one line) and at a relatively safe/harmless spot (reset one of the internal state back to its initial value inside the "reset()" method) The test has been verified to fail without the fix. The risk is estimated to be low.
13-07-2017

suggested fix src/java.base/share/native/libzip/zlib/deflate.c @@ -503,10 +503,12 @@ s = (deflate_state *)strm->state; s->pending = 0; s->pending_out = s->pending_buf; + s->high_water = 0; /* reset to its inital value 0 */ + if (s->wrap < 0) { s->wrap = -s->wrap; /* was made negative by deflate(..., Z_FINISH); */ } s->status = #ifdef GZIP
12-07-2017