Skip to content

Commit f21d641

Browse files
authored
Merge pull request #2715 from SixLabors/backport/v2-memlimit
V2 - Limit all memory allocations in the MemoryAllocator layer
2 parents 8f0b4d3 + cf9496d commit f21d641

15 files changed

+200
-43
lines changed

src/ImageSharp/Formats/Png/PngDecoderCore.cs

+3
Original file line numberDiff line numberDiff line change
@@ -1504,6 +1504,9 @@ private void SkipChunkDataAndCrc(in PngChunk chunk)
15041504
private IMemoryOwner<byte> ReadChunkData(int length)
15051505
{
15061506
// We rent the buffer here to return it afterwards in Decode()
1507+
// We don't want to throw a degenerated memory exception here as we want to allow partial decoding
1508+
// so limit the length.
1509+
length = (int)Math.Min(length, this.currentStream.Length - this.currentStream.Position);
15071510
IMemoryOwner<byte> buffer = this.Configuration.MemoryAllocator.Allocate<byte>(length, AllocationOptions.Clean);
15081511

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

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ namespace SixLabors.ImageSharp.Memory.Internals
1414
internal struct UnmanagedMemoryHandle : IEquatable<UnmanagedMemoryHandle>
1515
{
1616
// Number of allocation re-attempts when detecting OutOfMemoryException.
17-
private const int MaxAllocationAttempts = 1000;
17+
private const int MaxAllocationAttempts = 10;
1818

1919
// Track allocations for testing purposes:
2020
private static int totalOutstandingHandles;

src/ImageSharp/Memory/Allocators/MemoryAllocator.cs

+41-6
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
using System;
55
using System.Buffers;
6+
using System.Collections.Generic;
7+
using System.Runtime.CompilerServices;
68

79
namespace SixLabors.ImageSharp.Memory
810
{
@@ -11,6 +13,8 @@ namespace SixLabors.ImageSharp.Memory
1113
/// </summary>
1214
public abstract class MemoryAllocator
1315
{
16+
private const int OneGigabyte = 1 << 30;
17+
1418
/// <summary>
1519
/// Gets the default platform-specific global <see cref="MemoryAllocator"/> instance that
1620
/// serves as the default value for <see cref="Configuration.MemoryAllocator"/>.
@@ -21,6 +25,10 @@ public abstract class MemoryAllocator
2125
/// </summary>
2226
public static MemoryAllocator Default { get; } = Create();
2327

28+
internal long MemoryGroupAllocationLimitBytes { get; private set; } = Environment.Is64BitProcess ? 4L * OneGigabyte : OneGigabyte;
29+
30+
internal int SingleBufferAllocationLimitBytes { get; private set; } = OneGigabyte;
31+
2432
/// <summary>
2533
/// Gets the length of the largest contiguous buffer that can be handled by this allocator instance in bytes.
2634
/// </summary>
@@ -31,16 +39,24 @@ public abstract class MemoryAllocator
3139
/// Creates a default instance of a <see cref="MemoryAllocator"/> optimized for the executing platform.
3240
/// </summary>
3341
/// <returns>The <see cref="MemoryAllocator"/>.</returns>
34-
public static MemoryAllocator Create() =>
35-
new UniformUnmanagedMemoryPoolMemoryAllocator(null);
42+
public static MemoryAllocator Create() => Create(default);
3643

3744
/// <summary>
3845
/// Creates the default <see cref="MemoryAllocator"/> using the provided options.
3946
/// </summary>
4047
/// <param name="options">The <see cref="MemoryAllocatorOptions"/>.</param>
4148
/// <returns>The <see cref="MemoryAllocator"/>.</returns>
42-
public static MemoryAllocator Create(MemoryAllocatorOptions options) =>
43-
new UniformUnmanagedMemoryPoolMemoryAllocator(options.MaximumPoolSizeMegabytes);
49+
public static MemoryAllocator Create(MemoryAllocatorOptions options)
50+
{
51+
UniformUnmanagedMemoryPoolMemoryAllocator allocator = new(options.MaximumPoolSizeMegabytes);
52+
if (options.AllocationLimitMegabytes.HasValue)
53+
{
54+
allocator.MemoryGroupAllocationLimitBytes = options.AllocationLimitMegabytes.Value * 1024 * 1024;
55+
allocator.SingleBufferAllocationLimitBytes = (int)Math.Min(allocator.SingleBufferAllocationLimitBytes, allocator.MemoryGroupAllocationLimitBytes);
56+
}
57+
58+
return allocator;
59+
}
4460

4561
/// <summary>
4662
/// Allocates an <see cref="IMemoryOwner{T}" />, holding a <see cref="Memory{T}"/> of length <paramref name="length"/>.
@@ -65,16 +81,35 @@ public virtual void ReleaseRetainedResources()
6581
/// <summary>
6682
/// Allocates a <see cref="MemoryGroup{T}"/>.
6783
/// </summary>
84+
/// <typeparam name="T">The type of element to allocate.</typeparam>
6885
/// <param name="totalLength">The total length of the buffer.</param>
6986
/// <param name="bufferAlignment">The expected alignment (eg. to make sure image rows fit into single buffers).</param>
7087
/// <param name="options">The <see cref="AllocationOptions"/>.</param>
7188
/// <returns>A new <see cref="MemoryGroup{T}"/>.</returns>
7289
/// <exception cref="InvalidMemoryOperationException">Thrown when 'blockAlignment' converted to bytes is greater than the buffer capacity of the allocator.</exception>
73-
internal virtual MemoryGroup<T> AllocateGroup<T>(
90+
internal MemoryGroup<T> AllocateGroup<T>(
7491
long totalLength,
7592
int bufferAlignment,
7693
AllocationOptions options = AllocationOptions.None)
7794
where T : struct
78-
=> MemoryGroup<T>.Allocate(this, totalLength, bufferAlignment, options);
95+
{
96+
if (totalLength < 0)
97+
{
98+
throw new InvalidMemoryOperationException($"Attempted to allocate a buffer of negative length={totalLength}.");
99+
}
100+
101+
ulong totalLengthInBytes = (ulong)totalLength * (ulong)Unsafe.SizeOf<T>();
102+
if (totalLengthInBytes > (ulong)this.MemoryGroupAllocationLimitBytes)
103+
{
104+
throw new InvalidMemoryOperationException($"Attempted to allocate a buffer of length={totalLengthInBytes} that exceeded the limit {this.MemoryGroupAllocationLimitBytes}.");
105+
}
106+
107+
// Cast to long is safe because we already checked that the total length is within the limit.
108+
return this.AllocateGroupCore<T>(totalLength, (long)totalLengthInBytes, bufferAlignment, options);
109+
}
110+
111+
internal virtual MemoryGroup<T> AllocateGroupCore<T>(long totalLengthInElements, long totalLengthInBytes, int bufferAlignment, AllocationOptions options)
112+
where T : struct
113+
=> MemoryGroup<T>.Allocate(this, totalLengthInElements, bufferAlignment, options);
79114
}
80115
}

src/ImageSharp/Memory/Allocators/MemoryAllocatorOptions.cs

+20-1
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 Apache License, Version 2.0.
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,5 +28,23 @@ 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+
readonly 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
}
3150
}

src/ImageSharp/Memory/Allocators/SimpleGcMemoryAllocator.cs

+12-2
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 Apache License, Version 2.0.
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+
throw new InvalidMemoryOperationException($"Attempted to allocate a buffer of negative length={length}.");
24+
}
25+
26+
ulong lengthInBytes = (ulong)length * (ulong)Unsafe.SizeOf<T>();
27+
if (lengthInBytes > (ulong)this.SingleBufferAllocationLimitBytes)
28+
{
29+
throw new InvalidMemoryOperationException($"Attempted to allocate a buffer of length={lengthInBytes} that exceeded the limit {this.SingleBufferAllocationLimitBytes}.");
30+
}
2131

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

