From e144a59dbb3e8277894d0b8ef49ce8d1a0405d13 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 12 May 2026 17:21:18 +1000 Subject: [PATCH] EXR: validate sizes, prevent overflows, dispose image --- src/ImageSharp/Formats/Exr/ExrDecoderCore.cs | 59 +++++++++++++------ src/ImageSharp/Formats/Exr/ExrEncoderCore.cs | 20 +++++-- .../Formats/Exr/ExrDecoderSecurityTests.cs | 47 +++++++++++---- 3 files changed, 89 insertions(+), 37 deletions(-) diff --git a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs index 34ccbc1280..c24352d576 100644 --- a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs +++ b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs @@ -105,24 +105,33 @@ internal sealed class ExrDecoderCore : ImageDecoderCore ExrThrowHelper.ThrowNotSupported($"Compression {this.Compression} is not yet supported"); } - Image image = new(this.configuration, this.Width, this.Height, this.metadata); - Buffer2D pixels = image.GetRootFramePixelBuffer(); - - switch (this.PixelType) - { - case ExrPixelType.Half: - case ExrPixelType.Float: - this.DecodeFloatingPointPixelData(stream, pixels, cancellationToken); - break; - case ExrPixelType.UnsignedInt: - this.DecodeUnsignedIntPixelData(stream, pixels, cancellationToken); - break; - default: - ExrThrowHelper.ThrowNotSupported("Pixel type is not supported"); - break; - } + Image image = null; + try + { + image = new Image(this.configuration, this.Width, this.Height, this.metadata); + Buffer2D pixels = image.GetRootFramePixelBuffer(); + + switch (this.PixelType) + { + case ExrPixelType.Half: + case ExrPixelType.Float: + this.DecodeFloatingPointPixelData(stream, pixels, cancellationToken); + break; + case ExrPixelType.UnsignedInt: + this.DecodeUnsignedIntPixelData(stream, pixels, cancellationToken); + break; + default: + ExrThrowHelper.ThrowNotSupported("Pixel type is not supported"); + break; + } - return image; + return image; + } + catch + { + image?.Dispose(); + throw; + } } /// @@ -622,7 +631,10 @@ internal sealed class ExrDecoderCore : ImageDecoderCore long width = (long)dataWindow.XMax - dataWindow.XMin + 1; long height = (long)dataWindow.YMax - dataWindow.YMin + 1; - if (width > int.MaxValue || height > int.MaxValue) + + // Decoding stages each row as four color planes, so the width must be bounded + // before later width * 4 buffer sizing can overflow. + if (width > int.MaxValue / 4 || height > int.MaxValue) { ExrThrowHelper.ThrowInvalidImageContentException("EXR DataWindow dimensions exceed the maximum allowed size."); } @@ -633,7 +645,16 @@ internal sealed class ExrDecoderCore : ImageDecoderCore this.Compression = this.HeaderAttributes.Compression; uint rowsPerBlock = ExrUtils.RowsPerBlock(this.Compression); long chunkCount = (this.Height + (long)rowsPerBlock - 1) / rowsPerBlock; - this.MinimumChunkOffset = stream.Position + (chunkCount * sizeof(ulong)); + long offsetTableByteCount = chunkCount * sizeof(ulong); + + // The scanline offset table sits between the header and pixel chunks; proving it + // fits in the stream keeps all later chunk offsets on the pixel-data side. + if (stream.Position > stream.Length || offsetTableByteCount > stream.Length - stream.Position) + { + ExrThrowHelper.ThrowInvalidImageContentException("EXR chunk offset table is outside the bounds of the stream."); + } + + this.MinimumChunkOffset = stream.Position + offsetTableByteCount; this.PixelType = this.ValidateChannels(); this.ImageDataType = this.DetermineImageDataType(); diff --git a/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs b/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs index bb696cca1d..81b7c4da11 100644 --- a/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs +++ b/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs @@ -170,9 +170,13 @@ internal sealed class ExrEncoderCore CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { - uint bytesPerRow = (uint)ExrUtils.CalculateBytesPerRow(channels, (uint)width); + ulong bytesPerRow = ExrUtils.CalculateBytesPerRow(channels, (uint)width); uint rowsPerBlock = ExrUtils.RowsPerBlock(compression); - uint bytesPerBlock = bytesPerRow * rowsPerBlock; + ulong bytesPerBlock = bytesPerRow * rowsPerBlock; + if (bytesPerRow > uint.MaxValue || bytesPerBlock > int.MaxValue) + { + throw new ImageFormatException("Image is too large to encode in EXR format."); + } using IMemoryOwner rgbBuffer = this.memoryAllocator.Allocate(width * 4, AllocationOptions.Clean); using IMemoryOwner rowBlockBuffer = this.memoryAllocator.Allocate((int)bytesPerBlock, AllocationOptions.Clean); @@ -181,7 +185,7 @@ internal sealed class ExrEncoderCore Span blueBuffer = rgbBuffer.GetSpan().Slice(width * 2, width); Span alphaBuffer = rgbBuffer.GetSpan().Slice(width * 3, width); - using ExrBaseCompressor compressor = ExrCompressorFactory.Create(compression, this.memoryAllocator, stream, bytesPerBlock, bytesPerRow, rowsPerBlock, width); + using ExrBaseCompressor compressor = ExrCompressorFactory.Create(compression, this.memoryAllocator, stream, (uint)bytesPerBlock, (uint)bytesPerRow, rowsPerBlock, width); ulong[] rowOffsets = new ulong[height]; for (uint y = 0; y < height; y += rowsPerBlock) @@ -262,9 +266,13 @@ internal sealed class ExrEncoderCore CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { - uint bytesPerRow = (uint)ExrUtils.CalculateBytesPerRow(channels, (uint)width); + ulong bytesPerRow = ExrUtils.CalculateBytesPerRow(channels, (uint)width); uint rowsPerBlock = ExrUtils.RowsPerBlock(compression); - uint bytesPerBlock = bytesPerRow * rowsPerBlock; + ulong bytesPerBlock = bytesPerRow * rowsPerBlock; + if (bytesPerRow > uint.MaxValue || bytesPerBlock > int.MaxValue) + { + throw new ImageFormatException("Image is too large to encode in EXR format."); + } using IMemoryOwner rgbBuffer = this.memoryAllocator.Allocate(width * 4, AllocationOptions.Clean); using IMemoryOwner rowBlockBuffer = this.memoryAllocator.Allocate((int)bytesPerBlock, AllocationOptions.Clean); @@ -273,7 +281,7 @@ internal sealed class ExrEncoderCore Span blueBuffer = rgbBuffer.GetSpan().Slice(width * 2, width); Span alphaBuffer = rgbBuffer.GetSpan().Slice(width * 3, width); - using ExrBaseCompressor compressor = ExrCompressorFactory.Create(compression, this.memoryAllocator, stream, bytesPerBlock, bytesPerRow, rowsPerBlock, width); + using ExrBaseCompressor compressor = ExrCompressorFactory.Create(compression, this.memoryAllocator, stream, (uint)bytesPerBlock, (uint)bytesPerRow, rowsPerBlock, width); Rgba128 rgb = default; ulong[] rowOffsets = new ulong[height]; diff --git a/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderSecurityTests.cs b/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderSecurityTests.cs index 70f3d577e9..2d59c02355 100644 --- a/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderSecurityTests.cs +++ b/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderSecurityTests.cs @@ -13,6 +13,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Exr; /// The EXR decoder was merged to main but not yet included in a tagged NuGet release. /// Each test demonstrates a crafted-input crash present in the unfixed code. /// +[Trait("Format", "Exr")] +[ValidateDisposedMemoryAllocations] public class ExrDecoderSecurityTests { /// @@ -109,24 +111,21 @@ public class ExrDecoderSecurityTests } /// - /// EXR-3 — EXR bytesPerBlock uint Overflow Chain (DoS) + /// EXR-3 — Oversized EXR RGBA row sizing is rejected as invalid image content. /// - /// CalculateBytesPerRow is computed in ulong (fixed), and if the result exceeds - /// int.MaxValue the decoder throws InvalidImageContentException. With 4 RGBA HALF - /// channels and Width = 2^29: - /// bytesPerRow = 4 × 2 × 2^29 = 2^32 (> int.MaxValue) - /// → InvalidImageContentException before any allocation + /// With 4 RGBA HALF channels and Width = 2^29, the decoded row staging and + /// bytes-per-row arithmetic both exceed the supported buffer sizing limits. + /// The decoder must reject this as InvalidImageContentException before any allocation. /// /// Affected file: /// src/ImageSharp/Formats/Exr/ExrDecoderCore.cs lines 142–150, 215–223 /// src/ImageSharp/Formats/Exr/ExrUtils.cs CalculateBytesPerRow /// [Fact] - public void Decode_BytesPerBlockUintOverflow_Throws() + public void Decode_RgbaRowSizingExceedsBufferLimits_Throws() { - // 4 RGBA HALF channels, Width = 2^29: - // bytesPerRow = 4 × 2 × 536870912 = 4294967296 > int.MaxValue - // → InvalidImageContentException from the block-size guard + // 4 RGBA HALF channels at this width cannot be represented by the decoder's + // int-sized row staging or block buffers. byte[] data = BuildMinimalRgbaExr(xMin: 0, yMin: 0, xMax: 536870911, yMax: 0); using var stream = new MemoryStream(data); @@ -134,6 +133,30 @@ public class ExrDecoderSecurityTests () => ExrDecoder.Instance.Decode(DecoderOptions.Default, stream)); } + [Fact] + public void Decode_DataWindowWidthExceedsRowBufferLimit_Throws() + { + // A single HALF channel keeps bytesPerBlock below int.MaxValue, but the decoder + // still stages four color planes and must reject widths that overflow width × 4. + byte[] data = BuildMinimalExr(xMin: 0, yMin: 0, xMax: int.MaxValue / 4, yMax: 0); + + using var stream = new MemoryStream(data); + Assert.Throws( + () => ExrDecoder.Instance.Decode(DecoderOptions.Default, stream)); + } + + [Fact] + public void Identify_RowOffsetTableExceedsStream_Throws() + { + // Identify parses the header only, so this verifies the offset table bound is + // validated before scanline decoding reads from the table. + byte[] data = BuildMinimalExr(xMin: 0, yMin: 0, xMax: 1, yMax: 1); + + using var stream = new MemoryStream(data); + Assert.Throws( + () => ExrDecoder.Instance.Identify(DecoderOptions.Default, stream)); + } + // ------------------------------------------------------------------------- // Helpers: construct minimal valid-enough EXR scanline files. // @@ -144,7 +167,7 @@ public class ExrDecoderSecurityTests private static byte[] BuildMinimalExr( int xMin, int yMin, int xMax, int yMax, - byte[]? rowOffsetTableAppend = null) + byte[] rowOffsetTableAppend = null) { // channels: single "R" HALF channel with xSampling=1, ySampling=1 // Layout per ReadChannelInfo: name\0 (2) + pixelType (4) + pLinear+reserved (4) @@ -198,7 +221,7 @@ public class ExrDecoderSecurityTests private static byte[] BuildExrWithChannels( int xMin, int yMin, int xMax, int yMax, byte[] channelData, - byte[]? rowOffsetTableAppend = null) + byte[] rowOffsetTableAppend = null) { using var ms = new MemoryStream(); using var bw = new BinaryWriter(ms, System.Text.Encoding.ASCII, leaveOpen: true);