Skip to content

Commit e144a59

Browse files
EXR: validate sizes, prevent overflows, dispose image
1 parent d60ca72 commit e144a59

3 files changed

Lines changed: 89 additions & 37 deletions

File tree

src/ImageSharp/Formats/Exr/ExrDecoderCore.cs

Lines changed: 40 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -105,24 +105,33 @@ protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, Cance
105105
ExrThrowHelper.ThrowNotSupported($"Compression {this.Compression} is not yet supported");
106106
}
107107

108-
Image<TPixel> image = new(this.configuration, this.Width, this.Height, this.metadata);
109-
Buffer2D<TPixel> pixels = image.GetRootFramePixelBuffer();
110-
111-
switch (this.PixelType)
112-
{
113-
case ExrPixelType.Half:
114-
case ExrPixelType.Float:
115-
this.DecodeFloatingPointPixelData(stream, pixels, cancellationToken);
116-
break;
117-
case ExrPixelType.UnsignedInt:
118-
this.DecodeUnsignedIntPixelData(stream, pixels, cancellationToken);
119-
break;
120-
default:
121-
ExrThrowHelper.ThrowNotSupported("Pixel type is not supported");
122-
break;
123-
}
108+
Image<TPixel> image = null;
109+
try
110+
{
111+
image = new Image<TPixel>(this.configuration, this.Width, this.Height, this.metadata);
112+
Buffer2D<TPixel> pixels = image.GetRootFramePixelBuffer();
113+
114+
switch (this.PixelType)
115+
{
116+
case ExrPixelType.Half:
117+
case ExrPixelType.Float:
118+
this.DecodeFloatingPointPixelData(stream, pixels, cancellationToken);
119+
break;
120+
case ExrPixelType.UnsignedInt:
121+
this.DecodeUnsignedIntPixelData(stream, pixels, cancellationToken);
122+
break;
123+
default:
124+
ExrThrowHelper.ThrowNotSupported("Pixel type is not supported");
125+
break;
126+
}
124127

125-
return image;
128+
return image;
129+
}
130+
catch
131+
{
132+
image?.Dispose();
133+
throw;
134+
}
126135
}
127136

128137
/// <inheritdoc />
@@ -622,7 +631,10 @@ private ExrHeaderAttributes ReadExrHeader(BufferedReadStream stream)
622631

623632
long width = (long)dataWindow.XMax - dataWindow.XMin + 1;
624633
long height = (long)dataWindow.YMax - dataWindow.YMin + 1;
625-
if (width > int.MaxValue || height > int.MaxValue)
634+
635+
// Decoding stages each row as four color planes, so the width must be bounded
636+
// before later width * 4 buffer sizing can overflow.
637+
if (width > int.MaxValue / 4 || height > int.MaxValue)
626638
{
627639
ExrThrowHelper.ThrowInvalidImageContentException("EXR DataWindow dimensions exceed the maximum allowed size.");
628640
}
@@ -633,7 +645,16 @@ private ExrHeaderAttributes ReadExrHeader(BufferedReadStream stream)
633645
this.Compression = this.HeaderAttributes.Compression;
634646
uint rowsPerBlock = ExrUtils.RowsPerBlock(this.Compression);
635647
long chunkCount = (this.Height + (long)rowsPerBlock - 1) / rowsPerBlock;
636-
this.MinimumChunkOffset = stream.Position + (chunkCount * sizeof(ulong));
648+
long offsetTableByteCount = chunkCount * sizeof(ulong);
649+
650+
// The scanline offset table sits between the header and pixel chunks; proving it
651+
// fits in the stream keeps all later chunk offsets on the pixel-data side.
652+
if (stream.Position > stream.Length || offsetTableByteCount > stream.Length - stream.Position)
653+
{
654+
ExrThrowHelper.ThrowInvalidImageContentException("EXR chunk offset table is outside the bounds of the stream.");
655+
}
656+
657+
this.MinimumChunkOffset = stream.Position + offsetTableByteCount;
637658
this.PixelType = this.ValidateChannels();
638659
this.ImageDataType = this.DetermineImageDataType();
639660

