๐ memsafe
One of the most hardened memory guards in the Rust ecosystem.
Five layers of OS-level armor around every secret byte โ locked in RAM, kept out of swap and core dumps, sealed against reads even from your own process, never touching the heap, and provably wiped on drop.
One safe type. Zero configuration. Almost every platform Rust runs on.
๐ง Linux ย ยทย ๐ macOS ย ยทย ๐ช Windows ย ยทย โ๏ธ any Unix CI-proven on 10 OS/architecture targets โ x86_64 ยท aarch64 ยท ARM ยท i686 ยท RISC-V ยท PowerPC
The password you drop() doesn't die: its bytes linger in freed heap blocks, get paged to swap, and show up in core dumps. memsafe gives it armor instead โ one call runs the full protection pipeline that production secret stores build by hand:
use Secret;
let mut api_key = new_with.unwrap;
mmap โโโถ mlock โโโถ MADV_DONTDUMP โโโถ write in place โโโถ PROT_NONE
page pinned excluded from secret never locked down
alloc in RAM core dumpsยน touches the until *you*
heap or stack read() it
ยน Linux. See platform notes for exactly what each OS gets โ we don't claim what an OS can't deliver.
The life of your secret
in a plain String |
in a memsafe::Secret |
|
|---|---|---|
| Swapped to disk under memory pressure | ๐ฌ whenever the kernel feels like it | ๐ never โ page is mlock'd in RAM |
| Visible in a core dump | ๐ฌ yes | ๐ excluded on Linux (MADV_DONTDUMP) |
| Readable while your code isn't using it | ๐ฌ by any stray pointer or overflow | ๐ซ page is PROT_NONE (Unix) โ even your own process faults on access |
| Bytes after drop | ๐ฌ linger on the freed heap | ๐งน volatile-zeroed + compiler-fenced โ the optimizer can't skip the wipe |
Leaked by a stray {:?} in a log line |
๐ฌ trivially | ๐ค Secret has no Debug, no Display |
Why you can trust it: a public API with zero unsafe for you to write, exactly two platform-gated dependencies (libc on Unix, winapi on Windows โ nothing else), a CI matrix spanning Linux (x86_64, aarch64, i686, armv7, ppc64le, riscv64), macOS (x86_64, aarch64) and Windows (x86_64, i686), constructors that hand your data back instead of losing it on failure, and an explicit threat model that says out loud what we don't protect against.
Contents โ Quick start ยท Going deeper ยท What you get ยท How it compares ยท Threat model ยท Platform notes ยท Under the hood ยท Roadmap
๐ Quick start: guard a secret in 60 seconds
cargo add memsafe
Guard an API key, password, or token with Secret<N> โ pick N large enough for your secret; unused bytes stay zero:
use Secret;
// Write the secret straight into protected memory โ it never exists
// on the regular heap or in a stack temporary.
let mut secret = new_with.unwrap;
// Read it back through a guard. The page is unlocked only while
// `view` is alive, and re-sealed the moment it drops.
let view = secret.read.unwrap;
assert_eq!;
Already have the secret in a String or Vec<u8>? Hand it over โ the source is volatile-zeroed after the copy, so no readable trace stays behind:
use Secret;
let key = b"my-api-key".to_vec;
let mut secret = from_bytes.unwrap;
// Or via TryFrom:
let mut secret: = Stringfrom.try_into.unwrap;
Read a password without it ever touching a String:
use Secret;
use Read;
let mut secret = new_with.unwrap;
And if construction fails, you don't lose your data. Every owned-input constructor returns the source alongside the error, so you can retry with a bigger buffer or clean up on your own terms:
use Secret;
let too_long = Stringfrom;
match try_from
That's the whole onboarding. If all you need is a secret guard, you're done โ Secret<N> defaults to the most protective behavior on every path. Everything below is for going deeper.
๐ฌ Going deeper: the pro path
Two types are exported, one per job:
| Type | Use it for |
|---|---|
Secret<N> |
Secrets. Every constructor keeps all N bytes inline inside the protected page; the heap-pointer pitfall is unrepresentable. |
MemSafe<T> |
Non-secret protected memory. Any T that wants mlock + mprotect semantics โ FFI scratch buffers, guard regions, data you want fault-on-touch while idle. |
Secret<N> construction, precisely
| Constructor | Input | Source returned on error | Source zeroized |
|---|---|---|---|
Secret::<N>::new_with(F) (F: FnOnce(&mut [u8; N])) |
(closure writes in place) | N/A โ no source | N/A โ no source |
Secret::<N>::from_bytes(T) (T: AsMut<[u8]>) |
Owned bytes | Yes, with the error | Yes โ len bytes overwritten before drop |
<Secret<N> as TryFrom<String>>::try_from |
Owned String |
Yes, with the error | Yes โ via from_bytes on into_bytes() |
<Secret<N> as TryFrom<&str>>::try_from |
Borrowed | N/A โ borrow returned to caller | No (caller-managed) |
Capacity vs. length on
Vec/String.from_bytesonly zeroes the bytes exposed byAsMut::as_mut, which islen, notcapacity. If yourVecorStringhas unused trailing capacity that may have held earlier secret content, callshrink_to_fit()before passing it in.
MemSafe<T> โ the general-purpose protected container
MemSafe<T> is the lower-level building block: a protected memory region holding any T. It does not carry the secret-handling machinery โ no from_bytes, no TryFrom, no source zeroization.
use MemSafe;
let mut buffer = new.unwrap;
Why not just use
MemSafe<String>for a password? AStringkeeps its bytes on the regularmallocheap. Wrapping it inMemSafeonly places the 24-byte header (pointer + length + capacity) inside the protected page; the actual secret bytes still live on the unprotected heap โ swappable, dump-able, and not zeroed when the allocation is freed.Secret<N>exists precisely to make that misuse impossible โ it can only ever wrap[u8; N], which has no indirection.
Type-state API โ access states checked by the compiler
With the type-state feature, the buffer's access state is carried in the type, so reading a no-access buffer or writing through a read-only handle is a compile-time error:
cargo add memsafe --features type-state
use MemSafe;
// initialize a 32-byte buffer in no-access state
let secret = new.unwrap;
// transition to read-write and write into it
let info = "my-secret-info";
let mut secret = secret.read_write.unwrap;
secret.copy_from_slice;
// transition to read-only and read from it
let secret = secret.read_only.unwrap;
println!;
Secret<N>does not yet have a type-state variant โ secret bytes go through the runtime-stateSecretAPI regardless of thetype-statefeature. If you want compile-time-enforced access state on a secret, open an issue or PR; the building blocks (Cell+type_state::MemSafe) are already in place.
What you get
| Property | How it's enforced |
|---|---|
| Not paged to disk | mlock (Unix) / VirtualLock (Windows) on the backing page |
| Not in core dumps | madvise(MADV_DONTDUMP) โ Linux only |
| Inaccessible between operations | mprotect(PROT_NONE) on Unix; PAGE_READONLY on Windows (the strictest level Windows offers in this model) |
| Zero-on-drop | Byte-wise write_volatile + compiler_fence(SeqCst) over the page immediately before munmap / VirtualFree |
| No allocator round-trip | Bytes are copied straight from the source into the protected page; they never reach the regular malloc heap |
| Source zeroization | Secret::from_bytes(T) volatile-overwrites the source byte slice before drop, for any T: AsMut<[u8]> |
| Recoverable on error | Secret constructors return the original source alongside the error so the caller can retry, inspect, or further zeroize it |
| In-place initialization | Secret::new_with lets a closure write straight into the protected page โ the secret never visits a stack temporary or the regular heap |
No Debug, no Display |
Secret implements neither, so a stray {:?} in a log line cannot leak it |
| Heap-pointer pitfall unrepresentable | Secret<N> only ever wraps [u8; N] โ by construction it is impossible to protect a header while leaving the payload on the unprotected heap |
| Compile-time access enforcement | Optional, behind the type-state feature |
How it compares
The Rust ecosystem has excellent secret-hygiene crates; they solve different layers of the problem. memsafe's niche is automating the full OS-level stack behind a safe, typed API:
| Capability | zeroize |
secrecy |
memsec |
region |
memsafe |
|---|---|---|---|---|---|
| Elision-resistant zero-on-drop | โ | โ (via zeroize) | manual (memzero) |
โ | โ automatic |
Locked out of swap (mlock) |
โ | โ | manual | manual | โ automatic |
Page inaccessible between uses (mprotect) |
โ | โ | manual | manual | โ automatic per-access |
| Core-dump exclusion | โ | โ | via its mlock (Linux) |
โ | โ automatic (Linux) |
| Secret bytes kept off the global heap | โ | โ | your responsibility | your responsibility | โ
by construction (Secret<N>) |
Safe API (no unsafe for the caller) |
โ | โ | โ raw primitives | โ | โ |
| Source returned on constructor failure | โ | โ | โ | โ | โ |
These are complements, not competitors: zeroize/secrecy give you type-level hygiene for secrets that must live in ordinary structs, memsec and region give you raw primitives to build your own container โ and memsafe is that container, already built, tested, and cross-platform.
Threat model
We list this explicitly because vague "secure" claims are how secrets get leaked.
memsafe is designed to defend against:
- Secrets being written to disk via swap or hibernation (mitigated by
mlock/VirtualLock). - Secrets appearing in a Linux core dump (mitigated by
madvise(MADV_DONTDUMP)). - The Rust / system allocator handing freed-but-not-zeroed bytes to a later caller (mitigated by zero-on-drop and by never routing the secret through the global allocator).
- Accidental reads or writes while the buffer is logically inactive (mitigated by
mprotect/VirtualProtectbetween operations and, optionally, by the type-state API). - The "owned bytes" case: the secret being silently left on the regular heap after the source is dropped (mitigated by
Secret::from_bytes's explicit volatile zeroization of the source slice before drop). - The "wrong wrapper" case: a developer wrapping a
StringorVec<u8>inMemSafeand unknowingly leaving the payload on the unprotected heap (mitigated structurally bySecret<N>โ by construction it can only wrap[u8; N], where the secret lives inline).
memsafe does NOT defend against:
- A process running with privilege equivalent to or greater than your own โ for example, a debugger attached via
ptrace, or root on the same machine. OS protection ends at the process boundary. - Side-channel attacks (cache timing, Spectre/Meltdown-class speculation, power analysis, etc.).
- Bytes left behind in CPU registers, vector lanes, or temporary spill slots after a
copy_from_slice. The protection is at the memory-page level, not the microarchitectural level. - Hardware attackers with physical bus access, cold-boot capability, or DMA to the host.
- Hardware/microarchitectural attacks against zeroization itself: bytes evicted into cache lines that get speculatively read out, RowHammer, and similar. The crate's wipes are language-level (volatile + fence); the silicon is not in scope.
If your threat model includes any of the items in the second list, memsafe is a useful layer of defense in depth but is not sufficient on its own.
Platform-specific notes
mlocklimits. On Linux, unprivileged processes are subject toRLIMIT_MEMLOCK(commonly 64 KiB by default). EachMemSafeallocation is rounded up to a full page (typically 4 KiB), so a handful of secrets fit easily inside the default limit; allocating thousands may require raising the limit. macOS has its own per-process locked-memory caps.- Page rounding.
mmapandVirtualAllocallocate in page-sized units (typically 4 KiB). AMemSafe<[u8; 32]>therefore consumes a full page; the trailing bytes of the page are part of the same locked, access-controlled, zero-on-drop region. - Linux-only protections.
madvise(MADV_DONTDUMP)is Linux-only. macOS, the BSDs, and Windows do not currently have an equivalent in this crate; on those systems, the page may still appear in process dumps. Windows core-dump exclusion is on the Roadmap. - Windows access model. Windows lacks an exact
PROT_NONEanalogue in this crate's model โ the lowest-privilege state isPAGE_READONLY. This means reads through the protected handle are always possible on Windows; only writes are guarded behindwrite()/read_write(). Unix systems usePROT_NONEbetween operations, blocking both reads and writes. - Cross-platform by construction.
libcis pulled in only oncfg(unix)andwinapionly oncfg(windows); the correct backend is selected automatically โ consumers never pick a platform feature flag. CI covers Linux (x86_64, aarch64, i686, armv7, ppc64le, riscv64), macOS (x86_64, aarch64), and Windows (x86_64, i686), with both default andtype-statefeature variants.
Under the hood
The implementation behind each guarantee, for readers who want the details:
-
Allocation and locking. Each instance allocates one or more pages via
mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)on Unix orVirtualAlloc(NULL, len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE)on Windows. The page is then pinned withmlock/VirtualLockso the kernel cannot page it out to swap or include it in a hibernation file. On Linux,madvise(MADV_DONTDUMP)additionally tells the kernel to omit the page from any core dump produced for the process. -
Access transitions. All access to the backing page goes through the internal
Cellabstraction, which callsmprotect(Unix) orVirtualProtect(Windows) to elevate the page for the duration of a read or write. As soon as the temporary access guard is dropped, the page is restored to its lowest-privilege state (PROT_NONEon Unix,PAGE_READONLYon Windows). Thetype-statefeature lifts this state machine into the type system. -
In-place initialization avoids a stack copy.
Secret::<N>::new_with(|buf| ...)allocates the protected page first, then hands the closure a&mut [u8; N]that points inside the locked region. The secret bytes are written directly into protected memory; they never live in a stack-allocated[u8; N]that the compiler would otherwise leave behind in the calling frame. -
Panic-safe construction. Construction is guarded by an internal RAII rollback guard: if any setup step fails, or a caller-provided
new_withclosure panics after writing partial secret bytes, unwinding volatile-zeroes the page, unlocks it, and unmaps it before the error or panic propagates. A partially-built secret can neither leak nor linger. -
Source zeroization for
Secret::from_bytes. After copying the source bytes into the protected page,from_byteswalks the source slice (everythingAsMut::as_mutexposes) and overwrites every byte withptr::write_volatile(0), followed by acompiler_fence(Ordering::SeqCst). Volatile stores are guaranteed side effects the compiler may not elide, and the fence prevents the wipe from being reordered past later code. On a memory-protection error the zeroization has already run; on a length-mismatch error the source is returned untouched so the caller can decide what to do with it. -
Drop-time zeroization is volatile and fenced. The
Dropimplementation re-elevates the page to read/write, runsdrop_in_placeon the contents, overwrites the page byte-by-byte withwrite_volatile, emits acompiler_fence(SeqCst), releases the lock withmunlock/VirtualUnlock, and finally returns the page to the OS withmunmap/VirtualFree. The volatile + fence guarantee is language-level โ independent of whether the following syscall acts as an opaque barrier. -
Heap-pointer footgun is structurally prevented.
Secret<N>is a newtype overMemSafe<[u8; N]>. Itsnew_with,from_bytes,TryFrom<&str>, andTryFrom<String>constructors all produce inline byte storage; there is no way to instantiate aSecretwhose payload lives behind an indirection.
Roadmap
Planned hardening work, roughly in priority order:
- Closure-based access API. Replace
read()/write()guards withread(|r| ...)/write(|w| ...)closures, so the page is provably returned to its lowest-privilege state when the closure returns. - Guard pages. Allocate adjacent inaccessible pages around the protected region to turn over- and under-runs into immediate faults.
- Windows core-dump exclusion. Mark protected pages so they are omitted from minidumps.
- In-memory encryption at rest. Encrypt the page contents while the buffer is in its lowest-privilege state, decrypting only inside
read()/write(). Defense in depth against attackers who can read the process's memory. - Anti-debugging hooks. Detect or refuse
ptrace/ debugger attachment. - Side-channel mitigation. Constant-time read/write paths to resist timing and speculative-execution attacks.
- Custom allocator. Randomized placement and integrity checks to obscure memory locations.
See the Milestones for live status.
Security notice
memsafe is designed against the threat model documented above and has not undergone a formal third-party security audit. We recommend reviewing the Threat model section against your own requirements before relying on this crate for sensitive data. Bug reports and security findings are very welcome โ please open an issue or a PR.
Contributing
Issues and pull requests are welcome at github.com/po0uyan/memsafe โ enhancements, security fixes, and anything that pushes the crate toward the Roadmap.
cargo test # default features
cargo test --features type-state # compile-time state machine
cargo clippy --all-targets # lints
License
Licensed under the MIT License. See LICENSE for details.