-
-
Notifications
You must be signed in to change notification settings - Fork 851
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Limit all memory allocations in the MemoryAllocator layer #2706
Conversation
4L * OneGigabyte : | ||
OneGigabyte; | ||
|
||
internal int SingleBufferAllocationLimitBytes { get; private set; } = OneGigabyte; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, there is no good reason to set this to 1GB. Contiguous buffers have a natural limit of int.MaxValue ~ 2GB
so I think we should set the contiguous limit to min(int.MaxValue, MemoryAllocatorOptions.AllocationLimitMegabytes * 1MB)
. That would get rid of all obscure logic and make the semantics of MemoryAllocatorOptions.AllocationLimitMegabytes
very obvious.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah. I like that. The only risk we have with this stuff is in our decoders which can be easily mitigated with some care (PNG tweaks)
/// Gets or sets a value defining the maximum (discontiguous) buffer size that can be allocated by the allocator in Megabytes. | ||
/// <see langword="null"/> means platform default: 1GB on 32-bit processes, 4GB on 64-bit processes. | ||
/// </summary> | ||
public int? AllocationLimitMegabytes |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You got it in the options! Nice!
public static MemoryAllocator Create(MemoryAllocatorOptions options) | ||
{ | ||
UniformUnmanagedMemoryPoolMemoryAllocator allocator = new(options.MaximumPoolSizeMegabytes); | ||
if (options.AllocationLimitMegabytes.HasValue) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For V4 I think we should introduce UniformUnmanagedMemoryPoolOptions
which has the standard options as a property.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, AllocatorOptions
is specific to UniformUnmanagedMemoryAllocator
(which is intantiated with MemoryAllocator.Create()
, the type itself is not public). I don't think anyone ever would need another allocator IRL. The design keeps allocators pluggable just in case + SimpleGcMemoryAllocator
can be useful for diagnostic purposes since managed allocations are easy to track with standard .NET tools. But SimpleGcMemoryAllocator
should not be used in production.
To secure users who still use it (mostly legacy code designed to workaround 1.* issues), I applied the default limits, but you cannot configure SimpleGcMemoryAllocator
with MemoryAllocatorOptions
.
@@ -24,4 +26,8 @@ public InvalidMemoryOperationException(string message) | |||
public InvalidMemoryOperationException() | |||
{ | |||
} | |||
|
|||
[DoesNotReturn] | |||
internal static void ThrowAllocationOverLimitException(long length, long limit) => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this sitting here.
@@ -19,6 +20,13 @@ public override IMemoryOwner<T> Allocate<T>(int length, AllocationOptions option | |||
{ | |||
Guard.MustBeGreaterThanOrEqualTo(length, 0, nameof(length)); | |||
|
|||
int lengthInBytes = length * Unsafe.SizeOf<T>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For V4 I'd like to have this in MemoryAllocator
so we don't have to repeat the check.
long totalLength, | ||
int bufferAlignment, | ||
AllocationOptions options = AllocationOptions.None) | ||
where T : struct | ||
=> MemoryGroup<T>.Allocate(this, totalLength, bufferAlignment, options); | ||
{ | ||
long totalLengthInBytes = totalLength * Unsafe.SizeOf<T>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it might be possible to overflow this. I think one of our tests that allocates a 256 byte struct does so.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's much neater than my version. I say go with this. 👍
/// Gets or sets a value defining the maximum (discontiguous) buffer size that can be allocated by the allocator in Megabytes. | ||
/// <see langword="null"/> means platform default: 1GB on 32-bit processes, 4GB on 64-bit processes. | ||
/// </summary> | ||
public int? AllocationLimitMegabytes |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this consistent with how we talk about memory across the public api? do we talk about megabytes anywhere else in the public api or is it all bytes?
we want to make sure we are being consistent on our units of measure in the public api surface.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The other property on MemoryAllocatiorOptions
is MaximumPoolSizeMegabytes
. When I designed the MemoryAllocator configuration API it seemed like Megabytes are easier to reason about when talking about limits.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm happy with this @antonfirsov and have created a backport PR for V2.
I'll wait for your final read-through to merge though.
{ | ||
using Image<TPixel> image = provider.GetImage(BmpDecoder.Instance); | ||
}); | ||
Assert.IsType<InvalidMemoryOperationException>(ex.InnerException); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix an overlook from #2706. See 92b8277#r141770676.
Fix an overlook from #2706. See 92b8277#r141770676.
Fix an overlook from #2706. See 92b8277#r141770676. # Conflicts: # src/ImageSharp/Memory/Allocators/MemoryAllocator.cs
Prerequisites
Description
Fixes #2696.
This is an alternative take for #2704. Differences:
MemoryAllocatorOptions
(together with the pool size).32k x 32k
ofRgba32
which is more conservative than the limit in Limit all memory allocations to configurable values. #2704. For me this feels somewhat safer while I don't see many downsides. Note that this is the absolute hardcoded limit in WIC, while newer skia builds have a hardcoded 2GB limit (this hasn't been merged to in SkiaSharp 2.* yet)width * height
sizes so a 64MB limit would be easy to exceed with sane images.MaxAllocationAttempts
so we don't block too long if an OOM is still being hit.The PR is missing tests and PNG tweak from #2704. Will finalize it after agreeing on the direction.