src/ImageSharp/Formats/Exr/ExrEncoderCore.cs

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -170,9 +170,13 @@ private ulong[] EncodeFloatingPointPixelData<TPixel>(
170170
CancellationToken cancellationToken)
171171
where TPixel : unmanaged, IPixel<TPixel>
172172
{
173-
uint bytesPerRow = (uint)ExrUtils.CalculateBytesPerRow(channels, (uint)width);
173+
ulong bytesPerRow = ExrUtils.CalculateBytesPerRow(channels, (uint)width);
174174
uint rowsPerBlock = ExrUtils.RowsPerBlock(compression);
175-
uint bytesPerBlock = bytesPerRow * rowsPerBlock;
175+
ulong bytesPerBlock = bytesPerRow * rowsPerBlock;
176+
if (bytesPerRow > uint.MaxValue || bytesPerBlock > int.MaxValue)
177+
{
178+
throw new ImageFormatException("Image is too large to encode in EXR format.");
179+
}
176180

177181
using IMemoryOwner<float> rgbBuffer = this.memoryAllocator.Allocate<float>(width * 4, AllocationOptions.Clean);
178182
using IMemoryOwner<byte> rowBlockBuffer = this.memoryAllocator.Allocate<byte>((int)bytesPerBlock, AllocationOptions.Clean);
@@ -181,7 +185,7 @@ private ulong[] EncodeFloatingPointPixelData<TPixel>(
181185
Span<float> blueBuffer = rgbBuffer.GetSpan().Slice(width * 2, width);
182186
Span<float> alphaBuffer = rgbBuffer.GetSpan().Slice(width * 3, width);
183187

184-
using ExrBaseCompressor compressor = ExrCompressorFactory.Create(compression, this.memoryAllocator, stream, bytesPerBlock, bytesPerRow, rowsPerBlock, width);
188+
using ExrBaseCompressor compressor = ExrCompressorFactory.Create(compression, this.memoryAllocator, stream, (uint)bytesPerBlock, (uint)bytesPerRow, rowsPerBlock, width);
185189

186190
ulong[] rowOffsets = new ulong[height];
187191
for (uint y = 0; y < height; y += rowsPerBlock)
@@ -262,9 +266,13 @@ private ulong[] EncodeUnsignedIntPixelData<TPixel>(
262266
CancellationToken cancellationToken)
263267
where TPixel : unmanaged, IPixel<TPixel>
264268
{
265-
uint bytesPerRow = (uint)ExrUtils.CalculateBytesPerRow(channels, (uint)width);
269+
ulong bytesPerRow = ExrUtils.CalculateBytesPerRow(channels, (uint)width);
266270
uint rowsPerBlock = ExrUtils.RowsPerBlock(compression);
267-
uint bytesPerBlock = bytesPerRow * rowsPerBlock;
271+
ulong bytesPerBlock = bytesPerRow * rowsPerBlock;
272+
if (bytesPerRow > uint.MaxValue || bytesPerBlock > int.MaxValue)
273+
{
274+
throw new ImageFormatException("Image is too large to encode in EXR format.");
275+
}
268276

269277
using IMemoryOwner<uint> rgbBuffer = this.memoryAllocator.Allocate<uint>(width * 4, AllocationOptions.Clean);
270278
using IMemoryOwner<byte> rowBlockBuffer = this.memoryAllocator.Allocate<byte>((int)bytesPerBlock, AllocationOptions.Clean);
@@ -273,7 +281,7 @@ private ulong[] EncodeUnsignedIntPixelData<TPixel>(
273281
Span<uint> blueBuffer = rgbBuffer.GetSpan().Slice(width * 2, width);
274282
Span<uint> alphaBuffer = rgbBuffer.GetSpan().Slice(width * 3, width);
275283

276-
using ExrBaseCompressor compressor = ExrCompressorFactory.Create(compression, this.memoryAllocator, stream, bytesPerBlock, bytesPerRow, rowsPerBlock, width);
284+
using ExrBaseCompressor compressor = ExrCompressorFactory.Create(compression, this.memoryAllocator, stream, (uint)bytesPerBlock, (uint)bytesPerRow, rowsPerBlock, width);
277285

278286
Rgba128 rgb = default;
279287
ulong[] rowOffsets = new ulong[height];

tests/ImageSharp.Tests/Formats/Exr/ExrDecoderSecurityTests.cs

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Exr;
1313
/// The EXR decoder was merged to main but not yet included in a tagged NuGet release.
1414
/// Each test demonstrates a crafted-input crash present in the unfixed code.
1515
/// </summary>
16+
[Trait("Format", "Exr")]
17+
[ValidateDisposedMemoryAllocations]
1618
public class ExrDecoderSecurityTests
1719
{
1820
/// <summary>
@@ -109,31 +111,52 @@ public void Decode_CraftedRowOffsets_IntoOffsetTable_Throws()
109111
}
110112

111113
/// <summary>
112-
/// EXR-3 — EXR bytesPerBlock uint Overflow Chain (DoS)
114+
/// EXR-3 — Oversized EXR RGBA row sizing is rejected as invalid image content.
113115
///
114-
/// CalculateBytesPerRow is computed in ulong (fixed), and if the result exceeds
115-
/// int.MaxValue the decoder throws InvalidImageContentException. With 4 RGBA HALF
116-
/// channels and Width = 2^29:
117-
/// bytesPerRow = 4 × 2 × 2^29 = 2^32 (> int.MaxValue)
118-
/// → InvalidImageContentException before any allocation
116+
/// With 4 RGBA HALF channels and Width = 2^29, the decoded row staging and
117+
/// bytes-per-row arithmetic both exceed the supported buffer sizing limits.
118+
/// The decoder must reject this as InvalidImageContentException before any allocation.
119119
///
120120
/// Affected file:
121121
/// src/ImageSharp/Formats/Exr/ExrDecoderCore.cs lines 142–150, 215–223
122122
/// src/ImageSharp/Formats/Exr/ExrUtils.cs CalculateBytesPerRow
123123
/// </summary>
124124
[Fact]
125-
public void Decode_BytesPerBlockUintOverflow_Throws()
125+
public void Decode_RgbaRowSizingExceedsBufferLimits_Throws()
126126
{
127-
// 4 RGBA HALF channels, Width = 2^29:
128-
// bytesPerRow = 4 × 2 × 536870912 = 4294967296 > int.MaxValue
129-
// → InvalidImageContentException from the block-size guard
127+
// 4 RGBA HALF channels at this width cannot be represented by the decoder's
128+
// int-sized row staging or block buffers.
130129
byte[] data = BuildMinimalRgbaExr(xMin: 0, yMin: 0, xMax: 536870911, yMax: 0);
131130

132131
using var stream = new MemoryStream(data);
133132
Assert.Throws<InvalidImageContentException>(
134133
() => ExrDecoder.Instance.Decode<Rgba32>(DecoderOptions.Default, stream));
135134
}
136135

136+
[Fact]
137+
public void Decode_DataWindowWidthExceedsRowBufferLimit_Throws()
138+
{
139+
// A single HALF channel keeps bytesPerBlock below int.MaxValue, but the decoder
140+
// still stages four color planes and must reject widths that overflow width × 4.
141+
byte[] data = BuildMinimalExr(xMin: 0, yMin: 0, xMax: int.MaxValue / 4, yMax: 0);
142+
143+
using var stream = new MemoryStream(data);
144+
Assert.Throws<InvalidImageContentException>(
145+
() => ExrDecoder.Instance.Decode<Rgba32>(DecoderOptions.Default, stream));
146+
}
147+
148+
[Fact]
149+
public void Identify_RowOffsetTableExceedsStream_Throws()
150+
{
151+
// Identify parses the header only, so this verifies the offset table bound is
152+
// validated before scanline decoding reads from the table.
153+
byte[] data = BuildMinimalExr(xMin: 0, yMin: 0, xMax: 1, yMax: 1);
154+
155+
using var stream = new MemoryStream(data);
156+
Assert.Throws<InvalidImageContentException>(
157+
() => ExrDecoder.Instance.Identify(DecoderOptions.Default, stream));
158+
}
159+
137160
// -------------------------------------------------------------------------
138161
// Helpers: construct minimal valid-enough EXR scanline files.
139162
//
@@ -144,7 +167,7 @@ public void Decode_BytesPerBlockUintOverflow_Throws()
144167

145168
private static byte[] BuildMinimalExr(
146169
int xMin, int yMin, int xMax, int yMax,
147-
byte[]? rowOffsetTableAppend = null)
170+
byte[] rowOffsetTableAppend = null)
148171
{
149172
// channels: single "R" HALF channel with xSampling=1, ySampling=1
150173
// Layout per ReadChannelInfo: name\0 (2) + pixelType (4) + pLinear+reserved (4)
@@ -198,7 +221,7 @@ private static byte[] BuildMinimalRgbaExr(int xMin, int yMin, int xMax, int yMax
198221
private static byte[] BuildExrWithChannels(
199222
int xMin, int yMin, int xMax, int yMax,
200223
byte[] channelData,
201-
byte[]? rowOffsetTableAppend = null)
224+
byte[] rowOffsetTableAppend = null)
202225
{
203226
using var ms = new MemoryStream();
204227
using var bw = new BinaryWriter(ms, System.Text.Encoding.ASCII, leaveOpen: true);

0 commit comments

Comments
 (0)