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());
+ }
}
///