From 811d793c282c1c3c5975896dc373183697b2a65c Mon Sep 17 00:00:00 2001 From: dajiaohuang Date: Sun, 25 Oct 2026 11:55:11 +0800 Subject: [PATCH] [IMAGING-388] Prevent PNG scanline size overflow Calculate scanline byte lengths and sample offsets without int overflow. Reject unrepresentable or out-of-row lengths with ImagingException and add regression coverage. --- .../imaging/formats/png/BitParser.java | 19 ++++++---- .../formats/png/ScanExpediterSimple.java | 15 ++++++-- .../imaging/formats/png/BitParserTest.java | 36 +++++++++++++++++++ .../formats/png/ScanExpediterSimpleTest.java | 35 ++++++++++++++++++ 4 files changed, 96 insertions(+), 9 deletions(-) create mode 100644 src/test/java/org/apache/commons/imaging/formats/png/BitParserTest.java create mode 100644 src/test/java/org/apache/commons/imaging/formats/png/ScanExpediterSimpleTest.java diff --git a/src/main/java/org/apache/commons/imaging/formats/png/BitParser.java b/src/main/java/org/apache/commons/imaging/formats/png/BitParser.java index 5e28f8b52a..43c735ab5a 100644 --- a/src/main/java/org/apache/commons/imaging/formats/png/BitParser.java +++ b/src/main/java/org/apache/commons/imaging/formats/png/BitParser.java @@ -30,23 +30,28 @@ final class BitParser { } public int getSample(final int pixelIndexInScanline, final int sampleIndex) throws ImagingException { - final int pixelIndexBits = bitsPerPixel * pixelIndexInScanline; - final int sampleIndexBits = pixelIndexBits + sampleIndex * bitDepth; - final int sampleIndexBytes = sampleIndexBits >> 3; + final long pixelIndexBits = (long) bitsPerPixel * pixelIndexInScanline; + final long sampleIndexBits = pixelIndexBits + (long) sampleIndex * bitDepth; + final long sampleIndexBytes = sampleIndexBits >> 3; + + if (sampleIndexBytes < 0 || sampleIndexBytes >= bytes.length || bitDepth == 16 && sampleIndexBytes + 1 >= bytes.length) { + throw new ImagingException("PNG: sample is outside the scanline"); + } + final int sampleIndexByte = (int) sampleIndexBytes; if (bitDepth == 8) { - return 0xff & bytes[sampleIndexBytes]; + return 0xff & bytes[sampleIndexByte]; } if (bitDepth < 8) { - int b = 0xff & bytes[sampleIndexBytes]; - final int bitsToShift = 8 - ((pixelIndexBits & 7) + bitDepth); + int b = 0xff & bytes[sampleIndexByte]; + final int bitsToShift = 8 - ((int) (pixelIndexBits & 7) + bitDepth); b >>= bitsToShift; final int bitmask = (1 << bitDepth) - 1; return b & bitmask; } if (bitDepth == 16) { - return (0xff & bytes[sampleIndexBytes]) << 8 | 0xff & bytes[sampleIndexBytes + 1]; + return (0xff & bytes[sampleIndexByte]) << 8 | 0xff & bytes[sampleIndexByte + 1]; } throw new ImagingException("PNG: bad BitDepth: " + bitDepth); diff --git a/src/main/java/org/apache/commons/imaging/formats/png/ScanExpediterSimple.java b/src/main/java/org/apache/commons/imaging/formats/png/ScanExpediterSimple.java index 4332be15f9..907468a602 100644 --- a/src/main/java/org/apache/commons/imaging/formats/png/ScanExpediterSimple.java +++ b/src/main/java/org/apache/commons/imaging/formats/png/ScanExpediterSimple.java @@ -31,10 +31,21 @@ final class ScanExpediterSimple extends AbstractScanExpediter { super(width, height, is, bi, pngColorType, bitDepth, bitsPerPixel, pngChunkPLTE, gammaCorrection, abstractTransparencyFilter); } + static int getPixelBytesPerScanline(final int bitsPerPixel, final int width) throws ImagingException { + if (bitsPerPixel <= 0 || width <= 0) { + throw new ImagingException("PNG: invalid scanline dimensions"); + } + final long bitsPerScanLine = (long) bitsPerPixel * width; + final long pixelBytesPerScanLine = (bitsPerScanLine + 7) / 8; + if (pixelBytesPerScanLine > Integer.MAX_VALUE) { + throw new ImagingException("PNG: scanline is too large"); + } + return (int) pixelBytesPerScanLine; + } + @Override public void drive() throws ImagingException, IOException { - final int bitsPerScanLine = bitsPerPixel * width; - final int pixelBytesPerScanLine = getBitsToBytesRoundingUp(bitsPerScanLine); + final int pixelBytesPerScanLine = getPixelBytesPerScanline(bitsPerPixel, width); byte[] prev = null; for (int y = 0; y < height; y++) { diff --git a/src/test/java/org/apache/commons/imaging/formats/png/BitParserTest.java b/src/test/java/org/apache/commons/imaging/formats/png/BitParserTest.java new file mode 100644 index 0000000000..762027e064 --- /dev/null +++ b/src/test/java/org/apache/commons/imaging/formats/png/BitParserTest.java @@ -0,0 +1,36 @@ +/* + * 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.imaging.formats.png; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.apache.commons.imaging.ImagingException; +import org.junit.jupiter.api.Test; + +class BitParserTest { + @Test + void testSampleOutsideScanlineThrowsImagingException() { + final BitParser bitParser = new BitParser(new byte[9], 8, 8); + assertThrows(ImagingException.class, () -> bitParser.getSample(9, 0)); + } + + @Test + void testOverflowingPixelIndexThrowsImagingException() { + final BitParser bitParser = new BitParser(new byte[9], 8, 8); + assertThrows(ImagingException.class, () -> bitParser.getSample(268435457, 0)); + } +} diff --git a/src/test/java/org/apache/commons/imaging/formats/png/ScanExpediterSimpleTest.java b/src/test/java/org/apache/commons/imaging/formats/png/ScanExpediterSimpleTest.java new file mode 100644 index 0000000000..fc1a20cf0f --- /dev/null +++ b/src/test/java/org/apache/commons/imaging/formats/png/ScanExpediterSimpleTest.java @@ -0,0 +1,35 @@ +/* + * 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.imaging.formats.png; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.apache.commons.imaging.ImagingException; +import org.junit.jupiter.api.Test; + +class ScanExpediterSimpleTest { + @Test + void testScanlineSizeDoesNotOverflow() throws ImagingException { + assertEquals(536870921, ScanExpediterSimple.getPixelBytesPerScanline(8, 536870921)); + } + + @Test + void testScanlineSizeRejectsUnrepresentableResult() { + assertThrows(ImagingException.class, () -> ScanExpediterSimple.getPixelBytesPerScanline(16, Integer.MAX_VALUE)); + } +}