Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public ComponentProcessor(MemoryAllocator memoryAllocator, JpegFrame frame, Size
this.Component = component;

this.BlockAreaSize = component.SubSamplingDivisors * blockSize;
this.ColorBuffer = memoryAllocator.Allocate2DOveraligned<float>(
this.ColorBuffer = memoryAllocator.Allocate2DOverAligned<float>(
postProcessorBufferSize.Width,
postProcessorBufferSize.Height,
this.BlockAreaSize.Height);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public ComponentProcessor(MemoryAllocator memoryAllocator, Component component,
this.blockAreaSize = component.SubSamplingDivisors * 8;

// alignment of 8 so each block stride can be sampled from a single 'ref pointer'
this.ColorBuffer = memoryAllocator.Allocate2DOveraligned<float>(
this.ColorBuffer = memoryAllocator.Allocate2DOverAligned<float>(
postProcessorBufferSize.Width,
postProcessorBufferSize.Height,
8,
Expand Down
3 changes: 3 additions & 0 deletions src/ImageSharp/Formats/Png/PngDecoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1968,6 +1968,9 @@ private IMemoryOwner<byte> ReadChunkData(int length)
}

// We rent the buffer here to return it afterwards in Decode()
// We don't want to throw a degenerated memory exception here as we want to allow partial decoding
// so limit the length.
length = (int)Math.Min(length, this.currentStream.Length - this.currentStream.Position);
IMemoryOwner<byte> buffer = this.configuration.MemoryAllocator.Allocate<byte>(length, AllocationOptions.Clean);

this.currentStream.Read(buffer.GetSpan(), 0, length);
Expand Down
66 changes: 65 additions & 1 deletion src/ImageSharp/Memory/Allocators/MemoryAllocator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// Licensed under the Six Labors Split License.

using System.Buffers;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.PixelFormats;

namespace SixLabors.ImageSharp.Memory;

Expand All @@ -10,6 +12,16 @@ namespace SixLabors.ImageSharp.Memory;
/// </summary>
public abstract class MemoryAllocator
{
/// <summary>
/// Gets the default max allocatable size of a 1D buffer in bytes.
/// </summary>
public static readonly int DefaultMaxAllocatableSize1DInBytes = GetDefaultMaxAllocatableSize1DInBytes();

/// <summary>
/// Gets the default max allocatable size of a 2D buffer in bytes.
/// </summary>
public static readonly Size DefaultMaxAllocatableSize2DInBytes = GetDefaultMaxAllocatableSize2DInBytes();

/// <summary>
/// Gets the default platform-specific global <see cref="MemoryAllocator"/> instance that
/// serves as the default value for <see cref="Configuration.MemoryAllocator"/>.
Expand All @@ -20,6 +32,18 @@ public abstract class MemoryAllocator
/// </summary>
public static MemoryAllocator Default { get; } = Create();

/// <summary>
/// Gets or sets the maximum allowable allocatable size of a 2 dimensional buffer.
/// Defaults to <value><see cref="DefaultMaxAllocatableSize2DInBytes"/>.</value>
/// </summary>
public Size MaxAllocatableSize2DInBytes { get; set; } = DefaultMaxAllocatableSize2DInBytes;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of defining 2D dimension limits, we should define a memory group limit in bytes. Also see my comment on GetDefaultMaxAllocatableSize2DInBytes.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need it to somehow reflect the actual dimensions of a buffer (to the user an image). A memory group is an implementation detail but if I can say don't allow a total allocation of more than X, Y then the groups are covered.

Copy link
Member

@antonfirsov antonfirsov Mar 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to see it the other way around. If you define a limit for a discontigous allocation it will cover the image limit.

A memory group is an implementation

Not entirely, we have documented that our buffers are discontiguous and IMemoryGroup<T> is public. Regardless, we don't need to involve those concepts to when defining the limits. We can just document that maximum in-memory image size is X (mega/giga/tera)bytes. It is much better to reason about from memory management perspective.

Copy link
Member

@antonfirsov antonfirsov Mar 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we believe we need dimension limits (eg. bc various formats have them anyways), we can apply them separately either in Image<T> or in Buffer2D<T> code, without involving sizeof(TPixel). But for me that concern is orthogonal to the issue this PR is aimed to fix.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue directly stems from attempt to allocate an image that is too large. Fixing any additional allocation limits was a something we needed to do in addition to that public facing allocation.

By placing a limit at the point of allocation call, before we start even looking at memory groups, I avoid the possibility of over allocation in all cases. The limits are not dimensional limits, they reflect dimensional limits. We can now say, we've defaulted to an image of X*Y at this pixel format bit depth. It's a starting point that users can understand and update when required.

Whether our buffers are discontiguous doesn't really change things. We've already established that attempting to allocate an image of overly large dimensions will cause issues.


/// <summary>
/// Gets or sets the maximum allowable allocatable size of a 1 dimensional buffer.
/// </summary>
/// Defaults to <value><see cref="GetDefaultMaxAllocatableSize1DInBytes"/>.</value>
public int MaxAllocatableSize1DInBytes { get; set; } = DefaultMaxAllocatableSize1DInBytes;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is in fact a limit for a single contigous buffer.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, yes, but you have a different internal value for that which is used for contiguous chunks within a 2D buffer. I had to create something new.


/// <summary>
/// Gets the length of the largest contiguous buffer that can be handled by this allocator instance in bytes.
/// </summary>
Expand All @@ -42,7 +66,7 @@ public static MemoryAllocator Create(MemoryAllocatorOptions options) =>
new UniformUnmanagedMemoryPoolMemoryAllocator(options.MaximumPoolSizeMegabytes);

/// <summary>
/// Allocates an <see cref="IMemoryOwner{T}" />, holding a <see cref="Memory{T}"/> of length <paramref name="length"/>.
/// Allocates an <see cref="IMemoryOwner{T}"/>, holding a <see cref="Memory{T}"/> of length <paramref name="length"/>.
/// </summary>
/// <typeparam name="T">Type of the data stored in the buffer.</typeparam>
/// <param name="length">Size of the buffer to allocate.</param>
Expand All @@ -64,6 +88,7 @@ public virtual void ReleaseRetainedResources()
/// <summary>
/// Allocates a <see cref="MemoryGroup{T}"/>.
/// </summary>
/// <typeparam name="T">Type of the data stored in the buffer.</typeparam>
/// <param name="totalLength">The total length of the buffer.</param>
/// <param name="bufferAlignment">The expected alignment (eg. to make sure image rows fit into single buffers).</param>
/// <param name="options">The <see cref="AllocationOptions"/>.</param>
Expand All @@ -75,4 +100,43 @@ internal virtual MemoryGroup<T> AllocateGroup<T>(
AllocationOptions options = AllocationOptions.None)
where T : struct
=> MemoryGroup<T>.Allocate(this, totalLength, bufferAlignment, options);

internal static void MemoryGuardMustBeBetweenOrEqualTo<T>(int value, int min, int max, string paramName)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

min is 0 at each call of this method. I think it would be better to redefine this method as a buffer length validator, and throw a different exception (or use a different exception message) for length < 0. Also, length == 0 calls are suspicious but maybe there are valid cases (when not, we should disallow them at higher levels).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We allow 0 for some reason, returning an empty buffer. Don't ask me why 🤷

where T : struct
{
int typeSizeInBytes = Unsafe.SizeOf<T>();
long valueInBytes = value * typeSizeInBytes;

// If a sufficiently large value is passed in, the multiplication will overflow.
// We can detect this by checking if the result is less than the original value.
if (valueInBytes < value && value > 0)
{
valueInBytes = long.MaxValue;
}

if (valueInBytes >= min && valueInBytes <= max)
{
return;
}

throw new InvalidMemoryOperationException($"Parameter \"{paramName}\" must be between or equal to {min} and {max}, was {valueInBytes}");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For value>max, it would be nice to have an exception message that is more indicative of a huge allocation attempt.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dunno, they get a stack trace, they know it's from an allocation attempt.

}

private static Size GetDefaultMaxAllocatableSize2DInBytes()
{
// Limit dimensions to 65535x65535 and 32767x32767 @ 4 bytes per pixel for 64 and 32 bit processes respectively.
int maxLength = Environment.Is64BitProcess ? ushort.MaxValue : short.MaxValue;
int maxLengthInRgba32Bytes = maxLength * Unsafe.SizeOf<Rgba32>();
return new(maxLengthInRgba32Bytes, maxLengthInRgba32Bytes);
Copy link
Member

@antonfirsov antonfirsov Mar 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The largest memory group that will pass the check against this limit is of size width * sizeof(T) * height * sizeof(T) = 65535^2 * sizeof(Rgba32)^2, while we want it to be 65535^2 * sizeof(Rgba32).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We want 65535 * 4 in each direction. That's what I'm returning.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No we don't... We want 65535 * 4 * 65535. Will update,

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That doesn't make sense mathematically.

If we want the image area not to be larger than 65535 * 65535 of Rgba, that area is 65535 * 65535 * sizeof(Rgba) = 16 TB. The logic in the PR will allow 64 TB instead.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that's what I said in my follow up.
The area is 65535 rows of 65535 Rgba32 so (65535 * 4) * 65535

}

private static int GetDefaultMaxAllocatableSize1DInBytes()
{
// It's possible to require buffers that are not related to image dimensions.
// For example, when we need to allocate buffers for IDAT chunks in PNG files or when allocating
// cache buffers for image quantization.
// Limit the maximum buffer size to 64MB for 64bit processes and 32MB for 32 bit processes.
int limitInMB = Environment.Is64BitProcess ? 64 : 32;
return limitInMB * 1024 * 1024;
}
}
6 changes: 3 additions & 3 deletions src/ImageSharp/Memory/Allocators/SimpleGcMemoryAllocator.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Six Labors.
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

using System.Buffers;
Expand All @@ -7,7 +7,7 @@
namespace SixLabors.ImageSharp.Memory;

/// <summary>
/// Implements <see cref="MemoryAllocator"/> by newing up managed arrays on every allocation request.
/// Implements <see cref="MemoryAllocator"/> by creating new managed arrays on every allocation request.
/// </summary>
public sealed class SimpleGcMemoryAllocator : MemoryAllocator
{
Expand All @@ -17,7 +17,7 @@ public sealed class SimpleGcMemoryAllocator : MemoryAllocator
/// <inheritdoc />
public override IMemoryOwner<T> Allocate<T>(int length, AllocationOptions options = AllocationOptions.None)
{
Guard.MustBeGreaterThanOrEqualTo(length, 0, nameof(length));
MemoryGuardMustBeBetweenOrEqualTo<T>(length, 0, this.MaxAllocatableSize1DInBytes, nameof(length));

return new BasicArrayBuffer<T>(new T[length]);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,16 +79,14 @@ internal UniformUnmanagedMemoryPoolMemoryAllocator(
protected internal override int GetBufferCapacityInBytes() => this.poolBufferSizeInBytes;

/// <inheritdoc />
public override IMemoryOwner<T> Allocate<T>(
int length,
AllocationOptions options = AllocationOptions.None)
public override IMemoryOwner<T> Allocate<T>(int length, AllocationOptions options = AllocationOptions.None)
{
Guard.MustBeGreaterThanOrEqualTo(length, 0, nameof(length));
MemoryGuardMustBeBetweenOrEqualTo<T>(length, 0, this.MaxAllocatableSize1DInBytes, nameof(length));
int lengthInBytes = length * Unsafe.SizeOf<T>();

if (lengthInBytes <= this.sharedArrayPoolThresholdInBytes)
{
var buffer = new SharedArrayPoolBuffer<T>(length);
SharedArrayPoolBuffer<T> buffer = new(length);
if (options.Has(AllocationOptions.Clean))
{
buffer.GetSpan().Clear();
Expand All @@ -102,8 +100,7 @@ public override IMemoryOwner<T> Allocate<T>(
UnmanagedMemoryHandle mem = this.pool.Rent();
if (mem.IsValid)
{
UnmanagedBuffer<T> buffer = this.pool.CreateGuardedBuffer<T>(mem, length, options.Has(AllocationOptions.Clean));
return buffer;
return this.pool.CreateGuardedBuffer<T>(mem, length, options.Has(AllocationOptions.Clean));
}
}

Expand All @@ -124,7 +121,7 @@ internal override MemoryGroup<T> AllocateGroup<T>(

if (totalLengthInBytes <= this.sharedArrayPoolThresholdInBytes)
{
var buffer = new SharedArrayPoolBuffer<T>((int)totalLength);
SharedArrayPoolBuffer<T> buffer = new((int)totalLength);
return MemoryGroup<T>.CreateContiguous(buffer, options.Has(AllocationOptions.Clean));
}

Expand Down
4 changes: 3 additions & 1 deletion src/ImageSharp/Memory/Allocators/UnmanagedMemoryAllocator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ internal class UnmanagedMemoryAllocator : MemoryAllocator

public override IMemoryOwner<T> Allocate<T>(int length, AllocationOptions options = AllocationOptions.None)
{
var buffer = UnmanagedBuffer<T>.Allocate(length);
MemoryGuardMustBeBetweenOrEqualTo<T>(length, 0, this.MaxAllocatableSize1DInBytes, nameof(length));

UnmanagedBuffer<T> buffer = UnmanagedBuffer<T>.Allocate(length);
if (options.Has(AllocationOptions.Clean))
{
buffer.GetSpan().Clear();
Expand Down
22 changes: 15 additions & 7 deletions src/ImageSharp/Memory/MemoryAllocatorExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,23 @@ public static class MemoryAllocatorExtensions
/// <param name="memoryAllocator">The memory allocator.</param>
/// <param name="width">The buffer width.</param>
/// <param name="height">The buffer height.</param>
/// <param name="preferContiguosImageBuffers">A value indicating whether the allocated buffer should be contiguous, unless bigger than <see cref="int.MaxValue"/>.</param>
/// <param name="preferContiguousImageBuffers">A value indicating whether the allocated buffer should be contiguous, unless bigger than <see cref="int.MaxValue"/>.</param>
/// <param name="options">The allocation options.</param>
/// <returns>The <see cref="Buffer2D{T}"/>.</returns>
public static Buffer2D<T> Allocate2D<T>(
this MemoryAllocator memoryAllocator,
int width,
int height,
bool preferContiguosImageBuffers,
bool preferContiguousImageBuffers,
AllocationOptions options = AllocationOptions.None)
where T : struct
{
MemoryAllocator.MemoryGuardMustBeBetweenOrEqualTo<T>(width, 0, memoryAllocator.MaxAllocatableSize2DInBytes.Width, nameof(width));
MemoryAllocator.MemoryGuardMustBeBetweenOrEqualTo<T>(height, 0, memoryAllocator.MaxAllocatableSize2DInBytes.Height, nameof(height));

long groupLength = (long)width * height;
MemoryGroup<T> memoryGroup;
if (preferContiguosImageBuffers && groupLength < int.MaxValue)
if (preferContiguousImageBuffers && groupLength < int.MaxValue)
{
IMemoryOwner<T> buffer = memoryAllocator.Allocate<T>((int)groupLength, options);
memoryGroup = MemoryGroup<T>.CreateContiguous(buffer, false);
Expand Down Expand Up @@ -69,16 +72,16 @@ public static Buffer2D<T> Allocate2D<T>(
/// <typeparam name="T">The type of buffer items to allocate.</typeparam>
/// <param name="memoryAllocator">The memory allocator.</param>
/// <param name="size">The buffer size.</param>
/// <param name="preferContiguosImageBuffers">A value indicating whether the allocated buffer should be contiguous, unless bigger than <see cref="int.MaxValue"/>.</param>
/// <param name="preferContiguousImageBuffers">A value indicating whether the allocated buffer should be contiguous, unless bigger than <see cref="int.MaxValue"/>.</param>
/// <param name="options">The allocation options.</param>
/// <returns>The <see cref="Buffer2D{T}"/>.</returns>
public static Buffer2D<T> Allocate2D<T>(
this MemoryAllocator memoryAllocator,
Size size,
bool preferContiguosImageBuffers,
bool preferContiguousImageBuffers,
AllocationOptions options = AllocationOptions.None)
where T : struct =>
Allocate2D<T>(memoryAllocator, size.Width, size.Height, preferContiguosImageBuffers, options);
Allocate2D<T>(memoryAllocator, size.Width, size.Height, preferContiguousImageBuffers, options);

/// <summary>
/// Allocates a buffer of value type objects interpreted as a 2D region
Expand All @@ -96,14 +99,17 @@ public static Buffer2D<T> Allocate2D<T>(
where T : struct =>
Allocate2D<T>(memoryAllocator, size.Width, size.Height, false, options);

internal static Buffer2D<T> Allocate2DOveraligned<T>(
internal static Buffer2D<T> Allocate2DOverAligned<T>(
this MemoryAllocator memoryAllocator,
int width,
int height,
int alignmentMultiplier,
AllocationOptions options = AllocationOptions.None)
where T : struct
{
MemoryAllocator.MemoryGuardMustBeBetweenOrEqualTo<T>(width, 0, memoryAllocator.MaxAllocatableSize2DInBytes.Width, nameof(width));
MemoryAllocator.MemoryGuardMustBeBetweenOrEqualTo<T>(height, 0, memoryAllocator.MaxAllocatableSize2DInBytes.Height, nameof(height));

long groupLength = (long)width * height;
MemoryGroup<T> memoryGroup = memoryAllocator.AllocateGroup<T>(
groupLength,
Expand All @@ -127,6 +133,8 @@ internal static IMemoryOwner<byte> AllocatePaddedPixelRowBuffer(
int paddingInBytes)
{
int length = (width * pixelSizeInBytes) + paddingInBytes;
MemoryAllocator.MemoryGuardMustBeBetweenOrEqualTo<byte>(length, 0, memoryAllocator.MaxAllocatableSize1DInBytes, nameof(length));

return memoryAllocator.Allocate<byte>(length);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ private ResizeKernelMap(
this.sourceLength = sourceLength;
this.DestinationLength = destinationLength;
this.MaxDiameter = (radius * 2) + 1;
this.data = memoryAllocator.Allocate2D<float>(this.MaxDiameter, bufferHeight, preferContiguosImageBuffers: true, AllocationOptions.Clean);
this.data = memoryAllocator.Allocate2D<float>(this.MaxDiameter, bufferHeight, preferContiguousImageBuffers: true, AllocationOptions.Clean);
this.pinHandle = this.data.DangerousGetSingleMemory().Pin();
this.kernels = new ResizeKernel[destinationLength];
this.tempValues = new double[this.MaxDiameter];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ public ResizeWorker(
this.transposedFirstPassBuffer = configuration.MemoryAllocator.Allocate2D<Vector4>(
this.workerHeight,
targetWorkingRect.Width,
preferContiguosImageBuffers: true,
preferContiguousImageBuffers: true,
options: AllocationOptions.Clean);

this.tempRowBuffer = configuration.MemoryAllocator.Allocate<Vector4>(this.sourceRectangle.Width);
Expand Down
9 changes: 9 additions & 0 deletions tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -558,4 +558,13 @@ public void BmpDecoder_CanDecode_Os2BitmapArray<TPixel>(TestImageProvider<TPixel
// Compare to reference output instead.
image.CompareToReferenceOutput(provider, extension: "png");
}

[Theory]
[WithFile(Issue2696, PixelTypes.Rgba32)]
public void BmpDecoder_ThrowsException_Issue2696<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
=> Assert.Throws<InvalidImageContentException>(() =>
{
using Image<TPixel> image = provider.GetImage(BmpDecoder.Instance);
});
}
16 changes: 12 additions & 4 deletions tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
Expand Down Expand Up @@ -348,14 +349,21 @@ public void Encode_PreservesColorProfile<TPixel>(TestImageProvider<TPixel> provi
Assert.Equal(expectedProfileBytes, actualProfileBytes);
}

public static TheoryData<Size> Encode_WorksWithSizeGreaterThen65k_Data { get; set; } = new()
{
{ new Size(1, MemoryAllocator.DefaultMaxAllocatableSize2DInBytes.Height + 1) },
{ new Size(MemoryAllocator.DefaultMaxAllocatableSize2DInBytes.Width + 1, 1) }
};

[Theory]
[InlineData(1, 66535)]
[InlineData(66535, 1)]
public void Encode_WorksWithSizeGreaterThen65k(int width, int height)
[MemberData(nameof(Encode_WorksWithSizeGreaterThen65k_Data))]
public void Encode_WorksWithSizeGreaterThen65k(Size size)
{
Exception exception = Record.Exception(() =>
{
using Image image = new Image<Rgba32>(width, height);
Configuration configuration = Configuration.CreateDefaultInstance();
configuration.MemoryAllocator = new UniformUnmanagedMemoryPoolMemoryAllocator(null) { MaxAllocatableSize2DInBytes = size + new Size(1, 1) };
using Image image = new Image<L8>(configuration, size.Width, size.Height);
using MemoryStream memStream = new();
image.Save(memStream, BmpEncoder);
});
Expand Down
15 changes: 2 additions & 13 deletions tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -339,20 +339,9 @@ public void Issue2564_DecodeWorks<TPixel>(TestImageProvider<TPixel> provider)

[Theory]
[WithFile(TestImages.Jpeg.Issues.HangBadScan, PixelTypes.L8)]
public void DecodeHang<TPixel>(TestImageProvider<TPixel> provider)
public void DecodeHang_ThrowsException<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
if (TestEnvironment.IsWindows &&
TestEnvironment.RunsOnCI)
{
// Windows CI runs consistently fail with OOM.
return;
}

using Image<TPixel> image = provider.GetImage(JpegDecoder.Instance);
Assert.Equal(65503, image.Width);
Assert.Equal(65503, image.Height);
}
=> Assert.Throws<InvalidImageContentException>(() => { using Image<TPixel> image = provider.GetImage(JpegDecoder.Instance); });

// https://github.com/SixLabors/ImageSharp/issues/2517
[Theory]
Expand Down
Loading