diff --git a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs index ac810fff78..8ac96f9483 100644 --- a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs +++ b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs @@ -203,7 +203,7 @@ internal sealed class ExrDecoderCore : ImageDecoderCore uint rowsInBlock = Math.Min(rowsPerBlock, (uint)height - rowStartIndex); uint uncompressedBytesCount = (uint)(bytesPerRow * rowsInBlock); - decompressor.Decompress(stream, compressedBytesCount, uncompressedBytesCount, decompressedPixelData); + this.DecompressBlock(decompressor, stream, compressedBytesCount, uncompressedBytesCount, decompressedPixelData); int offset = 0; for (uint rowIndex = rowStartIndex; rowIndex < rowStartIndex + rowsPerBlock && rowIndex < height; rowIndex++) @@ -292,7 +292,7 @@ internal sealed class ExrDecoderCore : ImageDecoderCore uint rowsInBlock = Math.Min(rowsPerBlock, (uint)height - rowStartIndex); uint uncompressedBytesCount = (uint)(bytesPerRow * rowsInBlock); - decompressor.Decompress(stream, compressedBytesCount, uncompressedBytesCount, decompressedPixelData); + this.DecompressBlock(decompressor, stream, compressedBytesCount, uncompressedBytesCount, decompressedPixelData); int offset = 0; for (uint rowIndex = rowStartIndex; rowIndex < rowStartIndex + rowsPerBlock && rowIndex < height; rowIndex++) @@ -321,6 +321,28 @@ internal sealed class ExrDecoderCore : ImageDecoderCore } } + /// + /// Decompresses a block according to the configured image-data integrity policy. + /// + /// The decompressor for the stored compression type. + /// The encoded block stream. + /// The declared compressed byte count. + /// The expected byte count for the rows in this block. + /// The reusable decompressed pixel buffer. + private void DecompressBlock(ExrBaseDecompressor decompressor, BufferedReadStream stream, uint compressedBytes, uint uncompressedBytes, Span buffer) + { + try + { + decompressor.Decompress(stream, compressedBytes, uncompressedBytes, buffer); + } + catch (Exception ex) when (this.Options.SegmentIntegrityHandling == SegmentIntegrityHandling.IgnoreImageData && ex is InvalidImageContentException or InvalidDataException) + { + // The offset table locates the next block independently of this damaged payload. + // Discard the entire failed block so partial output or pooled bytes cannot become pixels. + buffer[..(int)uncompressedBytes].Clear(); + } + } + /// /// Reads float image channel data. /// diff --git a/tests/ImageSharp.Tests/Formats/Exr/ExrZipDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Exr/ExrZipDecoderTests.cs index e102ab8076..4435410316 100644 --- a/tests/ImageSharp.Tests/Formats/Exr/ExrZipDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Exr/ExrZipDecoderTests.cs @@ -16,12 +16,55 @@ namespace SixLabors.ImageSharp.Tests.Formats.Exr; [ValidateDisposedMemoryAllocations] public class ExrZipDecoderTests { - [Fact] - public void Decode_ShortInflatedBlock_Throws() + /// + /// Incomplete and oversized blocks are rejected unless image-data recovery is enabled. + /// + /// The inflated payload length. + /// The image-data integrity policy. + [Theory] + [InlineData(0, SegmentIntegrityHandling.Strict)] + [InlineData(8, SegmentIntegrityHandling.Strict)] + [InlineData(1025, SegmentIntegrityHandling.Strict)] + [InlineData(0, SegmentIntegrityHandling.IgnoreAncillary)] + [InlineData(8, SegmentIntegrityHandling.IgnoreAncillary)] + [InlineData(1025, SegmentIntegrityHandling.IgnoreAncillary)] + public void Decode_InvalidInflatedBlock_Throws(int length, SegmentIntegrityHandling integrity) + { + byte[] data = BuildExr(ZlibCompress(new byte[length]), ExrPixelType.Float, 2); + DecoderOptions options = new() { SegmentIntegrityHandling = integrity }; + + Assert.Throws(() => Image.Load(options, data)); + } + + /// + /// Recovering an invalid image-data block must not expose partially decoded or pooled bytes. + /// + /// The stored sample type. + /// The inflated payload length. + [Theory] + [InlineData(ExrPixelType.Half, 0)] + [InlineData(ExrPixelType.Half, 8)] + [InlineData(ExrPixelType.Half, 1025)] + [InlineData(ExrPixelType.Float, 0)] + [InlineData(ExrPixelType.Float, 8)] + [InlineData(ExrPixelType.Float, 1025)] + [InlineData(ExrPixelType.UnsignedInt, 0)] + [InlineData(ExrPixelType.UnsignedInt, 8)] + [InlineData(ExrPixelType.UnsignedInt, 1025)] + public void Decode_InvalidInflatedBlock_IgnoreImageData_ClearsPixels(ExrPixelType pixelType, int length) { - byte[] data = BuildExr(ZlibCompress(new byte[8]), ExrPixelType.Float, 2); + byte[] data = BuildExr(ZlibCompress(new byte[length]), pixelType, 2); + Configuration configuration = Configuration.Default.Clone(); + configuration.MemoryAllocator = new TestMemoryAllocator(0x3F); + DecoderOptions options = new() { Configuration = configuration, SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreImageData }; + + using Image image = Image.Load(options, data); + Assert.Equal(new Size(256, 1), image.Size); - Assert.Throws(() => Image.Load(data)); + for (int x = 0; x < image.Width; x++) + { + Assert.Equal(new Vector4(0, 0, 0, 1), image[x, 0].ToVector4()); + } } ///