src/ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs

+19-16
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,18 @@ public override IMemoryOwner<T> Allocate<T>(
8787
int length,
8888
AllocationOptions options = AllocationOptions.None)
8989
{
90-
Guard.MustBeGreaterThanOrEqualTo(length, 0, nameof(length));
91-
int lengthInBytes = length * Unsafe.SizeOf<T>();
90+
if (length < 0)
91+
{
92+
throw new InvalidMemoryOperationException($"Attempted to allocate a buffer of negative length={length}.");
93+
}
94+
95+
ulong lengthInBytes = (ulong)length * (ulong)Unsafe.SizeOf<T>();
96+
if (lengthInBytes > (ulong)this.SingleBufferAllocationLimitBytes)
97+
{
98+
throw new InvalidMemoryOperationException($"Attempted to allocate a buffer of length={lengthInBytes} that exceeded the limit {this.SingleBufferAllocationLimitBytes}.");
99+
}
92100

93-
if (lengthInBytes <= this.sharedArrayPoolThresholdInBytes)
101+
if (lengthInBytes <= (ulong)this.sharedArrayPoolThresholdInBytes)
94102
{
95103
var buffer = new SharedArrayPoolBuffer<T>(length);
96104
if (options.Has(AllocationOptions.Clean))
@@ -101,7 +109,7 @@ public override IMemoryOwner<T> Allocate<T>(
101109
return buffer;
102110
}
103111

104-
if (lengthInBytes <= this.poolBufferSizeInBytes)
112+
if (lengthInBytes <= (ulong)this.poolBufferSizeInBytes)
105113
{
106114
UnmanagedMemoryHandle mem = this.pool.Rent();
107115
if (mem.IsValid)
@@ -115,20 +123,15 @@ public override IMemoryOwner<T> Allocate<T>(
115123
}
116124

117125
/// <inheritdoc />
118-
internal override MemoryGroup<T> AllocateGroup<T>(
119-
long totalLength,
126+
internal override MemoryGroup<T> AllocateGroupCore<T>(
127+
long totalLengthInElements,
128+
long totalLengthInBytes,
120129
int bufferAlignment,
121130
AllocationOptions options = AllocationOptions.None)
122131
{
123-
long totalLengthInBytes = totalLength * Unsafe.SizeOf<T>();
124-
if (totalLengthInBytes < 0)
125-
{
126-
throw new InvalidMemoryOperationException("Attempted to allocate a MemoryGroup of a size that is not representable.");
127-
}
128-
129132
if (totalLengthInBytes <= this.sharedArrayPoolThresholdInBytes)
130133
{
131-
var buffer = new SharedArrayPoolBuffer<T>((int)totalLength);
134+
var buffer = new SharedArrayPoolBuffer<T>((int)totalLengthInElements);
132135
return MemoryGroup<T>.CreateContiguous(buffer, options.Has(AllocationOptions.Clean));
133136
}
134137

@@ -138,18 +141,18 @@ internal override MemoryGroup<T> AllocateGroup<T>(
138141
UnmanagedMemoryHandle mem = this.pool.Rent();
139142
if (mem.IsValid)
140143
{
141-
UnmanagedBuffer<T> buffer = this.pool.CreateGuardedBuffer<T>(mem, (int)totalLength, options.Has(AllocationOptions.Clean));
144+
UnmanagedBuffer<T> buffer = this.pool.CreateGuardedBuffer<T>(mem, (int)totalLengthInElements, options.Has(AllocationOptions.Clean));
142145
return MemoryGroup<T>.CreateContiguous(buffer, options.Has(AllocationOptions.Clean));
143146
}
144147
}
145148

146149
// Attempt to rent the whole group from the pool, allocate a group of unmanaged buffers if the attempt fails:
147-
if (MemoryGroup<T>.TryAllocate(this.pool, totalLength, bufferAlignment, options, out MemoryGroup<T> poolGroup))
150+
if (MemoryGroup<T>.TryAllocate(this.pool, totalLengthInElements, bufferAlignment, options, out MemoryGroup<T>? poolGroup))
148151
{
149152
return poolGroup;
150153
}
151154

152-
return MemoryGroup<T>.Allocate(this.nonPoolAllocator, totalLength, bufferAlignment, options);
155+
return MemoryGroup<T>.Allocate(this.nonPoolAllocator, totalLengthInElements, bufferAlignment, options);
153156
}
154157

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

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

+8-6
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ namespace SixLabors.ImageSharp.Memory
2020
/// <typeparam name="T">The element type.</typeparam>
2121
internal abstract partial class MemoryGroup<T> : IMemoryGroup<T>, IDisposable
2222
where T : struct
23-
{
23+
{
2424
private static readonly int ElementSize = Unsafe.SizeOf<T>();
2525

2626
private MemoryGroupSpanCache memoryGroupSpanCache;
@@ -43,7 +43,7 @@ private MemoryGroup(int bufferLength, long totalLength)
4343
/// <inheritdoc />
4444
public bool IsValid { get; private set; } = true;
4545

46-
public MemoryGroupView<T> View { get; private set; }
46+
public MemoryGroupView<T> View { get; private set; } = null!;
4747

4848
/// <inheritdoc />
4949
public abstract Memory<T> this[int index] { get; }
@@ -85,12 +85,14 @@ public static MemoryGroup<T> Allocate(
8585
{
8686
int bufferCapacityInBytes = allocator.GetBufferCapacityInBytes();
8787
Guard.NotNull(allocator, nameof(allocator));
88-
Guard.MustBeGreaterThanOrEqualTo(totalLengthInElements, 0, nameof(totalLengthInElements));
89-
Guard.MustBeGreaterThanOrEqualTo(bufferAlignmentInElements, 0, nameof(bufferAlignmentInElements));
9088

91-
int blockCapacityInElements = bufferCapacityInBytes / ElementSize;
89+
if (totalLengthInElements < 0)
90+
{
91+
throw new InvalidMemoryOperationException($"Attempted to allocate a buffer of negative length={totalLengthInElements}.");
92+
}
9293

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

src/ImageSharp/Memory/InvalidMemoryOperationException.cs

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the Apache License, Version 2.0.
33

44
using System;
5+
using System.Diagnostics.CodeAnalysis;
56

67
namespace SixLabors.ImageSharp.Memory
78
{

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

+13
Original file line numberDiff line numberDiff line change
@@ -619,5 +619,18 @@ public void BmpDecoder_CanDecode_Os2BitmapArray<TPixel>(TestImageProvider<TPixel
619619
// image.CompareToOriginal(provider);
620620
}
621621
}
622+
623+
[Theory]
624+
[WithFile(Issue2696, PixelTypes.Rgba32)]
625+
public void BmpDecoder_ThrowsException_Issue2696<TPixel>(TestImageProvider<TPixel> provider)
626+
where TPixel : unmanaged, IPixel<TPixel>
627+
{
628+
// On V2 this is throwing InvalidOperationException,
629+
// because of the validation logic in BmpInfoHeader.VerifyDimensions().
630+
Assert.Throws<InvalidOperationException>(() =>
631+
{
632+
using Image<TPixel> image = provider.GetImage(BmpDecoder);
633+
});
634+
}
622635
}
623636
}

tests/ImageSharp.Tests/Memory/Allocators/SimpleGcMemoryAllocatorTests.cs

+10-6
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,17 @@ public BufferTests()
2121

2222
protected SimpleGcMemoryAllocator MemoryAllocator { get; } = new SimpleGcMemoryAllocator();
2323

24-
[Theory]
25-
[InlineData(-1)]
26-
public void Allocate_IncorrectAmount_ThrowsCorrect_ArgumentOutOfRangeException(int length)
24+
public static TheoryData<int> InvalidLengths { get; set; } = new()
2725
{
28-
ArgumentOutOfRangeException ex = Assert.Throws<ArgumentOutOfRangeException>(() => this.MemoryAllocator.Allocate<BigStruct>(length));
29-
Assert.Equal("length", ex.ParamName);
30-
}
26+
{ -1 },
27+
{ (1 << 30) + 1 }
28+
};
29+
30+
[Theory]
31+
[MemberData(nameof(InvalidLengths))]
32+
public void Allocate_IncorrectAmount_ThrowsCorrect_InvalidMemoryOperationException(int length)
33+
=> Assert.Throws<InvalidMemoryOperationException>(
34+
() => this.MemoryAllocator.Allocate<BigStruct>(length));
3135

3236
[Fact]
3337
public unsafe void Allocate_MemoryIsPinnableMultipleTimes()

tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedPoolMemoryAllocatorTests.cs

+35
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,17 @@ public void AllocateGroup_SizeInBytesOverLongMaxValue_ThrowsInvalidMemoryOperati
118118
Assert.Throws<InvalidMemoryOperationException>(() => allocator.AllocateGroup<S4>(int.MaxValue * (long)int.MaxValue, int.MaxValue));
119119
}
120120

121+
public static TheoryData<int> InvalidLengths { get; set; } = new()
122+
{
123+
{ -1 },
124+
{ (1 << 30) + 1 }
125+
};
126+
127+
[Theory]
128+
[MemberData(nameof(InvalidLengths))]
129+
public void Allocate_IncorrectAmount_ThrowsCorrect_InvalidMemoryOperationException(int length)
130+
=> Assert.Throws<InvalidMemoryOperationException>(() => new UniformUnmanagedMemoryPoolMemoryAllocator(null).Allocate<S512>(length));
131+
121132
[Fact]
122133
public unsafe void Allocate_MemoryIsPinnableMultipleTimes()
123134
{
@@ -387,6 +398,30 @@ private static void AllocateSingleAndForget(UniformUnmanagedMemoryPoolMemoryAllo
387398
}
388399
}
389400

401+
[Fact]
402+
public void Allocate_OverLimit_ThrowsInvalidMemoryOperationException()
403+
{
404+
MemoryAllocator allocator = MemoryAllocator.Create(new MemoryAllocatorOptions()
405+
{
406+
AllocationLimitMegabytes = 4
407+
});
408+
const int oneMb = 1 << 20;
409+
allocator.Allocate<byte>(4 * oneMb).Dispose(); // Should work
410+
Assert.Throws<InvalidMemoryOperationException>(() => allocator.Allocate<byte>(5 * oneMb));
411+
}
412+
413+
[Fact]
414+
public void AllocateGroup_OverLimit_ThrowsInvalidMemoryOperationException()
415+
{
416+
MemoryAllocator allocator = MemoryAllocator.Create(new MemoryAllocatorOptions()
417+
{
418+
AllocationLimitMegabytes = 4
419+
});
420+
const int oneMb = 1 << 20;
421+
allocator.AllocateGroup<byte>(4 * oneMb, 1024).Dispose(); // Should work
422+
Assert.Throws<InvalidMemoryOperationException>(() => allocator.AllocateGroup<byte>(5 * oneMb, 1024));
423+
}
424+
390425
#if NETCOREAPP3_1_OR_GREATER
391426
[Fact]
392427
public void Issue2001_NegativeMemoryReportedByGc()

0 commit comments

Comments
 (0)