Skip to content

Commit b6b08ac

Browse files
authored
Merge pull request #2706 from SixLabors/af/memlimit-01
Limit all memory allocations in the MemoryAllocator layer
2 parents 4a26acb + 572366e commit b6b08ac

16 files changed

+216
-56
lines changed

src/ImageSharp/Formats/Png/PngDecoderCore.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1979,6 +1979,9 @@ private IMemoryOwner<byte> ReadChunkData(int length)
19791979
}
19801980

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

19841987
this.currentStream.Read(buffer.GetSpan(), 0, length);

src/ImageSharp/Memory/Allocators/Internals/UnmanagedMemoryHandle.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Memory.Internals;
1111
internal struct UnmanagedMemoryHandle : IEquatable<UnmanagedMemoryHandle>
1212
{
1313
// Number of allocation re-attempts when detecting OutOfMemoryException.
14-
private const int MaxAllocationAttempts = 1000;
14+
private const int MaxAllocationAttempts = 10;
1515

1616
// Track allocations for testing purposes:
1717
private static int totalOutstandingHandles;

src/ImageSharp/Memory/Allocators/MemoryAllocator.cs

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the Six Labors Split License.
33

44
using System.Buffers;
5+
using System.Runtime.CompilerServices;
56

67
namespace SixLabors.ImageSharp.Memory;
78

@@ -10,6 +11,8 @@ namespace SixLabors.ImageSharp.Memory;
1011
/// </summary>
1112
public abstract class MemoryAllocator
1213
{
14+
private const int OneGigabyte = 1 << 30;
15+
1316
/// <summary>
1417
/// Gets the default platform-specific global <see cref="MemoryAllocator"/> instance that
1518
/// serves as the default value for <see cref="Configuration.MemoryAllocator"/>.
@@ -20,6 +23,10 @@ public abstract class MemoryAllocator
2023
/// </summary>
2124
public static MemoryAllocator Default { get; } = Create();
2225

26+
internal long MemoryGroupAllocationLimitBytes { get; private set; } = Environment.Is64BitProcess ? 4L * OneGigabyte : OneGigabyte;
27+
28+
internal int SingleBufferAllocationLimitBytes { get; private set; } = OneGigabyte;
29+
2330
/// <summary>
2431
/// Gets the length of the largest contiguous buffer that can be handled by this allocator instance in bytes.
2532
/// </summary>
@@ -30,16 +37,24 @@ public abstract class MemoryAllocator
3037
/// Creates a default instance of a <see cref="MemoryAllocator"/> optimized for the executing platform.
3138
/// </summary>
3239
/// <returns>The <see cref="MemoryAllocator"/>.</returns>
33-
public static MemoryAllocator Create() =>
34-
new UniformUnmanagedMemoryPoolMemoryAllocator(null);
40+
public static MemoryAllocator Create() => Create(default);
3541

3642
/// <summary>
3743
/// Creates the default <see cref="MemoryAllocator"/> using the provided options.
3844
/// </summary>
3945
/// <param name="options">The <see cref="MemoryAllocatorOptions"/>.</param>
4046
/// <returns>The <see cref="MemoryAllocator"/>.</returns>
41-
public static MemoryAllocator Create(MemoryAllocatorOptions options) =>
42-
new UniformUnmanagedMemoryPoolMemoryAllocator(options.MaximumPoolSizeMegabytes);
47+
public static MemoryAllocator Create(MemoryAllocatorOptions options)
48+
{
49+
UniformUnmanagedMemoryPoolMemoryAllocator allocator = new(options.MaximumPoolSizeMegabytes);
50+
if (options.AllocationLimitMegabytes.HasValue)
51+
{
52+
allocator.MemoryGroupAllocationLimitBytes = options.AllocationLimitMegabytes.Value * 1024 * 1024;
53+
allocator.SingleBufferAllocationLimitBytes = (int)Math.Min(allocator.SingleBufferAllocationLimitBytes, allocator.MemoryGroupAllocationLimitBytes);
54+
}
55+
56+
return allocator;
57+
}
4358

4459
/// <summary>
4560
/// Allocates an <see cref="IMemoryOwner{T}" />, holding a <see cref="Memory{T}"/> of length <paramref name="length"/>.
@@ -64,15 +79,34 @@ public virtual void ReleaseRetainedResources()
6479
/// <summary>
6580
/// Allocates a <see cref="MemoryGroup{T}"/>.
6681
/// </summary>
82+
/// <typeparam name="T">The type of element to allocate.</typeparam>
6783
/// <param name="totalLength">The total length of the buffer.</param>
6884
/// <param name="bufferAlignment">The expected alignment (eg. to make sure image rows fit into single buffers).</param>
6985
/// <param name="options">The <see cref="AllocationOptions"/>.</param>
7086
/// <returns>A new <see cref="MemoryGroup{T}"/>.</returns>
7187
/// <exception cref="InvalidMemoryOperationException">Thrown when 'blockAlignment' converted to bytes is greater than the buffer capacity of the allocator.</exception>
72-
internal virtual MemoryGroup<T> AllocateGroup<T>(
88+
internal MemoryGroup<T> AllocateGroup<T>(
7389
long totalLength,
7490
int bufferAlignment,
7591
AllocationOptions options = AllocationOptions.None)
7692
where T : struct
77-
=> MemoryGroup<T>.Allocate(this, totalLength, bufferAlignment, options);
93+
{
94+
if (totalLength < 0)
95+
{
96+
InvalidMemoryOperationException.ThrowNegativeAllocationException(totalLength);
97+
}
98+
99+
ulong totalLengthInBytes = (ulong)totalLength * (ulong)Unsafe.SizeOf<T>();
100+
if (totalLengthInBytes > (ulong)this.MemoryGroupAllocationLimitBytes)
101+
{
102+
InvalidMemoryOperationException.ThrowAllocationOverLimitException(totalLengthInBytes, this.MemoryGroupAllocationLimitBytes);
103+
}
104+
105+
// Cast to long is safe because we already checked that the total length is within the limit.
106+
return this.AllocateGroupCore<T>(totalLength, (long)totalLengthInBytes, bufferAlignment, options);
107+
}
108+
109+
internal virtual MemoryGroup<T> AllocateGroupCore<T>(long totalLengthInElements, long totalLengthInBytes, int bufferAlignment, AllocationOptions options)
110+
where T : struct
111+
=> MemoryGroup<T>.Allocate(this, totalLengthInElements, bufferAlignment, options);
78112
}

src/ImageSharp/Memory/Allocators/MemoryAllocatorOptions.cs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) Six Labors.
1+
// Copyright (c) Six Labors.
22
// Licensed under the Six Labors Split License.
33

44
namespace SixLabors.ImageSharp.Memory;
@@ -9,6 +9,7 @@ namespace SixLabors.ImageSharp.Memory;
99
public struct MemoryAllocatorOptions
1010
{
1111
private int? maximumPoolSizeMegabytes;
12+
private int? allocationLimitMegabytes;
1213

1314
/// <summary>
1415
/// Gets or sets a value defining the maximum size of the <see cref="MemoryAllocator"/>'s internal memory pool
@@ -27,4 +28,22 @@ public int? MaximumPoolSizeMegabytes
2728
this.maximumPoolSizeMegabytes = value;
2829
}
2930
}
31+
32+
/// <summary>
33+
/// Gets or sets a value defining the maximum (discontiguous) buffer size that can be allocated by the allocator in Megabytes.
34+
/// <see langword="null"/> means platform default: 1GB on 32-bit processes, 4GB on 64-bit processes.
35+
/// </summary>
36+
public int? AllocationLimitMegabytes
37+
{
38+
get => this.allocationLimitMegabytes;
39+
set
40+
{
41+
if (value.HasValue)
42+
{
43+
Guard.MustBeGreaterThan(value.Value, 0, nameof(this.AllocationLimitMegabytes));
44+
}
45+
46+
this.allocationLimitMegabytes = value;
47+
}
48+
}
3049
}

src/ImageSharp/Memory/Allocators/SimpleGcMemoryAllocator.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
// Copyright (c) Six Labors.
1+
// Copyright (c) Six Labors.
22
// Licensed under the Six Labors Split License.
33

44
using System.Buffers;
5+
using System.Runtime.CompilerServices;
56
using SixLabors.ImageSharp.Memory.Internals;
67

78
namespace SixLabors.ImageSharp.Memory;
@@ -17,7 +18,16 @@ public sealed class SimpleGcMemoryAllocator : MemoryAllocator
1718
/// <inheritdoc />
1819
public override IMemoryOwner<T> Allocate<T>(int length, AllocationOptions options = AllocationOptions.None)
1920
{
20-
Guard.MustBeGreaterThanOrEqualTo(length, 0, nameof(length));
21+
if (length < 0)
22+
{
23+
InvalidMemoryOperationException.ThrowNegativeAllocationException(length);
24+
}
25+
26+
ulong lengthInBytes = (ulong)length * (ulong)Unsafe.SizeOf<T>();
27+
if (lengthInBytes > (ulong)this.SingleBufferAllocationLimitBytes)
28+
{
29+
InvalidMemoryOperationException.ThrowAllocationOverLimitException(lengthInBytes, this.SingleBufferAllocationLimitBytes);
30+
}
2131

2232
return new BasicArrayBuffer<T>(new T[length]);
2333
}

src/ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,18 @@ public override IMemoryOwner<T> Allocate<T>(
8383
int length,
8484
AllocationOptions options = AllocationOptions.None)
8585
{
86-
Guard.MustBeGreaterThanOrEqualTo(length, 0, nameof(length));
87-
int lengthInBytes = length * Unsafe.SizeOf<T>();
86+
if (length < 0)
87+
{
88+
InvalidMemoryOperationException.ThrowNegativeAllocationException(length);
89+
}
90+
91+
ulong lengthInBytes = (ulong)length * (ulong)Unsafe.SizeOf<T>();
92+
if (lengthInBytes > (ulong)this.SingleBufferAllocationLimitBytes)
93+
{
94+
InvalidMemoryOperationException.ThrowAllocationOverLimitException(lengthInBytes, this.SingleBufferAllocationLimitBytes);
95+
}
8896

89-
if (lengthInBytes <= this.sharedArrayPoolThresholdInBytes)
97+
if (lengthInBytes <= (ulong)this.sharedArrayPoolThresholdInBytes)
9098
{
9199
var buffer = new SharedArrayPoolBuffer<T>(length);
92100
if (options.Has(AllocationOptions.Clean))
@@ -97,7 +105,7 @@ public override IMemoryOwner<T> Allocate<T>(
97105
return buffer;
98106
}
99107

100-
if (lengthInBytes <= this.poolBufferSizeInBytes)
108+
if (lengthInBytes <= (ulong)this.poolBufferSizeInBytes)
101109
{
102110
UnmanagedMemoryHandle mem = this.pool.Rent();
103111
if (mem.IsValid)
@@ -111,20 +119,15 @@ public override IMemoryOwner<T> Allocate<T>(
111119
}
112120

113121
/// <inheritdoc />
114-
internal override MemoryGroup<T> AllocateGroup<T>(
115-
long totalLength,
122+
internal override MemoryGroup<T> AllocateGroupCore<T>(
123+
long totalLengthInElements,
124+
long totalLengthInBytes,
116125
int bufferAlignment,
117126
AllocationOptions options = AllocationOptions.None)
118127
{
119-
long totalLengthInBytes = totalLength * Unsafe.SizeOf<T>();
120-
if (totalLengthInBytes < 0)
121-
{
122-
throw new InvalidMemoryOperationException("Attempted to allocate a MemoryGroup of a size that is not representable.");
123-
}
124-
125128
if (totalLengthInBytes <= this.sharedArrayPoolThresholdInBytes)
126129
{
127-
var buffer = new SharedArrayPoolBuffer<T>((int)totalLength);
130+
var buffer = new SharedArrayPoolBuffer<T>((int)totalLengthInElements);
128131
return MemoryGroup<T>.CreateContiguous(buffer, options.Has(AllocationOptions.Clean));
129132
}
130133

@@ -134,18 +137,18 @@ internal override MemoryGroup<T> AllocateGroup<T>(
134137
UnmanagedMemoryHandle mem = this.pool.Rent();
135138
if (mem.IsValid)
136139
{
137-
UnmanagedBuffer<T> buffer = this.pool.CreateGuardedBuffer<T>(mem, (int)totalLength, options.Has(AllocationOptions.Clean));
140+
UnmanagedBuffer<T> buffer = this.pool.CreateGuardedBuffer<T>(mem, (int)totalLengthInElements, options.Has(AllocationOptions.Clean));
138141
return MemoryGroup<T>.CreateContiguous(buffer, options.Has(AllocationOptions.Clean));
139142
}
140143
}
141144

142145
// Attempt to rent the whole group from the pool, allocate a group of unmanaged buffers if the attempt fails:
143-
if (MemoryGroup<T>.TryAllocate(this.pool, totalLength, bufferAlignment, options, out MemoryGroup<T>? poolGroup))
146+
if (MemoryGroup<T>.TryAllocate(this.pool, totalLengthInElements, bufferAlignment, options, out MemoryGroup<T>? poolGroup))
144147
{
145148
return poolGroup;
146149
}
147150

148-
return MemoryGroup<T>.Allocate(this.nonPoolAllocator, totalLength, bufferAlignment, options);
151+
return MemoryGroup<T>.Allocate(this.nonPoolAllocator, totalLengthInElements, bufferAlignment, options);
149152
}
150153

151154
public override void ReleaseRetainedResources() => this.pool.Release();

src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.cs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -83,15 +83,16 @@ public static MemoryGroup<T> Allocate(
8383
{
8484
int bufferCapacityInBytes = allocator.GetBufferCapacityInBytes();
8585
Guard.NotNull(allocator, nameof(allocator));
86-
Guard.MustBeGreaterThanOrEqualTo(totalLengthInElements, 0, nameof(totalLengthInElements));
87-
Guard.MustBeGreaterThanOrEqualTo(bufferAlignmentInElements, 0, nameof(bufferAlignmentInElements));
8886

89-
int blockCapacityInElements = bufferCapacityInBytes / ElementSize;
87+
if (totalLengthInElements < 0)
88+
{
89+
InvalidMemoryOperationException.ThrowNegativeAllocationException(totalLengthInElements);
90+
}
9091

91-
if (bufferAlignmentInElements > blockCapacityInElements)
92+
int blockCapacityInElements = bufferCapacityInBytes / ElementSize;
93+
if (bufferAlignmentInElements < 0 || bufferAlignmentInElements > blockCapacityInElements)
9294
{
93-
throw new InvalidMemoryOperationException(
94-
$"The buffer capacity of the provided MemoryAllocator is insufficient for the requested buffer alignment: {bufferAlignmentInElements}.");
95+
InvalidMemoryOperationException.ThrowInvalidAlignmentException(bufferAlignmentInElements);
9596
}
9697

9798
if (totalLengthInElements == 0)

src/ImageSharp/Memory/InvalidMemoryOperationException.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Copyright (c) Six Labors.
22
// Licensed under the Six Labors Split License.
33

4+
using System.Diagnostics.CodeAnalysis;
5+
46
namespace SixLabors.ImageSharp.Memory;
57

68
/// <summary>
@@ -24,4 +26,17 @@ public InvalidMemoryOperationException(string message)
2426
public InvalidMemoryOperationException()
2527
{
2628
}
29+
30+
[DoesNotReturn]
31+
internal static void ThrowNegativeAllocationException(long length) =>
32+
throw new InvalidMemoryOperationException($"Attempted to allocate a buffer of negative length={length}.");
33+
34+
[DoesNotReturn]
35+
internal static void ThrowInvalidAlignmentException(long alignment) =>
36+
throw new InvalidMemoryOperationException(
37+
$"The buffer capacity of the provided MemoryAllocator is insufficient for the requested buffer alignment: {alignment}.");
38+
39+
[DoesNotReturn]
40+
internal static void ThrowAllocationOverLimitException(ulong length, long limit) =>
41+
throw new InvalidMemoryOperationException($"Attempted to allocate a buffer of length={length} that exceeded the limit {limit}.");
2742
}

tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,4 +558,16 @@ public void BmpDecoder_CanDecode_Os2BitmapArray<TPixel>(TestImageProvider<TPixel
558558
// Compare to reference output instead.
559559
image.CompareToReferenceOutput(provider, extension: "png");
560560
}
561+
562+
[Theory]
563+
[WithFile(Issue2696, PixelTypes.Rgba32)]
564+
public void BmpDecoder_ThrowsException_Issue2696<TPixel>(TestImageProvider<TPixel> provider)
565+
where TPixel : unmanaged, IPixel<TPixel>
566+
{
567+
InvalidImageContentException ex = Assert.Throws<InvalidImageContentException>(() =>
568+
{
569+
using Image<TPixel> image = provider.GetImage(BmpDecoder.Instance);
570+
});
571+
Assert.IsType<InvalidMemoryOperationException>(ex.InnerException);
572+
}
561573
}

tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -338,21 +338,11 @@ public void Issue2564_DecodeWorks<TPixel>(TestImageProvider<TPixel> provider)
338338
}
339339

340340
[Theory]
341-
[WithFile(TestImages.Jpeg.Issues.HangBadScan, PixelTypes.L8)]
342-
public void DecodeHang<TPixel>(TestImageProvider<TPixel> provider)
341+
[WithFile(TestImages.Jpeg.Issues.HangBadScan, PixelTypes.Rgb24)]
342+
public void DecodeHang_ThrowsException<TPixel>(TestImageProvider<TPixel> provider)
343343
where TPixel : unmanaged, IPixel<TPixel>
344-
{
345-
if (TestEnvironment.IsWindows &&
346-
TestEnvironment.RunsOnCI)
347-
{
348-
// Windows CI runs consistently fail with OOM.
349-
return;
350-
}
351-
352-
using Image<TPixel> image = provider.GetImage(JpegDecoder.Instance);
353-
Assert.Equal(65503, image.Width);
354-
Assert.Equal(65503, image.Height);
355-
}
344+
=> Assert.Throws<InvalidImageContentException>(
345+
() => { using Image<TPixel> image = provider.GetImage(JpegDecoder.Instance); });
356346

357347
// https://github.com/SixLabors/ImageSharp/issues/2517
358348
[Theory]

0 commit comments

Comments
 (0)