Skip to content

feat(vault-agent): mlock the user keys (no swap exposure)#32

Merged
UnbreakableMJ merged 1 commit into
mainfrom
agent-mlock
Jun 16, 2026
Merged

feat(vault-agent): mlock the user keys (no swap exposure)#32
UnbreakableMJ merged 1 commit into
mainfrom
agent-mlock

Conversation

@UnbreakableMJ

Copy link
Copy Markdown
Contributor

What

Completes the agent's memory-hygiene hardening (after core dumps + ptrace in PR #31): the unwrapped user keys (user_enc/user_mac) are now mlocked into RAM so they can't be paged to a swap file. This finishes the "Security-aware power user" persona's ask (no swap exposure, mlocked keys).

How

  • crates/vault-agent/src/sealed.rs — a new SealedKey newtype wrapping the key:
    • boxes the bytes for a stable heap address that survives Vault/SealedKey moves (so the lock taken at construction stays valid);
    • best-effort mlocks just that page via the safe region crate — surgical (only the key pages, never the whole process), so it's always within RLIMIT_MEMLOCK and can't OOM the tokio/rustls allocator (unlike mlockall(MCL_FUTURE)). Keeps #![forbid(unsafe_code)];
    • on Drop: zeroizes while the page is still locked, then munlocks;
    • stores locked: bool (not the LockGuard) so SealedKey: Send — required since Vault lives in Arc<Mutex<…>> across awaits.
  • state.rs / unlock.rsVault.user_enc/user_mac become SealedKey. Since SealedKey: Deref<Target=[u8; 32]>, the ~60 &v.user_enc encrypt/decrypt sites and the SessionBlob copy-out are unchanged; only the construction sites wrap the keys, and vault_from_user_key now borrows them.

Tests

sealed.rs: Deref returns the key bytes (the path every call site relies on) and Debug doesn't leak bytes. The whole existing vault-agent suite (unlock round-trips, add/edit, pin, resume) stays green through SealedKey's Deref — proof the newtype is transparent.

Verification

fmt --check, fresh-isolated clippy --all-features -D warnings, cargo test --workspace --all-targets, cargo deny check (region MIT), both headless builds (the dep is non-optional) — all green.

Manual on a normal kernel: unlock the agent, then grep VmLck /proc/$(pgrep -n vault-agent)/status → non-zero locked memory.

Out of scope (still pending for the v0.1 tag)

The ≥24h fuzz soak, a live PQC handshake test, and the §11.2 two-week daily-driver attestation — then a slice cuts v0.1.0.

🤖 Generated with Claude Code

Completes the agent memory-hygiene hardening (after core dumps + ptrace):
the unwrapped user keys are now locked into RAM so they can't be paged to
a swap file.

- sealed.rs: a `SealedKey` newtype wrapping the user key. It boxes the
  bytes (stable heap address that survives `Vault` moves) and best-effort
  `mlock`s the page via the safe `region` crate — surgical (only the key
  pages, never the whole process → no RLIMIT_MEMLOCK/OOM risk), keeping
  the crate's forbid(unsafe_code). On drop it zeroizes while still
  locked, then munlocks. Stores `locked: bool` (not the LockGuard) so it
  stays Send (Vault lives in Arc<Mutex<…>>).
- state.rs/unlock.rs: `Vault.user_enc`/`user_mac` become `SealedKey`.
  `SealedKey: Deref<Target=[u8;32]>`, so the ~60 encrypt/decrypt call
  sites and the SessionBlob copy-out are unchanged; only the construction
  sites wrap the keys. `vault_from_user_key` now borrows the keys.
- docs: CHANGELOG, README hardening line, plus the existing test stubs.

The persona's memory-hygiene ask (core dumps + ptrace + no-swap) is now
complete.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@UnbreakableMJ
UnbreakableMJ merged commit 20b999d into main Jun 16, 2026
8 checks passed
@UnbreakableMJ
UnbreakableMJ deleted the agent-mlock branch June 16, 2026 11:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant