Go Reference CI Go Report Card
Harden secrets in memory — keep private keys, tokens, and passwords off the Go garbage-collected heap, in OS-locked pages excluded from swap and, where the platform allows, from core dumps and from other processes. Bytes are wiped on release by an architecture-specific routine and reached only through a borrowing closure, so the plaintext never outlives its use.
Pure Go (CGO_ENABLED=0), depending only on golang.org/x/sys.
Built as internal tooling for a set of the author's own projects, then extracted and generalized. Governance is BDFL: bug fixes, hardening, and speedups-without-regression are all welcome.
Every guarantee is stated per platform, together with what it does not protect against. A security library that overstates its guarantees is worse than none. So:
- A protection that cannot be provided on a platform is reported through
Capabilities, never silently skipped. CallProbeonce at startup to see what is in force. - A platform with no lockable off-heap memory fails loudly
(
ErrNoSecureMemory) rather than degrading to unprotected heap — unless you opt in explicitly withWithInsecureFallback(). - Every
✓below is backed by a test, or is named inTESTING.md's "Deliberately not proven" list. The guard pages actually fault; the lock, dump, fork, THP and KSM flags are read back from the kernel's own/proc/self/smapsrecord on both allocation tiers; thememfd_secretisolation is checked against/proc/<pid>/memandprocess_vm_readvfrom inside the process, from a separate process, and as root; the region wipe (truncate, emergency, slot release), redaction, and no-panic promises are fuzzed. A test that skips in CI fails the job unless the skip is on a per-runner allowlist, so a proof cannot stop running unnoticed. SeeKERNELS.mdfor the kernels the suite has been executed on. - secmem has not had an independent third-party security audit. Every claim
here is self-verified by the suite that runs in CI, and self-verification is
not an audit —
TESTING.mdlists what is measured and, in its "Deliberately not proven" section, what is not. SeeSECURITY.md.
go get github.com/deadpoets/secmem
buf, err := secmem.NewBuffer(rawKey) // rawKey is wiped after the copy if err != nil { return err } defer buf.Destroy() // always defer immediately err = buf.WithBytesErr(func(borrowed []byte) error { // borrowed is valid ONLY inside this closure — never store it. return sign(borrowed, msg) })
For values you hold and might log, wrap them in a
Secret: it renders as
[REDACTED] through fmt, encoding/json, and log/slog. For scrubbing
free-form log text, the redact
subpackage provides a slog.Handler wrapper.
✓ enforced · ⚠ best-effort (failure is reported, not fatal) · ✗ not
provided · LOUD opt-in only. This table is the threat model's spine; see
THREAT-MODEL.md for what none of it protects against.
| Protection | linux/amd64·arm64 (≥5.14, secretmem live †) | linux (older / 32-bit / secretmem inert) | darwin | windows | other (compile-checked, not executed) |
|---|---|---|---|---|---|
| Off the Go heap | ✓ memfd_secret | ✓ mmap | ✓ mmap | ✓ VirtualAlloc | LOUD heap only |
| No swap (locked) | ✓ | ✓ mlock | ✓ mlock | ⚠ VirtualLock ‡ | ✗ |
Kernel isolation (defeats passive reads — ptrace, /proc/<pid>/mem, crash dumps — including by root: the in-tree proof runs unprivileged, and CI repeats it as root) |
✓ memfd_secret | ✗ (falls to mlock) | ✗ | ✗ | ✗ |
| Excluded from crash dumps | ⚠ MADV_DONTDUMP | ⚠ MADV_DONTDUMP | ✗ | ⚠ WER exclusion (reported by the registration call, not verified by a dump) | ✗ |
| Not inherited across fork | ⚠ MADV_DONTFORK | ⚠ MADV_DONTFORK | ✗ | n/a | ✗ |
| No THP/KSM secret copies | ✓ madvise | ✓ madvise | n/a | n/a | ✗ |
| Guaranteed wipe on destroy | ✓ asm + cache flush (the zeros are read back; the flush is structural, not measured) | ✓ (amd64/arm64 asm; else ⚠ barriered store loop) | ✓ asm | ✓ asm (amd64/arm64) | ⚠ barriered store loop, no flush |
| Guard pages + overflow canary | ✓ | ✓ | ✓ | ✓ | ✗ (heap fallback) |
Stack-frame scrub inside Scrub |
✓ asm | ✓ asm on amd64/arm64; ✗ stub elsewhere | ✓ asm | ✓ asm (amd64/arm64) | ✗ stub |
| No async register dump into the window (preemption signal blocked) | ✓ SIGURG+SIGPROF | ✓ SIGURG+SIGPROF | ✗ no pthread_sigmask binding |
✗ unmaskable (SetThreadContext) |
✗ |
| Vector registers cleared after the window, on the working thread | ✓ asm | ✓ asm on amd64/arm64; ✗ elsewhere | ✓ asm | ✓ asm (amd64/arm64) | ✗ |
Register + heap scrub (Scrub) |
✓ with GOEXPERIMENT=runtimesecret |
✓ if set (amd64/arm64) | ✗ | ✗ | ✗ |
Encrypted while sealed (Seal) |
✗ | ✗ | ✗ | ✓ CryptProtectMemory | ✗ |
Process hardening (HardenProcess) |
✓ dumpable=0, no-new-privs | ✓ | ✗ | ✓ ACG + strict handles | ✗ |
| Fails loudly, never silently degrades | ✓ | ✓ | ✓ | ✓ | ✓ (LOUD opt-in) |
The suite has been executed on real linux/amd64 and linux/arm64 hardware,
spanning kernels 5.10 through 7.x (see KERNELS.md). On arm64
(Ampere Altra), the memfd_secret L4 path, the guard-page fault, the
/proc/self/mem isolation proof, and the architecture-specific wipe assembly
all pass.
The four Scrub rows are separate because they degrade separately. On Linux
the window blocks Go's preemption signal for its duration, so
runtime.asyncPreempt cannot spill the whole register file onto the stack
partway through a cipher round; on amd64 and arm64 it then zeroes the vector
register file — where vectorised crypto keeps its working state, and which
nothing in the Go runtime clears — before the window closes. What each reaches
— and the residue that is a constraint of the Go runtime or the OS rather than
something this library can fix — is set out in
THREAT-MODEL.md.
The stack-frame scrub is asserted by planting markers down the stack and reading
the abandoned frames back as zero, on linux/amd64 and linux/arm64; the signal
block is asserted against the kernel's own SigBlk for the calling thread, on
linux/arm64 (Tegra 234, kernel 6.8.12; RK3328, kernel 6.18.35) and linux/amd64
(kernel 7.0.0); the vector clear is asserted by planting a pattern in the
registers inside a window and reading them back zero after it, with a control
that must show the pattern surviving when the clear is left out.
† Whether memfd_secret is live is not decided by the kernel version, and
not even by CONFIG_SECRETMEM alone. It needs the kernel to be able to split
the linear map at page granularity; on arm64 that means rodata=full (or
DEBUG_PAGEALLOC, or KFENCE). A measured counter-example: an NVIDIA Jetson
Orin Nano on kernel 6.8.12 ships CONFIG_SECRETMEM=y and still returns
ENOSYS, because CONFIG_RODATA_FULL_DEFAULT_ENABLED is off — see
KERNELS.md. Where it is inert, secmem reports "fallback" and
uses mmap+mlock, honestly, per allocation. Read
Capabilities
at runtime; do not infer the tier from a config symbol or a uname.
‡ VirtualLock is weaker than mlock: it pins pages into the process
working set, not into physical memory. While the process is running they
stay resident and are exempt from working-set trimming, but when the memory
manager outswaps an idle process's working set as a whole, locked pages go to
the pagefile with it (Microsoft's VirtualLock remarks; Raymond Chen,
"VirtualLock only locks your memory into the working set"). No user-mode
setting closes that; Capabilities.Warnings says so on Windows, and
WINDOWS.md covers the working-set budget.
On a unified-memory SoC (Tegra, Apple Silicon, AMD APUs, most ARM SBCs) note
also that locking a page constrains the CPU's view of it, not an on-die
GPU/NPU sharing the same DRAM — see THREAT-MODEL.md.
Guard pages and the canary are a memory-safety bug-catcher, not a
confidentiality control — they trap an accidental over/under-flow, and do
nothing against a privileged reader of process memory (that is
memfd_secret's job). The Windows sealed-state cipher raises the bar against
memory dumps of a dormant secret; it is not cold-boot protection. Both are
detailed in the godoc and the threat model.
secmem(this module) —SecureBuffer,SecureArena,Secret,Capabilities/Probe,Scrub, and the process-hardening helpers. Depends only ongolang.org/x/sys.secmem/redact—Sanitizerand anslog.Handlerfor boundary-level log scrubbing. Standard library only.secmem/httpauth— anhttp.RoundTripperthat injects a credential header per request from aSecureBuffer, so an HTTP client never holds the token as a long-lived string. Standard library only.
Two further modules live in this repository and are versioned and tagged independently, so neither adds anything to the core's dependency graph:
secmem-crypto— signing, AEAD, key agreement and KDFs that keep their key material inside aSecureBufferfor the whole operation rather than copying it out first. Includes an in-place RFC 8032 Ed25519 signer, becausecrypto/ed25519's FIPS-140 cache panics on mmap'd memory — the reasoning is set out in that module's README, up front, since "rolled their own Ed25519" is a claim that deserves scrutiny. Carries a wiping fork ofgolang.org/x/crypto/argon2for the same kind of reason. Addsfilippo.io/edwards25519,golang.org/x/cryptoandgolang.org/x/sys.secmem-lint— ago/analysisanalyzer (andgo vet -vettoolbinary) that checks the borrowing-closure discipline at compile time: the slice handed toWithBytesmust not escape the closure. It detects a documented set of escape shapes, not every possible one; its README lists exactly what it resolves. Depends only ongolang.org/x/tools.
Full API docs, per-symbol runnable Examples, and per-symbol guarantees are on
pkg.go.dev. For end-to-end
programs, examples/ holds a password register/login flow and a
working, hardened SSH agent — each composing the library under real I/O,
concurrency, and shutdown. Start with the
package overview, then THREAT-MODEL.md for the limits,
ADOPTION.md for putting it into an existing service (the
secret inventory, the boundary map, and sizing the lock budget),
PITFALLS.md for the mistakes that quietly defeat it,
TESTING.md for how each claim is proven (or why it can't be),
ENVIRONMENTS.md for behavior under root / non-root /
containers, KERNELS.md for the Linux kernels the suite has run
on, and WINDOWS.md for Windows editions/builds.
Bug fixes, hardening, and speedups-without-regression are welcome — see CONTRIBUTING.md for the workflow. Contributed PRs need an approving review from the maintainer; every PR, the maintainer's included, has to pass the full required-check matrix before it can land. Found a vulnerability? See SECURITY.md — please don't file it as a public issue. Participation is governed by the Code of Conduct.
Apache-2.0. See LICENSE.