This is an automated email from the ASF dual-hosted git repository.
ggregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-compress.git
The following commit(s) were added to refs/heads/master by this push:
new 517763b41 [COMPRESS-713] Unchecked pre-decremental notation in
for-loop as array index causes ArrayOutOfBounds access (#755)
517763b41 is described below
commit 517763b41defe0a885c10cd5b8e78c1a6bcc5b1e
Author: Gary Gregory <[email protected]>
AuthorDate: Tue Dec 9 16:39:13 2025 -0500
[COMPRESS-713] Unchecked pre-decremental notation in for-loop as array
index causes ArrayOutOfBounds access (#755)
* [COMPRESS-713] Unchecked pre-decremental notation in for-loop as array
index causes ArrayOutOfBounds access
Use a general try-catch up the call stack instead of checking for a bad
index in a possibly tight loop in
LZWInputStream.expandCodeToOutputStack(int, boolean)
* Properly resets `previousCode` upon clear
* Adapt expected exception
* fix: checkstyle
---------
Co-authored-by: Piotr P. Karwasz <[email protected]>
---
.../archivers/zip/UnshrinkingInputStream.java | 3 +
.../archivers/zip/ZipArchiveInputStream.java | 6 +-
.../archivers/zip/UnshrinkingInputStreamTest.java | 79 ++++++++++++++++++++++
.../compress/archivers/zip/ZipCompress713Test.java | 57 ++++++++++++++++
4 files changed, 144 insertions(+), 1 deletion(-)
diff --git
a/src/main/java/org/apache/commons/compress/archivers/zip/UnshrinkingInputStream.java
b/src/main/java/org/apache/commons/compress/archivers/zip/UnshrinkingInputStream.java
index 95f2a0c73..fbcc626cb 100644
---
a/src/main/java/org/apache/commons/compress/archivers/zip/UnshrinkingInputStream.java
+++
b/src/main/java/org/apache/commons/compress/archivers/zip/UnshrinkingInputStream.java
@@ -128,5 +128,8 @@ private void partialClear() {
setPrefix(i, UNUSED_PREFIX);
}
}
+ // Resets previous code
+ // See COMPRESS-713
+ resetPreviousCode();
}
}
diff --git
a/src/main/java/org/apache/commons/compress/archivers/zip/ZipArchiveInputStream.java
b/src/main/java/org/apache/commons/compress/archivers/zip/ZipArchiveInputStream.java
index 7c77fefcc..d24048786 100644
---
a/src/main/java/org/apache/commons/compress/archivers/zip/ZipArchiveInputStream.java
+++
b/src/main/java/org/apache/commons/compress/archivers/zip/ZipArchiveInputStream.java
@@ -1076,7 +1076,11 @@ public int read(final byte[] buffer, final int offset,
final int length) throws
read = readDeflated(buffer, offset, length);
} else if (method == ZipMethod.UNSHRINKING.getCode() || method ==
ZipMethod.IMPLODING.getCode() || method == ZipMethod.ENHANCED_DEFLATED.getCode()
|| method == ZipMethod.BZIP2.getCode() ||
ZipMethod.isZstd(method) || method == ZipMethod.XZ.getCode()) {
- read = current.checkInputStream().read(buffer, offset, length);
+ try {
+ read = current.checkInputStream().read(buffer, offset, length);
+ } catch (final RuntimeException e) {
+ throw new ArchiveException(e);
+ }
} else {
throw new
UnsupportedZipFeatureException(ZipMethod.getMethodByCode(method),
current.entry);
}
diff --git
a/src/test/java/org/apache/commons/compress/archivers/zip/UnshrinkingInputStreamTest.java
b/src/test/java/org/apache/commons/compress/archivers/zip/UnshrinkingInputStreamTest.java
new file mode 100644
index 000000000..c22e452af
--- /dev/null
+++
b/src/test/java/org/apache/commons/compress/archivers/zip/UnshrinkingInputStreamTest.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.commons.compress.archivers.zip;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+import org.apache.commons.compress.compressors.CompressorException;
+import org.junit.jupiter.api.Test;
+import org.junitpioneer.jupiter.Issue;
+
+class UnshrinkingInputStreamTest {
+
+ private static final int CONTROL_CODE = 256;
+ private static final int CLEAR_SUBCODE = 2;
+ private static final int NEW_CODE = 257;
+
+ /**
+ * Encodes a sequence of 9-bit codes into bytes, LSB-first, as used by
+ * PKZIP's “Shrunk” (Unshrink) method.
+ */
+ private static byte[] encodeLsbFirst9Bit(final int... codes) {
+ final ByteArrayOutputStream out = new ByteArrayOutputStream();
+ int bitBuffer = 0;
+ int bitCount = 0;
+
+ for (int code : codes) {
+ bitBuffer |= (code & 0x1FF) << bitCount;
+ bitCount += 9;
+
+ while (bitCount >= 8) {
+ out.write(bitBuffer & 0xFF);
+ bitBuffer >>>= 8;
+ bitCount -= 8;
+ }
+ }
+
+ if (bitCount > 0) {
+ out.write(bitBuffer & 0xFF);
+ }
+ return out.toByteArray();
+ }
+
+ @Test
+ @Issue("COMPRESS-713")
+ public void testCompress713() throws IOException {
+ final byte[] data = encodeLsbFirst9Bit(
+ 'A',
+ 'B', // defines 257 = "AB"
+ NEW_CODE, // 257 ("AB")
+ CONTROL_CODE, // 256 (control)
+ CLEAR_SUBCODE, // subcode 2 (partial clear)
+ NEW_CODE // 257 again, now undefined and treated as
prevCode + firstChar(prevCode)
+ );
+ try (UnshrinkingInputStream inputStream = new
UnshrinkingInputStream(new ByteArrayInputStream(data))) {
+ assertThrows(CompressorException.class, () -> inputStream.read(new
byte[1024]));
+ }
+ }
+
+}
diff --git
a/src/test/java/org/apache/commons/compress/archivers/zip/ZipCompress713Test.java
b/src/test/java/org/apache/commons/compress/archivers/zip/ZipCompress713Test.java
new file mode 100644
index 000000000..9e8833020
--- /dev/null
+++
b/src/test/java/org/apache/commons/compress/archivers/zip/ZipCompress713Test.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.commons.compress.archivers.zip;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.io.ByteArrayInputStream;
+import java.io.EOFException;
+import java.io.IOException;
+
+import org.apache.commons.compress.CompressException;
+import org.apache.commons.io.function.IOConsumer;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests https://issues.apache.org/jira/browse/COMPRESS-713
+ */
+public class ZipCompress713Test {
+
+ @Test
+ public void testIllegalArrayIndex() throws IOException {
+ final byte[] data = { 80, 75, 3, 4, 19, 7, 0, 1, 1, 0, -1, 1, 120, 8,
84, -99, 3, 48, 45, 1, 119, -70, 110, 61, 65, 104, 0, 0, 0, 0, 59, -4, -1, -1,
-1,
+ -1, -33, 0, -1, 0, 5, 0, -1, -1, -1, -1, -1, -1, -1, 0, 122 };
+ try (ZipArchiveInputStream inputStream = new ZipArchiveInputStream(new
ByteArrayInputStream(data))) {
+ inputStream.getNextEntry();
+ // Either the compressor fails properly or the archiver intercepts
the unchecked exception
+ assertThrows(CompressException.class, () -> inputStream.read(new
byte[1024]));
+ assertThrows(EOFException.class, () -> inputStream.getNextEntry());
+ }
+ }
+
+ @Test
+ public void testTruncated() throws IOException {
+ final byte[] data = { 80, 75, 3, 4, 19, 7, 0, 1, 1, 0, -1, 1, 120, 8,
84, -99, 3, 48, 45, 1, 119, -70, 110, 61, 65, 104, 0, 0, 0, 0, 59, -4, -1, -1,
-1,
+ -1, -33, 0, -1, 0, 5, 0, -1, -1, -1, -1, -1, -1, -1, 0, 122 };
+ try (ZipArchiveInputStream inputStream = new ZipArchiveInputStream(new
ByteArrayInputStream(data))) {
+ assertThrows(EOFException.class, () ->
inputStream.forEach(IOConsumer.noop()));
+ }
+ }
+}