Browse Source

EXR: validate sizes, prevent overflows, dispose image

pull/3126/head
James Jackson-South 2 months ago
parent
commit
e144a59dbb
  1. 59
      src/ImageSharp/Formats/Exr/ExrDecoderCore.cs
  2. 20
      src/ImageSharp/Formats/Exr/ExrEncoderCore.cs
  3. 47
      tests/ImageSharp.Tests/Formats/Exr/ExrDecoderSecurityTests.cs

59
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<TPixel> image = new(this.configuration, this.Width, this.Height, this.metadata);
Buffer2D<TPixel> 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<TPixel> image = null;
try
{
image = new Image<TPixel>(this.configuration, this.Width, this.Height, this.metadata);
Buffer2D<TPixel> 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;
}
}
/// <inheritdoc />
@ -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();

20
src/ImageSharp/Formats/Exr/ExrEncoderCore.cs

@ -170,9 +170,13 @@ internal sealed class ExrEncoderCore
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
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<float> rgbBuffer = this.memoryAllocator.Allocate<float>(width * 4, AllocationOptions.Clean);
using IMemoryOwner<byte> rowBlockBuffer = this.memoryAllocator.Allocate<byte>((int)bytesPerBlock, AllocationOptions.Clean);
@ -181,7 +185,7 @@ internal sealed class ExrEncoderCore
Span<float> blueBuffer = rgbBuffer.GetSpan().Slice(width * 2, width);
Span<float> 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<TPixel>
{
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<uint> rgbBuffer = this.memoryAllocator.Allocate<uint>(width * 4, AllocationOptions.Clean);
using IMemoryOwner<byte> rowBlockBuffer = this.memoryAllocator.Allocate<byte>((int)bytesPerBlock, AllocationOptions.Clean);
@ -273,7 +281,7 @@ internal sealed class ExrEncoderCore
Span<uint> blueBuffer = rgbBuffer.GetSpan().Slice(width * 2, width);
Span<uint> 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];

47
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.
/// </summary>
[Trait("Format", "Exr")]
[ValidateDisposedMemoryAllocations]
public class ExrDecoderSecurityTests
{
/// <summary>
@ -109,24 +111,21 @@ public class ExrDecoderSecurityTests
}
/// <summary>
/// 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
/// </summary>
[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<Rgba32>(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<InvalidImageContentException>(
() => ExrDecoder.Instance.Decode<Rgba32>(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<InvalidImageContentException>(
() => 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);

Loading…
Cancel
Save