memsafe 1.0.1

A Secure cross-platform Rust library for securely wrapping data in memory
Documentation

๐Ÿ” memsafe

Locked, sealed, zero-on-drop memory for secrets.

Secrets live in a locked memory page: kept out of swap, excluded from core dumps (Linux), inaccessible between uses (on Unix even your own process can't read it), never on the regular heap, and volatile-wiped on drop. All behind one safe Rust type.

Crates.io docs.rs CI Downloads License: MIT

๐Ÿง Linux ย ยทย  ๐ŸŽ macOS ย ยทย  ๐ŸชŸ Windows ย ยทย  โš™๏ธ other Unixes CI-tested on 10 OS/architecture targets โ€” x86_64 ยท aarch64 ยท ARM ยท i686 ยท RISC-V ยท PowerPC


Dropping a String doesn't erase it. The bytes stay in the freed heap block, can get paged out to swap, and show up in core dumps. memsafe exists to close those paths:

use memsafe::Secret;

let mut api_key = Secret::<64>::new_with(|buf| {
    buf[..10].copy_from_slice(b"my-api-key");
}).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 only. See platform notes for what each OS actually provides.

Compared to a plain String

plain String memsafe::Secret
Swap under memory pressure may be paged to disk stays in RAM โ€” page is mlock'd
Core dumps included excluded on Linux (MADV_DONTDUMP)
While your code isn't using it readable through any stray pointer or overflow page is PROT_NONE on Unix โ€” even your own process faults on access (PAGE_READONLY on Windows)
Bytes after drop linger on the freed heap volatile-zeroed + compiler-fenced โ€” the optimizer cannot elide the wipe
A stray {:?} in a log line leaks it does not compile โ€” Secret has no Debug, no Display

A few things that may help you evaluate the crate. The public API needs no unsafe on your side. There are two dependencies, both platform-gated (libc on Unix, winapi on Windows). The test suite includes tests where the kernel itself verifies the page sealing: a child process reads the sealed page and must die by SIGSEGV for the test to pass. And the threat model below spells out what this crate does not protect against, which for a security library matters as much as what it does.

Contents โ€” Quick start ยท Going deeper ยท What you get ยท How it compares ยท Performance ยท Threat model ยท Platform notes ยท Under the hood ยท Roadmap


Quick start

cargo add memsafe

Store an API key, password, or token in a Secret<N>. N is the buffer size; pick one large enough for your secret, unused bytes stay zero:

use memsafe::Secret;

// Write the secret straight into protected memory โ€” it never exists
// on the regular heap or in a stack temporary.
let mut secret = Secret::<64>::new_with(|buf| {
    buf[..10].copy_from_slice(b"my-api-key");
}).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!(&view[..10], b"my-api-key");

If the secret already sits in a String or Vec<u8>, hand it over. The source gets volatile-zeroed after the copy, so no readable trace stays behind:

use memsafe::Secret;

let key = b"my-api-key".to_vec();
let mut secret = Secret::<64>::from_bytes(key).unwrap();

// Or via TryFrom:
let mut secret: Secret<64> = String::from("my-api-key").try_into().unwrap();

Reading a password straight from stdin, without it ever touching a String:

use memsafe::Secret;
use std::io::Read;

let mut secret = Secret::<256>::new_with(|buf| {
    let _ = std::io::stdin().read(&mut buf[..]).unwrap();
}).unwrap();

Construction can fail (buffer too small, mlock limit hit). When it does, the owned-input constructors give you the source back along with the error instead of eating it:

use memsafe::Secret;

let too_long = String::from("way-too-long-to-fit-in-this-buffer");
match Secret::<4>::try_from(too_long) {
    Ok(_) => unreachable!(),
    Err((source, err)) => {
        eprintln!("could not encapsulate ({err}); got back {} bytes", source.len());
    }
}

If all you need is a secret guard, that's the whole API. The rest of this document is detail.


Going deeper

The crate exports two types:

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> constructors

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_bytes only zeroes the bytes exposed by AsMut::as_mut, which is len, not capacity. If your Vec or String has unused trailing capacity that may have held earlier secret content, call shrink_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::MemSafe;

let mut buffer = MemSafe::new([0_u8; 32]).unwrap();

{
    let mut write = buffer.write().unwrap();
    write[..14].copy_from_slice(b"working-buffer");
}
{
    let read = buffer.read().unwrap();
    println!("data: {:02X?}", *read);
}

Why not just use MemSafe<String> for a password? Because it doesn't do what it looks like it does. A String keeps its bytes on the regular malloc heap; wrapping it in MemSafe only protects the 24-byte header (pointer, length, capacity). The actual secret stays on the unprotected heap: swappable, dumpable, and not zeroed when freed. This is exactly the mistake Secret<N> is designed to rule out, since it can only wrap [u8; N] and there is no indirection to get wrong.

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::type_state::MemSafe;

// initialize a 32-byte buffer in no-access state
let secret = MemSafe::new([0_u8; 32]).unwrap();

// transition to read-write and write into it
let info = "my-secret-info";
let mut secret = secret.read_write().unwrap();
secret[..info.len()].copy_from_slice(info.as_bytes());

// transition to read-only and read from it
let secret = secret.read_only().unwrap();
println!("Secure data: {:02X?}", *secret);

Secret<N> does not yet have a type-state variant โ€” secret bytes go through the runtime-state Secret API regardless of the type-state feature. 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
Zeroed in forked children madvise(MADV_WIPEONFORK) โ€” Linux only; the kernel wipes the child's copy of the page
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

There are several good crates in this space, and they solve different layers of the problem. Where memsafe fits: it automates the OS-level protections behind a typed API, instead of giving you primitives or a wrapper type alone.

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 combine well rather than compete. zeroize and secrecy are the right tools for secrets that have to live in ordinary structs. memsec and region are the right tools if you want to build your own container. memsafe is for when you want the container already built.

Performance

Protection isn't free, and it's better to know the cost up front than to discover it in a profile. Construction does about five syscalls (mmap, mlock, madvise twice on Linux, mprotect), and every guard cycle is two mprotect calls. Measured with the criterion benches in benches/ on an Apple Silicon macOS machine, release build:

Operation with memsafe plain heap (Vec<u8>)
Create + drop a 64 B secret ~2.8 ยตs ~9.5 ns
Create + drop a 4 KiB secret ~3.8 ยตs โ€”
Ingest an owned Vec (from_bytes, 64 B) ~2.9 ยตs โ€”
Read access (unseal, read, reseal) ~1.1 ยตs ~0.35 ns
Write access (unseal, write, reseal) ~1.1 ยตs โ€”

So a protected secret costs roughly 300ร— a plain allocation to create, and each access costs about a microsecond. Two things follow from that:

  • For credentials, keys, and tokens this is noise. An API key read a few times per process lifetime costs single-digit microseconds in total.
  • Don't put a per-request hot loop behind a guard cycle. If you need the secret thousands of times per second, take one guard and do the batch inside it (the page stays readable for exactly that scope), rather than opening and dropping a guard per operation.

Also note the memory cost: each secret occupies at least one OS page (typically 4 KiB) and counts against the locked-memory limit (RLIMIT_MEMLOCK), so size is effectively free up to a page but the number of live secrets is not unbounded.

Absolute numbers are dominated by syscall cost and will differ on your OS and hardware โ€” cargo bench reproduces the table for your machine.

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 (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 / VirtualProtect between 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 String or Vec<u8> in MemSafe and unknowingly leaving the payload on the unprotected heap (mitigated structurally by Secret<N> โ€” by construction it can only wrap [u8; N], where the secret lives inline).
  • A forked child inheriting a copy of the secret โ€” Linux only, via madvise(MADV_WIPEONFORK): the kernel replaces the child's view of the page with zeros. On other Unixes the child still receives an unlocked copy-on-write copy; see below.

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.
  • Hibernation (suspend-to-disk). The hibernation image contains all of physical RAM, including mlock'd pages โ€” mlock prevents swapping, not hibernation. If laptops are in your threat model, pair this crate with encrypted swap/hibernation or full-disk encryption.
  • fork() inheritance on non-Linux Unixes. Outside Linux, a forked child receives a copy-on-write copy of the secret page without the mlock โ€” an unlocked, swappable copy in a process that may not know it holds one. (On Linux the page is kernel-zeroed in the child via MADV_WIPEONFORK.) On macOS and the BSDs, avoid fork-based worker models while secrets are live, or construct secrets after forking.
  • 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

  • mlock limits. On Linux, unprivileged processes are subject to RLIMIT_MEMLOCK (commonly 64 KiB by default). Each MemSafe allocation 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. mmap and VirtualAlloc allocate in page-sized units (typically 4 KiB). A MemSafe<[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_NONE analogue in this crate's model โ€” the lowest-privilege state is PAGE_READONLY. This means reads through the protected handle are always possible on Windows; only writes are guarded behind write() / read_write(). Unix systems use PROT_NONE between operations, blocking both reads and writes.
  • Cross-platform by construction. libc is pulled in only on cfg(unix) and winapi only on cfg(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 and type-state feature 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 or VirtualAlloc(NULL, len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE) on Windows. The page is then pinned with mlock / VirtualLock so the kernel cannot page it out to swap. (Note: mlock does not exclude the page from a hibernation image โ€” see the threat model.) 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 Cell abstraction, which calls mprotect (Unix) or VirtualProtect (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_NONE on Unix, PAGE_READONLY on Windows). The type-state feature 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_with closure panics after writing partial secret bytes, unwinding volatile-zeroes the page, unlocks it, and unmaps it before the error or panic propagates.

  • Source zeroization for Secret::from_bytes. After copying the source bytes into the protected page, from_bytes walks the source slice (everything AsMut::as_mut exposes) and overwrites every byte with ptr::write_volatile(0), followed by a compiler_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 Drop implementation re-elevates the page to read/write, runs drop_in_place on the contents, overwrites the page byte-by-byte with write_volatile, emits a compiler_fence(SeqCst), releases the lock with munlock / VirtualUnlock, and finally returns the page to the OS with munmap / 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 over MemSafe<[u8; N]>. Its new_with, from_bytes, TryFrom<&str>, and TryFrom<String> constructors all produce inline byte storage; there is no way to instantiate a Secret whose payload lives behind an indirection.

Roadmap

Planned hardening work, roughly in priority order:

  • fork() hygiene beyond Linux. Linux already gets madvise(MADV_WIPEONFORK); add minherit(INHERIT_ZERO) on the BSDs where available.
  • Closure-based access API. Replace read() / write() guards with read(|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.