JDK-7003462 : cannot read InputStream returned by java.util.ZipFile.getInputStream(ZipEntry)
  • Type: Bug
  • Component: core-libs
  • Sub-Component: java.util.jar
  • Affected Version: 6u22
  • Priority: P4
  • Status: Closed
  • Resolution: Fixed
  • OS: linux_ubuntu
  • CPU: x86
  • Submitted: 2010-11-30
  • Updated: 2013-12-05
  • Resolved: 2011-03-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 6 JDK 7
6u75Fixed 7 b123Fixed
Description
FULL PRODUCT VERSION :
java version "1.6.0_22"
Java(TM) SE Runtime Environment (build 1.6.0_22-b04)
Java HotSpot(TM) 64-Bit Server VM (build 17.1-b03, mixed mode)


ADDITIONAL OS VERSION INFORMATION :
Linux 2.6.35-23-generic #40-Ubuntu SMP Wed Nov 17 22:14:33 UTC 2010 x86_64 GNU/Linux


A DESCRIPTION OF THE PROBLEM :
ZipFile keeps a pool of Inflaters, that are used to read deflated resources in the ZipFile.
 
When calling ZipFile.getInputStream(ZipEntry) for a deflated entry, a nameless subclass of InflaterInputStream is returned, which uses one of the Inflaters that are managed by ZipFile. Calling the close method on this nameless subclass puts the Inflater instance back to the pool in ZipFile.
 
If however, one calls close on this InflaterInputStream subclass in a finalize method, the Inflater wrapped by the stream gets finalized as well (since no references exist to it except the one in the wrapping stream). Finalizing the Inflater makes it unavailable for future use.

Note that according to the above, by calling close on the InflaterInputStream subclass in a finalizer, the Inflater is put back into the pool, and made unavailable for future use at the same time. The invalid Inflater can be reused from the pool, and causes a NullPointerException when trying to read from a different  InputStream acquired through ZipFile.getInputStream(ZipEntry).

The supplied code tries to write the contents of two deflated files (a.txt and b.txt) from an archive test.zip to System.out. The method printFileContents() is used to read a file inside the .zip archive. First, the program gets a wrapper instance (MyInputStreamWrapper) which contains an InputStream acquired through ZipFile.getInputStream. MyInputStreamWrapper is defined to close the stream in its finalize() method. After reading the stream to end, the contents are written to System.out. After the printFileContents method finishes, the wrapper instance can be garbage collected - finalization is "forced" in the run() method. This triggers the finalization of the wrapped InputStream, and Inflater, and the Inflater is reinsert to the Inflater pool in ZipFile. When trying to read the second file, the Inflater is reused, and an Exception is thrown while trying to read from the InputStream.

STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
1. Create a test.zip archive with two files in the root, a.txt and b.txt. The files should contain redundant data (or make sure they are stored deflated).
Example content for a.txt:
aaaaa
aaaa
aaa
aa
a
Example content for b.txt:
bbbbb
bbbb
bbb
bb
b
2. Use the supplied code to print the contents of both files. Create an executable jar from the class, and place the .jar in the same directory as test.zip.  Use java -jar to run the test program.

EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
aaaaa
aaaa
aaa
aa
a

Inflater closed

bbbbb
bbbb
bbb
bb
b
ACTUAL -
aaaaa
aaaa
aaa
aa
a

Inflater closed
Exception in thread "main" java.lang.NullPointerException: Inflater has been closed
	at java.util.zip.Inflater.ensureOpen(Inflater.java:364)
	at java.util.zip.Inflater.inflate(Inflater.java:237)
	at java.util.zip.InflaterInputStream.read(InflaterInputStream.java:135)
	at java.io.FilterInputStream.read(FilterInputStream.java:90)
	at hu.cjp.inflatertest.Main.printFileContents(Main.java:48)
	at hu.cjp.inflatertest.Main.run(Main.java:28)
	at hu.cjp.inflatertest.Main.main(Main.java:13)


REPRODUCIBILITY :
This bug can be reproduced always.

---------- BEGIN SOURCE ----------

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

public class Main {

	public static void main(String[] args) {
		Main m = new Main();
		m.run();
	}

	public void run(){
		try{
			File file = new File("test.zip");
			if(file.exists()){
				ZipFile zipFile = new ZipFile(file);
				
				printFileContents("a.txt",zipFile);
				//trigger the run of finalizer of the MyInputStreamWrapper allocated in printFileContents
				System.gc();
				System.runFinalization();
				System.gc();

				printFileContents("b.txt", zipFile);
				zipFile.close();
			}
			else{
				System.out.println("Error: file: "+file.getAbsolutePath() + " doesn't exist!");
			}
		}
		catch (IOException e) {
			e.printStackTrace();
		}
	}

	private void printFileContents(String fileName, ZipFile zipFile) {
		ZipEntry ze = new ZipEntry(fileName);
		//get a new wrapper on the inputstream
		MyInputStreamWrapper wrapper = getInputStreamWrapper(ze,zipFile);
		InputStream ios = wrapper.ios;
		byte[] buffer = new byte[32];
		StringBuilder sb = new StringBuilder();
		try{
			while(ios.read(buffer)>0){
				sb.append(new String(buffer));
			}
		}catch (IOException e) {
			e.printStackTrace();
		}
		//print file contents
		System.out.println(sb);
	}

	private MyInputStreamWrapper getInputStreamWrapper(ZipEntry ze,
			ZipFile zipFile) {
		try{
			InputStream ios = zipFile.getInputStream(ze);
			MyInputStreamWrapper wrapper = new MyInputStreamWrapper(ios);
			return wrapper;
		}catch (IOException e) {
			e.printStackTrace();
		}
		return null;
	}

	public class MyInputStreamWrapper{
		InputStream ios;
		public MyInputStreamWrapper(InputStream ios) {
			assert ios==null:"The supllied InputStream must not be null!";
			this.ios = ios;
		}
		
		@Override
		protected void finalize() throws Throwable {
			super.finalize();
			//close the wrapped inputstream
			ios.close();
			System.out.println("Inflater closed");
		}
	}
	
}

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

Comments
EVALUATION http://hg.openjdk.java.net/jdk7/build/jdk/rev/1740ad242f56
25-12-2010

EVALUATION Yes, it appears ZipFile doesn't hold a ref to the "leased out" inflater and doesn't check if the "returned" inflater is actually "finalized/ended". To have the "streams" to hold a reference to the "InflaterInputStream" (current implementation adds ZipFileInputStream into the "streams" map instead, which is obvious a mistake) should in fact hold a strong reference to its embedded inflater and therefor prevent the inflater from being finalized.
07-12-2010