-
Bug
-
Resolution: Won't Fix
-
P3
-
None
-
24, 25
-
b21
-
generic
-
generic
ADDITIONAL SYSTEM INFORMATION :
MacOS/arm64, Linux/amd64, any JDK 24 distribution (tried temurin, zulu, adoptopenjdk)
A DESCRIPTION OF THE PROBLEM :
If a zip file contains local entries with large extras (65505 bytes) with 0-len name and comments then reading that file with `ZipInputStream` will fail.
While such files aren't technically compliant to the spec (these entries can only have local file headers as they don't fit in a central file header) there are implementations out there that emit zips like this, notably ZipFlinger:
> If an entry has been deleted in the middle of the archive, Zipflinger will not leave a “hole” there. This is done in order to be compatible with top-down parsers such as jarsigner or the JDK zip classes. To this effect, Zipflinger fills empty space with virtual entries (a.k.a a Local File Header with no name, up to 64KiB extra and no Central Directory entry). Alignment is also done via “extra field”.
https://android.googlesource.com/platform/tools/base/+/refs/heads/mirror-goog-studio-master-dev/zipflinger/
REGRESSION : Last worked in version 23.0.2
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Construct a zip file with a large LOC header.
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
ZipInputStream does not crash when iterating over the file.
ACTUAL -
ZipInputStream throws IllegalArgumentException:
Exception in thread "main" java.lang.IllegalArgumentException: invalid extra field length
at java.base/java.util.zip.ZipEntry.setExtra0(ZipEntry.java:550)
at java.base/java.util.zip.ZipInputStream.readLOC(ZipInputStream.java:556)
at java.base/java.util.zip.ZipInputStream.getNextEntry(ZipInputStream.java:158)
at Zip.main(Zip.java:90)
---------- BEGIN SOURCE ----------
import java.io.IOException;
import java.nio.file.Files;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
public class Zip {
public static final String hex = """
# file 1: file.txt
504b 0304 # local file header signature (0x04034b50)
0a00 # version needed to extract (10)
0000 # general purpose bit flag
0000 # compression method (STORE)
0000 # last modified file time
0000 # last modified file date
dddd 147d # CRC-32
0d00 0000 # compressed size, 13 bytes
0d00 0000 # uncompressed size, 13 bytes
0800 # file name length, 8 bytes
0000 # extra field length, 0 bytes
# filename
6669 6c65 2e74 7874 # "file.txt"
# actual file contents
4865 6c6c 6f20 576f 726c 6421 0a # "Hello world!\\n"
# file 2: a hole - dummy empty file with a large extra
504b 0304 # local file header signature (0x04034b50)
0a00 # version needed to extract (10)
0000 # general purpose bit flag
0000 # compression method (STORE)
0000 # last modified file time
0000 # last modified file date
0000 0000 # CRC-32
0000 0000 # compressed size, 0 bytes
0000 0000 # uncompressed size, 0 bytes
0000 # file name length, 0 bytes
e1ff # extra field length, 65505 bytes
# extra field data, 65505 bytes of zeroes
00 * 65505
# central directory
#############################################
504b 0102 # central file header signature (0x02014b50)
1e03 # version made by, (798?)
0a00 # version needed to extract (10)
0000 # general purpose bit flag
0000 # compression method (STORE)
0000 # last mod file time
0000 # last mod file date
dddd 147d # CRC-32
0d00 0000 # compressed size, 13 bytes
0d00 0000 # uncompressed size, 13 bytes
0800 # file name length, 8 bytes
0000 # extra field length, 0 bytes
0000 # file comment length
0000 # disk number start
0000 # internal file attributes
0000 0000 # external file attributes
0000 0000 # offset of local header
6669 6c65 2e74 7874 # "file.txt"
504b 0506 # end of central directory signature (0x06054b50)
0000 # number of this disk
0000 # number of the disk with the start of the central directory
0100 # total number of entries in the central directory on this disk
0100 # total number of entries in the central directory
3600 0000 # size of the central directory, 76 bytes
3200 0100 # offset of start of central directory
0000 # file comment length
""";
public static void main(String[] args) throws IOException {
var byteArray = parseHexString(hex);
System.out.println("ZipInputStream entries:");
try (var zipInputStream = new ZipInputStream(new java.io.ByteArrayInputStream(byteArray))) {
var entry = zipInputStream.getNextEntry();
while (entry != null) {
printEntry(entry);
entry = zipInputStream.getNextEntry();
}
}
System.out.println("\nZipFile entries:");
var out = Files.createTempFile("out", ".zip");
try {
Files.write(out, byteArray);
try (var zipFile = new ZipFile(out.toFile())) {
zipFile.entries().asIterator().forEachRemaining(Zip::printEntry);
}
} finally {
Files.deleteIfExists(out);
}
}
private static byte[] parseHexString(String hexString) {
var bytes = hexString.lines()
.map(line -> {
var hexBytes = line.split("#", 2)[0].replace(" ", "");
var repeat = hexBytes.split("\\*", 2);
if (repeat.length == 2) {
return repeat[0].repeat(Integer.parseInt(repeat[1]));
} else {
return hexBytes;
}
})
.flatMap(byteString -> Stream.iterate(0, i -> i + 2)
.limit(byteString.length() / 2)
.map(off -> (byte)(Integer.parseInt(byteString.substring(off, off + 2), 16) & 0xff)))
.collect(Collectors.toList());
var byteArray = new byte[bytes.size()];
IntStream.range(0, bytes.size()).forEach(i -> byteArray[i] = bytes.get(i));
return byteArray;
}
private static void printEntry(ZipEntry entry) {
System.out.println("name=" + entry.getName() +
" size=" + entry.getSize() +
" compressedSize=" + entry.getCompressedSize() +
" crc=" + Long.toHexString(entry.getCrc()) +
" extraSize=" + (entry.getExtra() == null ? 0 : entry.getExtra().length));
}
}
---------- END SOURCE ----------
MacOS/arm64, Linux/amd64, any JDK 24 distribution (tried temurin, zulu, adoptopenjdk)
A DESCRIPTION OF THE PROBLEM :
If a zip file contains local entries with large extras (65505 bytes) with 0-len name and comments then reading that file with `ZipInputStream` will fail.
While such files aren't technically compliant to the spec (these entries can only have local file headers as they don't fit in a central file header) there are implementations out there that emit zips like this, notably ZipFlinger:
> If an entry has been deleted in the middle of the archive, Zipflinger will not leave a “hole” there. This is done in order to be compatible with top-down parsers such as jarsigner or the JDK zip classes. To this effect, Zipflinger fills empty space with virtual entries (a.k.a a Local File Header with no name, up to 64KiB extra and no Central Directory entry). Alignment is also done via “extra field”.
https://android.googlesource.com/platform/tools/base/+/refs/heads/mirror-goog-studio-master-dev/zipflinger/
REGRESSION : Last worked in version 23.0.2
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Construct a zip file with a large LOC header.
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
ZipInputStream does not crash when iterating over the file.
ACTUAL -
ZipInputStream throws IllegalArgumentException:
Exception in thread "main" java.lang.IllegalArgumentException: invalid extra field length
at java.base/java.util.zip.ZipEntry.setExtra0(ZipEntry.java:550)
at java.base/java.util.zip.ZipInputStream.readLOC(ZipInputStream.java:556)
at java.base/java.util.zip.ZipInputStream.getNextEntry(ZipInputStream.java:158)
at Zip.main(Zip.java:90)
---------- BEGIN SOURCE ----------
import java.io.IOException;
import java.nio.file.Files;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
public class Zip {
public static final String hex = """
# file 1: file.txt
504b 0304 # local file header signature (0x04034b50)
0a00 # version needed to extract (10)
0000 # general purpose bit flag
0000 # compression method (STORE)
0000 # last modified file time
0000 # last modified file date
dddd 147d # CRC-32
0d00 0000 # compressed size, 13 bytes
0d00 0000 # uncompressed size, 13 bytes
0800 # file name length, 8 bytes
0000 # extra field length, 0 bytes
# filename
6669 6c65 2e74 7874 # "file.txt"
# actual file contents
4865 6c6c 6f20 576f 726c 6421 0a # "Hello world!\\n"
# file 2: a hole - dummy empty file with a large extra
504b 0304 # local file header signature (0x04034b50)
0a00 # version needed to extract (10)
0000 # general purpose bit flag
0000 # compression method (STORE)
0000 # last modified file time
0000 # last modified file date
0000 0000 # CRC-32
0000 0000 # compressed size, 0 bytes
0000 0000 # uncompressed size, 0 bytes
0000 # file name length, 0 bytes
e1ff # extra field length, 65505 bytes
# extra field data, 65505 bytes of zeroes
00 * 65505
# central directory
#############################################
504b 0102 # central file header signature (0x02014b50)
1e03 # version made by, (798?)
0a00 # version needed to extract (10)
0000 # general purpose bit flag
0000 # compression method (STORE)
0000 # last mod file time
0000 # last mod file date
dddd 147d # CRC-32
0d00 0000 # compressed size, 13 bytes
0d00 0000 # uncompressed size, 13 bytes
0800 # file name length, 8 bytes
0000 # extra field length, 0 bytes
0000 # file comment length
0000 # disk number start
0000 # internal file attributes
0000 0000 # external file attributes
0000 0000 # offset of local header
6669 6c65 2e74 7874 # "file.txt"
504b 0506 # end of central directory signature (0x06054b50)
0000 # number of this disk
0000 # number of the disk with the start of the central directory
0100 # total number of entries in the central directory on this disk
0100 # total number of entries in the central directory
3600 0000 # size of the central directory, 76 bytes
3200 0100 # offset of start of central directory
0000 # file comment length
""";
public static void main(String[] args) throws IOException {
var byteArray = parseHexString(hex);
System.out.println("ZipInputStream entries:");
try (var zipInputStream = new ZipInputStream(new java.io.ByteArrayInputStream(byteArray))) {
var entry = zipInputStream.getNextEntry();
while (entry != null) {
printEntry(entry);
entry = zipInputStream.getNextEntry();
}
}
System.out.println("\nZipFile entries:");
var out = Files.createTempFile("out", ".zip");
try {
Files.write(out, byteArray);
try (var zipFile = new ZipFile(out.toFile())) {
zipFile.entries().asIterator().forEachRemaining(Zip::printEntry);
}
} finally {
Files.deleteIfExists(out);
}
}
private static byte[] parseHexString(String hexString) {
var bytes = hexString.lines()
.map(line -> {
var hexBytes = line.split("#", 2)[0].replace(" ", "");
var repeat = hexBytes.split("\\*", 2);
if (repeat.length == 2) {
return repeat[0].repeat(Integer.parseInt(repeat[1]));
} else {
return hexBytes;
}
})
.flatMap(byteString -> Stream.iterate(0, i -> i + 2)
.limit(byteString.length() / 2)
.map(off -> (byte)(Integer.parseInt(byteString.substring(off, off + 2), 16) & 0xff)))
.collect(Collectors.toList());
var byteArray = new byte[bytes.size()];
IntStream.range(0, bytes.size()).forEach(i -> byteArray[i] = bytes.get(i));
return byteArray;
}
private static void printEntry(ZipEntry entry) {
System.out.println("name=" + entry.getName() +
" size=" + entry.getSize() +
" compressedSize=" + entry.getCompressedSize() +
" crc=" + Long.toHexString(entry.getCrc()) +
" extraSize=" + (entry.getExtra() == null ? 0 : entry.getExtra().length));
}
}
---------- END SOURCE ----------
- relates to
-
JDK-8340553 ZipEntry field validation does not take into account the size of a CEN header
-
- Resolved
-