PinnedMemory is a lightweight .NET library for handling sensitive or performance-critical arrays that should stay pinned in memory, be zeroed, and optionally be memory-locked at the OS level.
It is designed for scenarios such as:
- handling secrets in byte/char buffers,
- passing stable pointers to native interop,
- reducing GC relocation concerns for low-level memory operations.
- Why use PinnedMemory?
- Install
- Supported platforms and types
- Quick start
- API behavior at a glance
- Usage patterns
- Security and correctness best practices
- Performance notes
- Common pitfalls
- FAQ
- License
When managed arrays are not pinned, the GC may move them. PinnedMemory<T> pins an array for the lifetime of the object and can also:
- zero the buffer,
- lock memory pages (platform permitting),
- zero + unlock on dispose.
This gives you a safer lifecycle for sensitive data and a predictable address for native interop work.
This project targets .NET 8 (net8.0) by default.
dotnet add package PinnedMemoryInstall-Package PinnedMemory- Windows
- Linux
- macOS
- Android
- iOS
sbytebytecharshortushortintuintlongulongfloatdoubledecimalbool
If you use an unsupported struct, construction throws ArrayTypeMismatchException.
using PinnedMemory;
var bytes = new byte[32];
using var secret = new PinnedMemory<byte>(bytes);
secret[0] = 0x41;
secret.Write(1, 0x42);
var first = secret.Read(0);
var copy = secret.ToArray();By default, construction uses
zero: trueandlocked: true.
PinnedMemory(T[] value, bool zero = true, bool locked = true, SystemType type = SystemType.Unknown)value: source array copied into an internal pooled buffer.zero:true(default): internal memory is zeroed immediately after allocation.false: initial contents are preserved.
locked:true(default): attempts OS-level page lock.false: skip lock attempt.
type: optional explicit OS selection (Windows,Linux,Osx,Android,Ios); whenUnknown, OS is detected automatically at runtime.
- Construction rents an internal buffer from
ArrayPool<T>.Shared. Dispose():- zeroes internal memory,
- unlocks if locking was requested,
- frees the pin handle,
- returns the buffer to the pool.
Length: logical length from the original input array.- Indexer
this[int i]: get/set values. Read(): returns internal buffer reference.Read(int index): read one value.Write(int index, T value): write one value.ToArray(): returns internal buffer reference.Clone(): deep-copies into a newPinnedMemory<T>.
Use zero: false when you provide meaningful input data.
using var pin = new PinnedMemory<byte>(new byte[] { 0x10, 0x20, 0x30 }, zero: false);Keep default zero: true and fill explicitly.
using var pin = new PinnedMemory<byte>(new byte[3]);
pin[0] = 65;
pin[1] = 61;
pin[2] = 77;using var original = new PinnedMemory<byte>(new byte[] { 1, 2, 3 }, zero: false);
using var clone = original.Clone();using var text = new PinnedMemory<char>(new[] { 's', 'e', 'c', 'r', 'e', 't' }, zero: false);Prefer char[]/byte[] over string when handling secrets.
-
Always dispose promptly
- Use
using/using var. - Do not rely on process shutdown for cleanup.
- Use
-
Choose
zerointentionallyzero: truefor allocate-then-populate flows.zero: falsefor pre-populated input arrays.
-
Avoid converting secrets to
stringstringis immutable and not controllably zeroable.- Keep secrets in pinned
byte[]/char[]as long as possible.
-
Minimize copies
Read()/ToArray()expose the internal buffer reference.- If you copy data elsewhere, you now have additional memory to scrub.
-
Treat locking as best effort
- OS lock calls can be constrained by permissions/limits.
- Keep least-privilege defaults in deployment (e.g., memlock limits on Linux).
-
Do not access after dispose
- The underlying buffer is returned to
ArrayPool<T>. - Retained references can observe reused data from other code.
- The underlying buffer is returned to
-
Keep scope small
- Hold pinned memory only for the shortest practical duration.
- Long-lived pinned regions can negatively affect GC behavior.
- The library uses
ArrayPool<T>.Sharedto reduce allocation pressure. - Pinning and OS page-locking have costs; use only where justified.
- For small, frequent operations, benchmark your real workload with and without locking.
-
“My provided bytes are all zero.”
- You likely used default
zero: true. - Set
zero: falsewhen preserving initial values.
- You likely used default
-
“I used
ToArray()then disposed, but still read old reference.”ToArray()returns internal buffer reference, not a defensive copy.- Do not keep references past disposal.
-
“Can I call
ToString()to inspect data?”- No.
ToString()intentionally throwsSecurityException.
- No.
No. It helps memory hygiene inside your process. It is not a full secret-management system.
No. Locking is platform- and permission-dependent.
No explicit thread-safety guarantees are provided. Synchronize concurrent access at the caller boundary.
This project is licensed under the MIT License.