Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-8227150

GZIPInputStream#read throws NPE when reading from closed stream

XMLWordPrintable

      ADDITIONAL SYSTEM INFORMATION :
      tmaret-macOS:java tmaret$ java --version
      java 11.0.3 2019-04-16 LTS
      Java(TM) SE Runtime Environment 18.9 (build 11.0.3+12-LTS)
      Java HotSpot(TM) 64-Bit Server VM 18.9 (build 11.0.3+12-LTS, mixed mode)

      System Version: macOS 10.12.6 (16G2016)
      Kernel Version: Darwin 16.7.0
      Boot Mode: Normal
      Secure Virtual Memory: Enabled

      A DESCRIPTION OF THE PROBLEM :
      Assume two threads referencing a GZIPInputStream instance. Without coordination, the first thread reads from the stream in a loop, the second thread closes the stream.

      When the stream is closed, the first thread usually throws an IOException which is expected according to the GZIPInputStream Javadoc [0]. However, sometimes the fist thread throws an NullPointerException, see the stack trace below


      java.util.concurrent.ExecutionException: java.lang.NullPointerException: Inflater has been closed
          at java.util.concurrent.FutureTask.report(FutureTask.java:122)
          at java.util.concurrent.FutureTask.get(FutureTask.java:192)
          at test.GZIPInputStreamReadNPE.test(GZIPInputStreamReadNPE.java:62)
          at test.GZIPInputStreamReadNPE.main(GZIPInputStreamReadNPE.java:25)
          at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
          at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
          at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
          at java.lang.reflect.Method.invoke(Method.java:498)
          at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)
      Caused by: java.lang.NullPointerException: Inflater has been closed
          at java.util.zip.Inflater.ensureOpen(Inflater.java:389)
          at java.util.zip.Inflater.inflate(Inflater.java:257)
          at java.util.zip.InflaterInputStream.read(InflaterInputStream.java:152)
          at java.util.zip.GZIPInputStream.read(GZIPInputStream.java:117)
          at java.util.zip.InflaterInputStream.read(InflaterInputStream.java:122)
          at test.GZIPInputStreamReadNPE.lambda$test$2(GZIPInputStreamReadNPE.java:57)
          at java.util.concurrent.FutureTask.run(FutureTask.java:266)
          at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180)
          at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293)
          at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
          at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
          at java.lang.Thread.run(Thread.java:745)


      Throwing a NPE instead of an IOE is unexpected and violates the read contract [0] where a NPE is documented to be thrown in a different senario (`If b is null`).

      The InflaterInputStream is affected in the same way.

      I looked in the database for similar issues but could find it.

      [0] https://docs.oracle.com/javase/8/docs/api/java/util/zip/GZIPInputStream.html#read-byte:A-int-int- 

      STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
      I have attached a piece of code that allows reproducing the issue.

      In a nutshell, the code does the following

      1. Build an infinite GZIPInputStream stream
      2. Run a `reader` thread that busy reads from the infinite stream
      3. Run a `closer` thread that close the stream
      4. Keep track of the exception thrown by the `reader` thread and check if it's a IOE

      EXPECTED VERSUS ACTUAL BEHAVIOR :
      EXPECTED -
      Based on the GZIPInputStream#read Javadoc, I would expect the exception thrown by the `reader` thread to be a java.io.IOException.
      ACTUAL -
      Often, the `reader` thread throws an unexpected java.lang.NullPointerException.

      ---------- BEGIN SOURCE ----------
      package test;

      import java.io.IOException;
      import java.io.PipedInputStream;
      import java.io.PipedOutputStream;
      import java.util.Iterator;
      import java.util.Random;
      import java.util.concurrent.ExecutionException;
      import java.util.concurrent.Future;
      import java.util.concurrent.ScheduledExecutorService;
      import java.util.zip.GZIPInputStream;
      import java.util.zip.GZIPOutputStream;

      import static java.lang.String.format;
      import static java.lang.Thread.currentThread;
      import static java.util.concurrent.Executors.newScheduledThreadPool;
      import static java.util.concurrent.TimeUnit.SECONDS;

      public class GZIPInputStreamReadNPE {

          private static final ScheduledExecutorService EXECUTOR = newScheduledThreadPool(8);

          public static void main(String[] args) {
              try {
                  test();
              } catch (Exception e) {
                  throw new RuntimeException("Test failed its execution", e);
              } finally {
                  EXECUTOR.shutdownNow();
              }
          }

          private static void test() throws Exception {

              PipedOutputStream pos = new PipedOutputStream();
              PipedInputStream pis = new PipedInputStream(pos);

              GZIPOutputStream gos = new GZIPOutputStream(pos, true);
              GZIPInputStream gis = new GZIPInputStream(pis);

              EXECUTOR.submit(() -> {
                  Iterator<Integer> values = new Random().ints(0, 256).iterator();
                  while (!currentThread().isInterrupted()) {
                      gos.write(values.next());
                      gos.flush();
                  }
                  return null;
              });

              EXECUTOR.schedule(() -> {
                  gis.close();
                  return null;
              }, 1, SECONDS);

              Future<Void> reader =
              EXECUTOR.submit(() -> {
                  while (gis.read() != -1);
                  return null;
              });

              try {
                  reader.get();
              } catch (ExecutionException e) {
                  e.printStackTrace();
                  if (e.getCause() instanceof IOException) {
                      System.out.println("The test passed, GZIPInputStream#read threw a java.io.IOException when reading from a closed stream.");
                  } else {
                      System.out.println(format("The test failed, GZIPInputStream#read threw an unexpected %s when reading from a closed stream.", (e.getCause() != null) ? e.getCause().getClass() : null));
                  }
              }
          }
      }

      ---------- END SOURCE ----------

      CUSTOMER SUBMITTED WORKAROUND :
      The workaround is for the caller code to catch the NPE and parse the exception message to distinguish between two cases

      1. b (the buffer passed to the read method) is null
      2. the stream is closed

      FREQUENCY : often


            lancea Lance Andersen
            webbuggrp Webbug Group
            Votes:
            0 Vote for this issue
            Watchers:
            4 Start watching this issue

              Created:
              Updated: