This article contains fine-grained recommendations for specific unsafe patterns, the risks they entail, and how to mitigate those risks. These guidelines target all developers who are writing or reviewing unsafe code in C#. Other .NET languages such as F# and Visual Basic are outside of the scope of this article, although some recommendations might be applicable to those languages as well.
Glossary
- AVE - Access violation exception.
- Byref - A managed pointer (
ref T t) that's similar to unmanaged pointer but tracked by the GC. Typically points to arbitrary parts of objects or stack. Reference is effectively a managed pointer with +0 offset. - CVE - Publicly disclosed cybersecurity vulnerabilities.
- JIT - Just-in-time compiler (RyuJIT in CoreCLR and NativeAOT).
- PGO - Profile-guided optimization.
- Unmanaged pointer (or raw pointer) - A pointer (
T* p) that points to arbitrary memory location and is not managed or tracked by the GC.
For other terms, see .NET Runtime Glossary.
Common unreliable patterns
C# provides a safe environment where developers don't need to worry about the internal workings of the runtime and the GC. Unsafe code allows you to bypass these safety checks, potentially introducing unreliable patterns that can lead to memory corruption. While such patterns might be useful in certain scenarios, you should use them with caution and only when absolutely necessary. Not only do C# and .NET not provide tools to verify the soundness of unsafe code (as various C/C++ sanitizers might provide), GC-specific behaviors might introduce additional risks in unsafe C# beyond those that traditional C/C++ developers might be familiar with.
Unsafe code around managed references should be written with the following conservative assumptions in mind:
- The GC can interrupt the execution of any method at any point of time at any instruction.
- The GC can move objects in memory and update all tracked references.
- The GC knows precisely when references are no longer needed.
A classic example of heap corruption occurs when the GC loses track of an object reference or treats invalid pointers as heap references. This often results in non-deterministic crashes or memory corruption. Heap corruption bugs are particularly challenging to diagnose and reproduce because:
- These issues can remain hidden for a long time and only manifest after an unrelated code change or runtime update.
- They often require precise timing to reproduce, such as the GC interrupting execution at a specific location and starting heap compaction, which is a rare and nondeterministic event.
The next sections describe common unsafe patterns with ✔️ DO and ❌ DON'T recommendations.
1. Untracked managed pointers (Unsafe.AsPointer and friends)
It's not possible to convert a managed (tracked) pointer to an unmanaged (untracked)
pointer in safe C#. When such need arises, it might be tempting to use System.Runtime.CompilerServices.Unsafe.AsPointer(@)
to avoid the overhead of a fixed statement. While there are valid
use cases for that, it introduces a risk of creating untracked pointers to moveable objects.
Example:
unsafe void UnreliableCode(ref int x)
{
int* nativePointer = (int*)Unsafe.AsPointer(ref x);
nativePointer[0] = 42;
}If the GC interrupts the execution of the UnreliableCode method right after the pointer has been read
(the address referenced by x) and relocates the referenced object, the GC will correctly update the
location stored in x but won't know anything about nativePointer and will not update the value
it contains. At that point, writing to nativePointer is writing to arbitrary memory.
unsafe void UnreliableCode(ref int x)
{
int* nativePointer = (int*)Unsafe.AsPointer(ref x);
// <-- GC happens here between the two lines of code and updates `x` to point to a new location.
// However, `nativePointer` still points to the old location as it's not reported to the GC
nativePointer[0] = 42; // Potentially corrupting write, access violation, or other issue.
}Once GC resumes the execution of the method, it will write 42 into the old location of x, which might lead
to an unexpected exception, general global state corruption, or process termination via an access violation.
The recommended solution is instead to use the fixed keyword and & address-of operator to ensure that the
GC cannot relocate the target reference for the duration of the operation.
unsafe void ReliableCode(ref int x)
{
fixed (int* nativePointer = &x) // `x` cannot be relocated for the duration of this block.
{
nativePointer[0] = 42;
}
}Recommendations
- ❌ DON'T use
ref Xarguments with an implicit contract thatXis always stack-allocated, pinned, or otherwise not relocatable by the GC. Same applies to plain objects and Spans - don't introduce non-obvious caller-based contracts about their lifetimes in methods signatures. Consider instead taking a ref struct argument or changing the argument to be a raw pointer type (X*). - ❌ DON'T use a pointer from System.Runtime.CompilerServices.Unsafe.AsPointer
(@) if it can outlive the original object it is pointing to. Per the API's documentation), it's up to the caller of System.Runtime.CompilerServices.Unsafe.AsPointer(@) to guarantee that the GC cannot relocate the reference. Ensure it's clearly visible to code reviewers that the caller has fulfilled this prerequisite. - ✔️ DO use GCHandle or
fixedscopes instead of System.Runtime.CompilerServices.Unsafe.AsPointer(@) to define explicit scopes for unmanaged pointers and to ensure that the object is always pinned. - ✔️ DO use unmanaged pointers (with
fixed) instead of byrefs when you need to align an array to a specific boundary. This ensures the GC won't relocate the object and invalidate any alignment assumptions your logic might rely upon.
2. Exposing pointers outside of the fixed scope
While the fixed
keyword defines a scope for the pointer obtained from the pinned object, it's still possible for that pointer
to escape the fixed scope and introduce bugs, as C# doesn't provide any ownership/lifecycle protections for it.
A typical example is the following snippet:
unsafe int* GetPointerToArray(int[] array)
{
fixed (int* pArray = array)
{
_ptrField = pArray; // Bug!
Method(pArray); // Bug if `Method` allows `pArray` to escape,
// perhaps by assigning it to a field.
return pArray; // Bug!
// And other ways to escape the scope.
}
}In this example, the array is pinned properly using the fixed keyword (ensuring the GC can't relocate it within the fixed block),
but then the pointer is exposed outside of the fixed block. This creates a dangling pointer whose dereference will result in undefined behavior.
Recommendations
- ✔️ DO make sure that pointers in
fixedblocks do not leave the defined scope. - ✔️ DO prefer safe low-level primitives with built-in escape analysis, such as C#'s ref struct. For more information, see Safe context constraint.
3. Internal implementation details of the runtime and libraries
While accessing or relying on internal implementation details is bad practice in general (and not supported by .NET), it's worth calling out specific commonly observed cases. This is not intended to be an exhaustive list of all possible things that could go wrong when code inappropriately relies on an internal implementation detail.
Recommendations
❌ DON'T alter or read any parts of an object's header.
- Object headers might differ across runtimes.
- In CoreCLR, the object header cannot be accessed safely without pinning the object first.
- Never change object's type by modifying the MethodTable pointer.
❌ DON'T store any data in an object's padding. Don't assume padding contents will be preserved or that padding is always zeroed by default.
❌ DON'T make assumptions about the sizes and offsets of anything other than primitives and structs with sequential or explicit layout. Even then, exceptions exist, such as when GC handles are involved.
❌ DON'T invoke nonpublic methods, access nonpublic fields, or mutate readonly fields in BCL types with reflection or unsafe code.
❌ DON'T assume any given nonpublic member in the BCL will always be present or will have a specific shape. The .NET team does occasionally modify or remove nonpublic APIs in servicing releases.
❌ DON'T change
static readonlyfields using reflection or unsafe code, as they're assumed to be constant. For example, RyuJIT usually inlines them as explicit constants.❌ DON'T simply assume that a reference is non-relocatable. This guidance applies to string and UTF-8 (
"..."u8) literals, static fields, RVA fields, LOH objects, and so on.- These are runtime implementation details that might hold for some runtimes but not for others.
- Unmanaged pointers to such objects might not stop assemblies from being unloaded, causing the pointers to become dangling. Use
fixedscopes to ensure correctness.
ReadOnlySpan<int> rva = [1, 2, 4, 4]; int* p = (int*)Unsafe.AsPointer(ref MemoryMarshal.GetReference(rva)); // Bug! The assembly containing the RVA field might be unloaded at this point // and `p` becomes a dangling pointer. int value = p[0]; // Access violation or other issue.❌ DON'T write code that relies on the implementation details of a specific runtime.
4. Invalid managed pointers (even if they are never dereferenced)
Certain categories of code end up leaning on pointer manipulation and arithmetic, and such code
often has a choice between using unmanaged pointers (T* p) and managed pointers (ref T p).
These pointers can be manipulated arbitrarily, for instance, via operators on unmanaged pointers (p++) and
via Unsafe methods on managed pointers (p = ref Unsafe.Add(ref p, 1)). Both are considered "unsafe code"
and it's possible to create unreliable patterns with both. However, for certain algorithms, it can be easier to
accidentally create GC-unsafe patterns when manipulating managed pointers. Since unmanaged pointers aren't tracked
by the GC, the value they contain is only relevant when dereferenced by the developer's code. In contrast,
a managed pointer's value is relevant not only when it's dereferenced by the developer's code, but also
when it's examined by the GC. Thus, a developer can create invalid unmanaged pointers without consequence
as long as they're not dereferenced, but creating any invalid managed pointer is a bug. Example:
unsafe void UnmanagedPointers(int[] array)
{
fixed (int* p = array)
{
int* invalidPtr = p - 1000;
// invalidPtr is pointing to an undefined location in memory
// it's ok as long as it's not dereferenced.
int* validPtr = invalidPtr + 1000; // Returning back to the original location
*validPtr = 42; // OK
}
}However, similar code using byrefs (managed pointers) is invalid.
void ManagedPointers_Incorrect(int[] array)
{
ref int invalidPtr = ref Unsafe.Add(ref array[0], -1000); // Already a bug!
ref int validPtr = ref Unsafe.Add(ref invalidPtr, 1000);
validPtr = 42; // possibly corrupting write
}While the managed implementation here avoids the minor pinning overhead, it is unsound because
invalidPtr might become an exterior pointer while the actual address of array[0] is being updated by GC.
Such bugs are subtle, and even .NET has run afoul of them during development.
Recommendations
- ❌ DON'T create invalid managed pointers, even if they are not dereferenced or they are located inside never-executed code paths.
- For more information on what constitutes a valid managed pointer, see ECMA-335, Sec. II.14.4.2 Managed pointers; and ECMA-335 CLI Specification Addendum, Sec. II.14.4.2.
- ✔️ DO use pinned unmanaged pointers if the algorithm requires such manipulations.
5. Reinterpret-like type casts
While all kinds of struct-to-class or class-to-struct casts are an undefined behavior by definition, it's also possible to encounter unreliable patterns with struct-to-struct or class-to-class conversions. A typical example of an unreliable pattern is the following code:
struct S1
{
string a;
nint b;
}
struct S2
{
string a;
string b;
}
S1 s1 = ...
S2 s2 = Unsafe.As<S1, S2>(ref s1); // Bug! A random nint value becomes a reference reported to the GC.And even if the layout is similar, you should still be careful when GC references (fields) are involved.
Recommendations
- ❌ DON'T cast structs to classes or vice versa.
- ❌ DON'T use
Unsafe.Asfor struct-to-struct or class-to-class conversions unless you're absolutely sure that the cast is legal. For more information, see the Remarks section of theUnsafe.AsAPI docs). - ✔️ DO prefer safer field-by-field copying, external libraries such as AutoMapper, or Source Generators for such conversions.
- ✔️ DO prefer
Unsafe.BitCastoverUnsafe.As, asBitCastprovides some rudimentary usage checks. Note that these checks do not provide full correctness guarantees, meaningBitCastis still considered an unsafe API.
6. Bypassing the Write Barrier and non-atomic operations on GC references
Normally, all kind of writes or reads of GC references are always atomic. Also, all attempts to assign a GC reference (or a byref to struct with GC fields) to a potential heap location go through the Write Barrier that ensures that the GC is aware of new connections between objects. However, unsafe code allows us to bypass these guarantees and introduce unreliable patterns. Example:
unsafe void InvalidCode1(object[] arr1, object[] arr2)
{
fixed (object* p1 = arr1)
fixed (object* p2 = arr2)
{
nint* ptr1 = (nint*)p1;
nint* ptr2 = (nint*)p2;
// Bug! We're assigning a GC pointer to a heap location
// without going through the Write Barrier.
// Moreover, we also bypass array covariance checks.
*ptr1 = *ptr2;
}
}Similarly, the following code with managed pointers is also unreliable:
struct StructWithGcFields
{
object a;
int b;
}
void InvalidCode2(ref StructWithGcFields dst, ref StructWithGcFields src)
{
// It's already a bad idea to cast a struct with GC fields to `ref byte`, etc.
ref byte dstBytes = ref Unsafe.As<StructWithGcFields, byte>(ref dst);
ref byte srcBytes = ref Unsafe.As<StructWithGcFields, byte>(ref src);
// Bug! Bypasses the Write Barrier. Also, non-atomic writes/reads for GC references.
Unsafe.CopyBlockUnaligned(
ref dstBytes, ref srcBytes, (uint)Unsafe.SizeOf<StructWithGcFields>());
// Bug! Same as above.
Vector128.LoadUnsafe(ref srcBytes).StoreUnsafe(ref dstBytes);
}Recommendations
- ❌ DON'T use non-atomic operations on GC references (for example, SIMD operations often don't provide them).
- ❌ DON'T use unmanaged pointers to store GC references into heap locations (omitting the Write Barrier).
7. Assumptions about object lifetimes (finalizers, GC.KeepAlive)
Avoid making assumptions about the lifetime of objects from the GC's perspective. Specifically, do not assume that an object is still alive when it might not be. Object lifetimes can vary across different runtimes or even between different Tiers of the same method (Tier0 and Tier1 in RyuJIT). Finalizers are a common scenario where such assumptions can be incorrect.
public class MyClassWithBadCode
{
public IntPtr _handle;
public void DoWork() => DoSomeWork(_handle); // A use-after-free bug!
~MyClassWithBadCode() => DestroyHandle(_handle);
}
// Example usage:
var obj = new MyClassWithBadCode()
obj.DoWork();In this example, DestroyHandle might be called before DoWork completes or even before it begins.
Therefore, it's crucial not to assume that objects, such as this, will remain alive until the end of the method.
void DoWork()
{
// A pseudo-code of what might happen under the hood:
IntPtr reg = this._handle;
// 'this' object is no longer alive at this point.
// <-- GC interrupts here, collects the 'this' object, and triggers its finalizer.
// DestroyHandle(_handle) is called.
// Bug! 'reg' is now a dangling pointer.
DoSomeWork(reg);
// You can resolve the issue and force 'this' to be kept alive (thus ensuring the
// finalizer will not run) by uncommenting the line below:
// GC.KeepAlive(this);
}Therefore, it is recommended to explicitly extend the lifetime of objects using System.GC.KeepAlive(System.Object) or System.Runtime.InteropServices.SafeHandle.
Another classic instance of this problem is System.Runtime.InteropServices.Marshal.GetFunctionPointerForDelegate() API:
var callback = new NativeCallback(OnCallback);
// Convert delegate to function pointer
IntPtr fnPtr = Marshal.GetFunctionPointerForDelegate(callback);
// Bug! The delegate might be collected by the GC here.
// It should be kept alive until the native code is done with it.
RegisterCallback(fnPtr);Recommendations
- ❌ DON'T make assumptions about object lifetimes. For instance, never assume
thisis always alive through the end of the method. - ✔️ DO use System.Runtime.InteropServices.SafeHandle for managing native resources.
- ✔️ DO use System.GC.KeepAlive(System.Object) to extend the lifetime of objects when necessary.
8. Cross-thread access to local variables
Accessing local variables from a different thread is generally considered bad practice. However, it becomes explicitly undefined behavior when managed references are involved, as outlined in the .NET Memory Model.
Example: A struct containing GC references might be zeroed or overwritten in a non-thread-safe manner within a no-GC region while another thread is reading it, leading to undefined behavior.
Recommendations
- ❌ DON'T access locals across threads (especially if they contain GC references).
- ✔️ DO use heap or unmanaged memory (for example, System.Runtime.InteropServices.NativeMemory.Alloc) instead.
9. Unsafe bounds check removal
In C#, all idiomatic memory accesses include bounds checks by default. The JIT compiler can remove these checks if it can prove that they are unnecessary, as in the example below.
int SumAllElements(int[] array)
{
int sum = 0;
for (int i = 0; i < array.Length; i++)
{
// The JIT knows that within this loop body, i >= 0 and i < array.Length.
// The JIT can reason that its own bounds check would be duplicative and
// unnecessary, so it opts not to emit the bounds check into the final
// generated code.
sum += array[i];
}
}While the JIT is continually improving at recognizing such patterns, there are still scenarios where it leaves the checks in place, potentially impacting performance in hot code. In such cases, you might be tempted to use unsafe code to manually remove these checks without fully understanding the risks or accurately assessing the performance benefits.
Consider for example the following method.
int FetchAnElement(int[] array, int index)
{
return array[index];
}If the JIT cannot prove that index is always legally within the bounds of array, it will rewrite the method to look something like the below.
int FetchAnElement_AsJitted(int[] array, int index)
{
if (index < 0 || index >= array.Length)
throw new IndexOutOfBoundsException();
return array.GetElementAt(index);
}To reduce the overhead from that check in hot code, you might be tempted to use unsafe-equivalent APIs (Unsafe and MemoryMarshal):
int FetchAnElement_Unsafe1(int[] array, int index)
{
// DANGER: The access below is not bounds-checked and could cause an access violation.
return Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(array), index);
}Or use pinning and raw pointers:
unsafe int FetchAnElement_Unsafe2(int[] array, int index)
{
fixed (int* pArray = array)
{
// DANGER: The access below is not bounds-checked and could cause an access violation.
return pArray[index];
}
}This can lead to random crashes or state corruption if index is outside the bounds of array.
Such unsafe transformations can have performance benefits on very hot paths, but these benefits are
often transient, as each .NET release improves the JIT's ability to eliminate unnecessary
bounds checks when it is safe to do so.
Recommendations
- ✔️ DO verify whether the latest version of .NET still can't eliminate the bounds check. If it can, rewrite using safe code. Otherwise, file an issue against the RyuJIT. Use this tracking issue as a good starting point.
- ✔️ DO measure the real-world performance impact. If the performance gain is negligible or the code isn't proven to be hot outside of a trivial microbenchmark, rewrite using safe code.
- ✔️ DO provide additional hints to the JIT, such as manual bounds checks before loops and saving fields to locals, as .NET Memory Model might conservatively prevent the JIT from removing bounds checks in some scenarios.
- ✔️ DO guard code with
Debug.Assertbounds checks if unsafe code is still necessary. Consider the example below.
Debug.Assert(array is not null);
Debug.Assert((index >= 0) && (index < array.Length));
// Unsafe code hereYou might even refactor these checks into reusable helper methods.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static T UnsafeGetElementAt<T>(this T[] array, int index)
{
Debug.Assert(array is not null);
Debug.Assert((index >= 0) && (index < array.Length));
return Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(array), index);
}Inclusion of Debug.Assert doesn't provide any soundness checks for Release builds, but it might help detect potential bugs in Debug builds.
10. Memory access coalescing
You might be tempted to use unsafe code to coalesce memory accesses to improve performance.
A classic example is the following code to write "False" into a char array:
// Naive implementation
static void WriteToDestination_Safe(char[] dst)
{
if (dst.Length < 5) { throw new ArgumentException(); }
dst[0] = 'F';
dst[1] = 'a';
dst[2] = 'l';
dst[3] = 's';
dst[4] = 'e';
}
// Unsafe coalesced implementation
static void WriteToDestination_Unsafe(char[] destination)
{
Span<char> dstSpan = destination;
if (dstSpan.Length < 5) { throw new ArgumentException(); }
ulong fals_val = BitConverter.IsLittleEndian ? 0x0073006C00610046ul : 0x00460061006C0073ul;
MemoryMarshal.Write(MemoryMarshal.AsBytes(dstSpan.Slice(0, 4)), in fals_val); // Write "Fals" (4 chars)
dstSpan[4] = 'e'; // Write "e" (1 char)
}In previous versions of .NET, the unsafe version using MemoryMarshal was measurably faster than
the straightforward safe version. However, modern versions of .NET contain a much improved JIT
that produces equivalent codegen for both cases. As of .NET 10, the x64 codegen is:
; WriteToDestination_Safe
cmp eax, 5
jl THROW_NEW_ARGUMENTEXCEPTION
mov rax, 0x73006C00610046
mov qword ptr [rdi+0x10], rax
mov word ptr [rdi+0x18], 101
; WriteToDestination_Unsafe
cmp edi, 5
jl THROW_NEW_ARGUMENTEXCEPTION
mov rdi, 0x73006C00610046
mov qword ptr [rax], rdi
mov word ptr [rax+0x08], 101There is an even simpler and more readable version of the code:
"False".CopyTo(dst);As of .NET 10, this call produces identical codegen as above. It even has an additional benefit: it hints to the JIT that strict per-element writes are not required to be atomic. The JIT might combine this hint with other contextual knowledge to provide even more optimizations beyond what was discussed here.
Recommendations
- ✔️ DO prefer idiomatic safe code instead of unsafe for memory access coalescing:
- Prefer
Span<T>.CopyToandSpan<T>.TryCopyTofor copying data. - Prefer
String.EqualsandSpan<T>.SequenceEqualfor comparing data (even when usingStringComparer.OrdinalIgnoreCase). - Prefer
Span<T>.Fillfor filling data andSpan<T>.Clearfor clearing data. - Be aware that per-element or per-fields writes/reads might be coalesced by JIT automatically.
- Prefer
- ✔️ DO file an issue against dotnet/runtime if you write idiomatic code and observe that it is not optimized as expected.
- ❌ DON'T coalesce memory accesses manually if you're not sure about misaligned memory access risks, atomicity guarantees, or the associated performance benefits.
11. Unaligned memory access
The memory access coalescing described in Memory access coalescing often results in explicit or implicit misaligned reads/writes. While this usually doesn't cause serious issues (aside from potential performance penalties due to crossing cache and page boundaries), it still poses some real risks.
For example, consider the scenario where you're clearing two elements of an array at once:
uint[] arr = _arr;
arr[i + 0] = 0;
arr[i + 1] = 0;Let's say the previous values at these locations were both uint.MaxValue (0xFFFFFFFF).
The .NET Memory Model guarantees that both writes are atomic, so all other threads in the process will only ever
observe the new value 0 or the old value 0xFFFFFFFF, never "torn" values like 0xFFFF0000.
However, assume the following unsafe code is used to bypass the bounds check and zero both elements with a single 64-bit store:
ref uint p = ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(arr), i);
Unsafe.WriteUnaligned<ulong>(ref Unsafe.As<uint, byte>(ref p), 0UL);This code has the side effect of removing the atomicity guarantee. Torn values might be observed by other threads, leading to undefined behavior. For such a coalesced write to be atomic, the memory must be aligned to the size of the write (8 bytes in this case). If you attempt to manually align the memory prior to the operation, you must consider that the GC can relocate (and, effectively, change the alignment of) the array at any time if it's not pinned. See the .NET Memory Model documentation for more details.
Another risk of unaligned memory access is the potential for an application crash in certain scenarios. While some .NET runtimes rely on the OS to fixup misaligned accesses, there are still some scenarios on some platforms where misaligned access can lead to an System.DataMisalignedException (or System.Runtime.InteropServices.SEHException). Some of the examples include:
Interlockedoperations on misaligned memory on some platforms (example).- Misaligned floating point operations on ARM.
- Accessing special device memory with certain alignment requirements (not really supported by .NET).
Recommendations
- ❌ DON'T use unaligned memory accesses in lock-free algorithms and other scenarios where atomicity is important.
- ✔️ DO align data manually if necessary, but keep in mind that the GC can relocate objects
at any time, effectively changing the alignment dynamically. This is especially important for various
StoreAligned/LoadAlignedAPIs in SIMD. - ✔️ DO use explicit unaligned Read/Write APIs such as System.Runtime.CompilerServices.Unsafe.ReadUnaligned/System.Runtime.CompilerServices.Unsafe.WriteUnaligned
instead of aligned ones such as [System.Runtime.CompilerServices.Unsafe.Read
(System.Void*)](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.unsafe.read--1)/[System.Runtime.CompilerServices.Unsafe.Write(System.Void*,)](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.unsafe.write--1) or [System.Runtime.CompilerServices.Unsafe.As(`@)](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.unsafe.as--2) if data might be misaligned. - ✔️ DO keep in mind that various memory manipulation APIs such as System.Span.CopyTo(System.Span{}) also don't provide atomicity guarantees.
- ✔️ DO consult with the .NET Memory Model documentation (see references) for more details on atomicity guarantees.
- ✔️ DO measure performance across all your target platforms, as some platforms impose a significant performance penalty for unaligned memory accesses. You may find that on these platforms, naive code performs better than clever code.
- ✔️ DO keep in mind that there are scenarios and platforms where unaligned memory access might lead to an exception.
12. Binary (de)serialization of structs with paddings or non-blittable members
Be cautious when you use various serialization-like APIs to copy or read structs to or from byte arrays.
If a struct contains paddings or non-blittable members (for example, bool or GC fields), then classic unsafe memory operations such as Fill, CopyTo, and SequenceEqual might accidentally copy sensitive data from the stack to the paddings or treat garbage data as significant during comparisons creating rarely reproducible bugs. A common anti-pattern might look like this: