From 141bd49c18cb7b018f5f8301d31985efb82fc535 Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Thu, 28 May 2026 00:23:06 -0700 Subject: [PATCH] Document memory safety violations and provide example Added a note on memory safety violations due to data races and provided an example of a NativeBuffer class that illustrates potential issues with concurrent access. --- accepted/2025/memory-safety/caller-unsafe.md | 44 ++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/accepted/2025/memory-safety/caller-unsafe.md b/accepted/2025/memory-safety/caller-unsafe.md index c494963e2..21ff5d7ce 100644 --- a/accepted/2025/memory-safety/caller-unsafe.md +++ b/accepted/2025/memory-safety/caller-unsafe.md @@ -156,6 +156,50 @@ According to the above definition, there are two valid constructors: the explici These data races are considered fundamental to the .NET type system. The safe subset of C# does not protect against them. +Note that memory safety violations that occur _due to data races_ would be considered invalid and any code that can produce this result must be marked unsafe. Consider the following example: + +```C# +public class NativeBuffer : IDisposable +{ + private unsafe byte* _ptr; + public int Length { get; } + + public NativeBuffer(int length) + { + ArgumentOutOfRangeException.ThrowIfNegative(length); + unsafe + { + _ptr = (byte*)NativeMemory.Alloc((nuint)length); + } + Length = length; + } + + public byte ReadAt(int index) + { + ArgumentOutOfRangeException.ThrowIfNegative(index); + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, Length); + unsafe + { + ObjectDisposedException.ThrowIf(_ptr is null, this); + // SAFETY: bounds checked above; null check just above; _ptr therefore points to Length bytes + return _ptr[index]; + } + } + + public void Dispose() + { + unsafe + { + // SAFETY: _ptr is null or was returned by NativeMemory.Alloc; Free accepts both + NativeMemory.Free(_ptr); + _ptr = null; + } + } +} +``` + +The above code can produce memory safety violations in the presence of data races. Consider two threads, one that invokes `Dispose` and the other that invokes `ReadAt` simultaneously. If the first thread executes right between the `_ptr[index]` derefence, the dereference will be invalid (out of bounds/use after free). Both the `Dispose` method and `ReadAt` method would need to be marked `unsafe` to correctly communicate the requirement that they must not be executed concurrently, or the implementation would have to be rewritten to use synchronization. + - Resource lifetime. Some code patterns, like object pools, require manual lifetime management. When this management is done incorrectly bad behaviors can occur, including improper memory reuse. Notably, none of those behaviors include invalid memory access, although it can include symptoms that look like memory corruption. Because invalid memory access is not possible, this is considered safe. ### Evolution