Documentation
¶
Overview ¶
Package secmem hardens secrets in memory.
secmem keeps sensitive bytes — private keys, tokens, passwords — off the Go garbage-collected heap, in OS-locked pages that are excluded from swap and, where the platform allows, from core dumps and from other processes. The bytes are wiped on release by an architecture-specific routine and are reached only through a borrowing closure, so the plaintext never outlives its use.
The core module is pure Go — CGO is not required — and depends only on golang.org/x/sys.
Honesty first ¶
Every guarantee secmem makes is stated per platform, together with what it does not protect against. A security library that overstates its guarantees is worse than none. A protection that cannot be provided on a given platform is reported through Capabilities rather than silently skipped, and a platform with no lockable off-heap memory fails loudly rather than degrading to unprotected heap storage. Use Probe at startup to see exactly which protections are in force for your build, and inspect a buffer's own Capabilities to see how that allocation was actually backed.
Protection model ¶
The mechanisms below are best-effort and vary by platform. What each one achieves on a given OS and architecture — and what it does not — is set out in the per-platform capability matrix; the summary here is the intent.
Off the Go heap. Secret bytes live in mmap'd pages (VirtualAlloc on Windows), outside the region the garbage collector scans, moves, or copies.
No swap. Pages are locked with mlock (VirtualLock on Windows) so they are not written to the swap device.
Kernel isolation. On 64-bit Linux (amd64/arm64, kernel 5.14+ with CONFIG_SECRETMEM) pages are backed by memfd_secret, which hides them from /proc/<pid>/mem, ptrace, and other readers of process memory. Elsewhere this is unavailable and the mapping falls through to locked anonymous memory.
Excluded from core dumps. Where the OS allows it (MADV_DONTDUMP, and an opt-in process-wide dumpable=0), the pages are kept out of core dumps. This is best-effort and its failure is reported, not fatal.
Guaranteed wipe. On destroy the pages are overwritten by an architecture-specific assembly routine that the compiler cannot elide.
Overflow trap. Each mapping is bracketed by inaccessible guard pages and its slack is canary-filled, so an adjacent over- or under-flow traps or is caught on destroy. This is a memory-safety bug-catcher, not a confidentiality control: it does nothing against a privileged reader of process memory.
Emergency wipe (opt-in). Live buffers are registered so a single WipeAllSecrets call zeroes every one at once. secmem installs no signal handler itself: call WipeAllSecrets from your own shutdown or panic handler, or opt into InstallTerminationWipe to wipe on termination signals.
Lifecycle ¶
Create a buffer, defer its destruction immediately, and touch the plaintext only inside a borrowing closure:
buf, err := secmem.NewBuffer(rawBytes)
if err != nil {
return fmt.Errorf("create secure buffer: %w", err)
}
defer buf.Destroy() // always defer immediately after creation
err = buf.WithBytesErr(func(borrowed []byte) error {
// borrowed is valid ONLY inside this closure; never store it,
// send it to a goroutine, or copy it into an escaping slice.
return doSomethingWith(borrowed)
})
Scope and ScopeWith bind a buffer's lifetime to a function, wiping it on return:
err = secmem.Scope(len(rawBytes), func(buf *secmem.SecureBuffer) error {
if _, err := buf.CopyIn(rawBytes, 0); err != nil {
return fmt.Errorf("write: %w", err)
}
return doSomethingWith(buf)
})
RLIMIT_MEMLOCK budget ¶
Each SecureBuffer locks at least one page (4 KiB on amd64). The default RLIMIT_MEMLOCK on many Linux systems is 64 KiB, which allows only about six to ten concurrent buffers before allocation fails. A process that holds one buffer per live secret should raise the limit once, before its first allocation — either through the OS:
# /etc/security/limits.d/secmem.conf someuser soft memlock 262144 someuser hard memlock 262144
or programmatically with EnsureMemlockLimit at startup. Raising the soft limit up to the hard limit needs no privilege; raising the hard limit needs CAP_SYS_RESOURCE. Over-budget allocations return an error — they never panic.
Reachability ¶
Secret bytes are outside the Go heap, so the garbage collector never scans, moves, or copies them. As a last resort, a buffer whose Destroy was forgotten is wiped when it becomes unreachable, and a warning is logged to flag the oversight; correct code always defers Destroy rather than relying on this.
redact.go makes SecureBuffer, SecureArena, and ArenaSlot redact themselves under every formatting and logging path.
Without this, fmt reflects into the guarded, mmap'd struct — and rather than dumping plaintext, it hits a hardware fault reflecting past the guard pages: fmt.Printf("%v", buf), slog.Any("k", buf), %s, %+v, %#v, and %x all crash the process. Implementing fmt.Formatter routes every verb through the same fixed redaction; Stringer/GoStringer/slog.LogValuer cover the non-fmt callers (the standard log package, an error wrapped with %w whose %v chain includes a buffer, ...).
The redaction is the fixed "[REDACTED]" constant already used by Secret — it reads no fields, so it never locks, never touches the guarded region, and is safe to call even from inside a WithBytes/WithBytesErr callback.
SecureBuffer and ArenaSlot use VALUE receivers so both the pointer and an accidental dereference redact — a value copy would otherwise escape these pointer-receiver-only APIs and fall through to raw reflection. This is safe specifically because the methods read no fields: the struct copy the value receiver triggers is never inspected. SecureArena can't take a value receiver directly — its slot-bookkeeping mutex is a value sync.Mutex that go vet's copylocks check would flag on any value-receiver method declared directly on SecureArena — so it embeds the zero-size [arenaRedactor] instead: an embedded type's methods promote into both the value and pointer method sets of the embedding struct without copying it, so redaction covers both forms while copylocks still correctly flags any accidental SecureArena value copy elsewhere in the codebase.
Go prototype for the AMD64 scrub-frame assembly. The implementation lives in scrubframe_amd64.s.
Assembly prototypes for AMD64 cryptographic memory wipe routines. Implementations live in wipe_amd64.s.
AMD64 CPU feature detection and high-level wipe helpers. Separated from wipe_amd64.go to keep assembly prototypes minimal.
Example ¶
The package-level example shows the core lifecycle: create, defer Destroy immediately, and touch the plaintext only inside a borrowing closure.
package main
import (
"fmt"
"github.com/deadpoets/secmem"
)
func main() {
key := []byte("super-secret-signing-key")
buf, err := secmem.NewBuffer(key) // key is wiped after the copy
if err != nil {
// On a platform with no lockable off-heap memory this is
// ErrNoSecureMemory unless WithInsecureFallback() is passed.
fmt.Println("alloc:", err)
return
}
defer func() { _ = buf.Destroy() }()
err = buf.WithBytesErr(func(borrowed []byte) error {
// borrowed is valid ONLY here — never store it or send it elsewhere.
fmt.Println("secret length:", len(borrowed))
return nil
})
if err != nil {
fmt.Println("access:", err)
}
}
Output: secret length: 24
Index ¶
- Variables
- func AssertRuntimeSecret() error
- func DisableCoreDumps() error
- func EnsureMemlockLimit(bytes uint64) (achieved uint64, err error)
- func HasCLFLUSHOPT() bool
- func InstallTerminationWipe(signals ...os.Signal) (uninstall func())
- func RuntimeSecretActive() bool
- func RuntimeSecretEnforced() bool
- func Scope(size int, fn func(*SecureBuffer) error) (retErr error)
- func ScopeWith(ctor func() (*SecureBuffer, error), fn func(*SecureBuffer) error) (retErr error)
- func Scrub(fn func())
- func ScrubErr(fn func() error) (err error)
- func SecureWipe(b []byte)
- func WipeAllSecrets() error
- type ArenaSlot
- func (s ArenaSlot) Format(f fmt.State, _ rune)
- func (s ArenaSlot) GoString() string
- func (s *ArenaSlot) Index() int
- func (s *ArenaSlot) IsLive() bool
- func (s ArenaSlot) LogValue() slog.Value
- func (s *ArenaSlot) Release() error
- func (s ArenaSlot) String() string
- func (s *ArenaSlot) WithBytes(fn func([]byte)) error
- func (s *ArenaSlot) WithBytesErr(fn func([]byte) error) error
- type Capabilities
- type HardenLevel
- type Option
- type Secret
- func (s Secret) ConstantTimeEqual(other Secret) bool
- func (s Secret) Destroy() error
- func (s Secret) GoString() string
- func (s Secret) LogValue() slog.Value
- func (s Secret) MarshalJSON() ([]byte, error)
- func (s Secret) MarshalText() ([]byte, error)
- func (s Secret) String() string
- func (s Secret) WithBytes(fn func([]byte)) error
- func (s Secret) WriteTo(w io.Writer) (int64, error)
- type SecureArena
- func (a *SecureArena) Acquire() (*ArenaSlot, error)
- func (a *SecureArena) Cap() int
- func (a *SecureArena) Capabilities() Capabilities
- func (a *SecureArena) Destroy() error
- func (SecureArena) Format(f fmt.State, _ rune)
- func (SecureArena) GoString() string
- func (a *SecureArena) IsDestroyed() bool
- func (a *SecureArena) LiveCount() int
- func (SecureArena) LogValue() slog.Value
- func (a *SecureArena) ReadOnly() error
- func (a *SecureArena) ReadWrite() error
- func (a *SecureArena) SlotSize() int
- func (SecureArena) String() string
- type SecureBuffer
- func NewBuffer(raw []byte, opts ...Option) (*SecureBuffer, error)
- func NewBufferFromReader(r io.Reader, size int, opts ...Option) (*SecureBuffer, int64, error)
- func NewEmptyBuffer(size int, opts ...Option) (*SecureBuffer, error)
- func NewSyscallSafeBuffer(raw []byte, opts ...Option) (*SecureBuffer, error)
- func (s *SecureBuffer) ByteAt(i int) (byte, error)
- func (s *SecureBuffer) Capabilities() Capabilities
- func (s *SecureBuffer) ConstantTimeEqual(other []byte) (bool, error)
- func (s *SecureBuffer) CopyIn(src []byte, dstOffset int) (int, error)
- func (s *SecureBuffer) CopyOut(dst []byte, srcOffset int) (int, error)
- func (s *SecureBuffer) Destroy() error
- func (s *SecureBuffer) ExposeString() (string, error)
- func (s SecureBuffer) Format(f fmt.State, _ rune)
- func (s SecureBuffer) GoString() string
- func (s *SecureBuffer) IsDestroyed() bool
- func (s *SecureBuffer) IsSealed() bool
- func (s *SecureBuffer) Len() int
- func (s SecureBuffer) LogValue() slog.Value
- func (s *SecureBuffer) MappedLen() int
- func (s *SecureBuffer) ReadFrom(r io.Reader) (int64, error)
- func (s *SecureBuffer) ReadOnly() error
- func (s *SecureBuffer) ReadWrite() error
- func (s *SecureBuffer) Seal() error
- func (s *SecureBuffer) SetByteAt(i int, v byte) error
- func (s SecureBuffer) String() string
- func (s *SecureBuffer) Truncate(n int) error
- func (s *SecureBuffer) Unseal() error
- func (s *SecureBuffer) WithBytes(fn func([]byte)) error
- func (s *SecureBuffer) WithBytesErr(fn func([]byte) error) error
- func (s *SecureBuffer) WriteTo(w io.Writer) (int64, error)
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ErrArenaDestroyed = errors.New("secmem: secure arena has been destroyed")
ErrArenaDestroyed is returned by SecureArena and ArenaSlot methods after the arena has been destroyed via Destroy().
var ErrArenaFull = errors.New("secmem: secure arena is full — no free slots")
ErrArenaFull is returned by SecureArena.Acquire when all slots are in use.
var ErrCanaryViolation = errors.New(
"secmem: canary violation — memory adjacent to a secret was overwritten (out-of-bounds write bug in this process)")
ErrCanaryViolation is returned by Destroy (and ArenaSlot.Release) when the canary slack adjacent to the secret was overwritten: some code in this process wrote past the end of a buffer or slot. The wipe and release complete regardless — the violation is reported, never left mapped.
This is a memory-safety bug report, not a security breach by itself: treat it like a failed assertion. Find and fix the overflow; do not suppress the error.
var ErrDestroyed = errors.New("secmem: secure buffer has been destroyed")
ErrDestroyed is returned by SecureBuffer methods after the buffer has been destroyed. It is the canonical sentinel for the destroyed/wiped state.
var ErrNoSecureMemory = errors.New(
"secmem: no lockable off-heap memory on this platform — refusing the unprotected heap (opt in with WithInsecureFallback)")
ErrNoSecureMemory is returned by constructors on platforms with no lockable off-heap memory (everything except linux, darwin, and windows). secmem refuses to place secrets on the unprotected Go heap; a caller that accepts that exposure must say so explicitly with WithInsecureFallback.
var ErrReadOnly = errors.New("secmem: secure buffer is read-only — call ReadWrite before mutating")
ErrReadOnly is returned by the mutating methods (CopyIn, SetByteAt, Truncate, ReadFrom) when the buffer is in the read-only (PROT_READ) state set by SecureBuffer.ReadOnly. Call SecureBuffer.ReadWrite before mutating.
The guard is a memory-safety boundary: a mutating method that wrote through to the PROT_READ page would fault the process, so secmem refuses the write at the API boundary instead — misuse returns an error, it never crashes.
var ErrRuntimeSecretInactive = errors.New(
"runtime/secret erasure inactive: built without GOEXPERIMENT=runtimesecret on a supported platform")
ErrRuntimeSecretInactive indicates the process was built without GOEXPERIMENT=runtimesecret on a platform that supports it — the register/stack/heap erasure layer (Scrub) is therefore NOT active and only the legacy best-effort frame scrub is in force.
var ErrSealed = errors.New("secmem: secure buffer is sealed")
ErrSealed is returned by SecureBuffer access methods when the buffer is in the sealed (PROT_NONE) state. Call SecureBuffer.Unseal before accessing.
var ErrSlotReleased = errors.New("secmem: arena slot has been released")
ErrSlotReleased is returned by ArenaSlot methods after the slot has been released via Release(). Calling Release again is a no-op (idempotent).
Functions ¶
func AssertRuntimeSecret ¶
func AssertRuntimeSecret() error
AssertRuntimeSecret enforces the memory-hardening posture at startup (fail-closed).
- On a supported platform (linux/amd64|arm64) it returns ErrRuntimeSecretInactive when the runtimesecret experiment was not compiled in. Production entrypoints MUST treat this as fatal: a binary shipping without the erasure layer on a platform that supports it is a misbuild (build with `GOEXPERIMENT=runtimesecret`, as the Taskfile does).
- On unsupported platforms (Windows, Darwin, non-amd64/arm64) the legacy best-effort scrub layer is the expected posture, so it returns nil.
Call early in main(), after HardenProcess. Callers that must tolerate an inactive layer (e.g. a developer running an ad-hoc `go build` without the experiment) can downgrade the error to a warning, but the default posture is to refuse to start.
func DisableCoreDumps ¶
func DisableCoreDumps() error
DisableCoreDumps sets the process core-dump size limit to zero (setrlimit(RLIMIT_CORE, 0), soft AND hard) on Linux and Darwin.
It is the blunt backstop to the surgical per-mapping protections (MADV_DONTDUMP, memfd_secret): those cover only secmem's own mappings and can silently not apply; RLIMIT_CORE=0 stops the entire process from dumping. Setting the hard limit is deliberate and IRREVERSIBLE without privilege — a compromised process cannot quietly re-enable dumps.
Never called implicitly: changing a process rlimit is the application's decision, not the library's. On Windows there is no core-dump rlimit and this returns errors.ErrUnsupported — the per-allocation WER dump exclusion is applied automatically by the allocator instead.
func EnsureMemlockLimit ¶
EnsureMemlockLimit raises the locked-memory budget to at least bytes, returning the value actually achieved.
Each SecureBuffer locks a page-rounded minimum (typically 4 KiB), and the default RLIMIT_MEMLOCK on Linux is 64 KiB — roughly a dozen buffers before NewBuffer starts returning mlock errors. A server holding one buffer per live secret WILL hit this; call EnsureMemlockLimit once at startup, before the first allocation.
Honesty notes: raising the soft limit up to the hard limit needs no privilege; raising the hard limit needs CAP_SYS_RESOURCE (or root). When the request cannot be met the function raises the soft limit as far as the hard limit allows and returns that value together with a non-nil error — it never silently under-delivers. On Windows the equivalent budget is the process minimum working-set size, adjusted via SetProcessWorkingSetSizeEx.
func HasCLFLUSHOPT ¶
func HasCLFLUSHOPT() bool
HasCLFLUSHOPT reports whether the CPU supports the CLFLUSHOPT instruction. Detected once via CPUID at package init; cached for the process lifetime. When true, secureWipe uses pipelined CLFLUSHOPT (Intel Skylake+ / AMD Zen+). When false, it falls back to strictly-ordered CLFLUSH (universally supported).
func InstallTerminationWipe ¶
InstallTerminationWipe installs a cooperative signal handler that calls WipeAllSecrets when the process receives a termination signal, then lets the process terminate as it otherwise would. It returns a function that uninstalls the handler.
This is opt-in: importing secmem installs nothing. Call it once early in main if you want automatic wiping on Ctrl-C / kill:
defer secmem.InstallTerminationWipe()()
With no arguments it handles os.Interrupt (SIGINT) and SIGTERM. Pass explicit signals to override — note that adding SIGQUIT both suppresses Go's default SIGQUIT goroutine dump and re-raises to a core-dumping disposition.
It does NOT clobber other signal handling. It registers its own channel with signal.Notify (which is additive: a handler you installed with signal.Notify still receives the signal too). On the signal it wipes, deregisters ONLY its own channel with signal.Stop — never the process-global signal.Reset/signal.Ignore — and re-raises the signal: if secmem is the only handler the default disposition is restored and the process terminates; if you have your own handler it receives the signal and decides when to exit, so secmem never forces the exit out from under your graceful shutdown.
If you already have a termination handler, prefer calling WipeAllSecrets from inside it rather than using this installer.
func RuntimeSecretActive ¶
func RuntimeSecretActive() bool
RuntimeSecretActive reports whether runtime/secret erasure is active. On the legacy path it is always false; Scrub uses best-effort frame scrubbing.
func RuntimeSecretEnforced ¶
func RuntimeSecretEnforced() bool
RuntimeSecretEnforced reports whether an inactive runtime/secret erasure layer on a supported platform should be treated as fatal at startup.
In production (!devmode) builds it returns true: a binary that ships without the erasure layer on a platform that supports it is a misbuild and must fail-closed. The devmode counterpart returns false so developer and test builds (which legitimately run without GOEXPERIMENT=runtimesecret) only warn.
Pair with AssertRuntimeSecret in main():
if err := secmem.AssertRuntimeSecret(); err != nil {
if secmem.RuntimeSecretEnforced() {
// fatal
}
// warn
}
func Scope ¶
func Scope(size int, fn func(*SecureBuffer) error) (retErr error)
Scope creates a zero-filled SecureBuffer of the given size, calls fn, then destroys the buffer deterministically.
Destroy() errors are joined to fn's return value via errors.Join:
- fn error only → returned as-is
- Destroy error only → returned as-is
- Both errors → errors.Join(fn error, Destroy error)
This ensures that a failed Munmap is never silently discarded.
Example ¶
Scope binds a buffer's lifetime to a function and wipes it on return, even on panic.
package main
import (
"fmt"
"github.com/deadpoets/secmem"
)
func main() {
err := secmem.Scope(32, func(buf *secmem.SecureBuffer) error {
if _, err := buf.CopyIn([]byte("derived-key-material"), 0); err != nil {
return err
}
return buf.WithBytesErr(func(b []byte) error {
fmt.Println("filled", buf.Len(), "bytes")
return nil
})
})
if err != nil {
fmt.Println("scope:", err)
}
}
Output: filled 32 bytes
func ScopeWith ¶
func ScopeWith(ctor func() (*SecureBuffer, error), fn func(*SecureBuffer) error) (retErr error)
ScopeWith creates a SecureBuffer using the provided constructor, calls fn, then destroys the buffer deterministically.
If ctor returns an error, fn is not called and the error is returned. Destroy() errors are joined to fn's return value (same semantics as Scope).
Example:
err := secmem.ScopeWith(
func() (*secmem.SecureBuffer, error) {
return secmem.NewSyscallSafeBuffer(rawKey)
},
func(buf *secmem.SecureBuffer) error {
return buf.WithBytesErr(func(key []byte) error {
block, err := aes.NewCipher(key)
if err != nil { return err }
block.Encrypt(dst, src)
return nil
})
},
)
func Scrub ¶
func Scrub(fn func())
Scrub runs fn and then best-effort scrubs the stack region fn used.
On linux/amd64 and linux/arm64 built with GOEXPERIMENT=runtimesecret, Scrub is backed by runtime/secret and erases the registers, stack, and heap of fn's entire call tree with runtime cooperation. This file is the fallback used on every other build; it cannot match that, and scrubs only fn's stack frame via assembly (REP STOSB + SFENCE on amd64; a no-op on other architectures).
Why the wipe runs twice ¶
wipeScratchFrameFull zeroes a 32 KiB region by allocating it as its own (non-inlined) stack frame. Goroutines start with an 8 KiB stack, so on a shallow call that allocation triggers a stack copy (morestack): a single deferred wipe would then run on the RELOCATED stack, zeroing the fresh copy while fn's real residue sits on the old segment the runtime just freed — untouched. Calling the wipe once on entry forces any growth to happen BEFORE fn writes a secret (nothing sensitive is on the abandoned copy) and pre-cleans the band; the deferred call is then guaranteed to run in place.
Best-effort limits (stated honestly) ¶
Even with headroom reserved, this scrubs only the 32 KiB band below fn's return point, and only stack memory. It does NOT cover:
- a call tree deeper than 32 KiB (the tail survives);
- a stack relocation triggered inside fn if fn exceeds the reserved band;
- a GC stack-shrink that frees fn's segment before the wipe (asynchronous, runtime-owned, and unreachable from Go);
- CPU or vector registers.
None of these are fixable in pure Go without runtime support — the runtime/secret path handles them, which is why it is the primary path. Keep fn shallow and keep secrets in a SecureBuffer so there is little residue to miss.
Panics propagate; the frame is still scrubbed during unwind via the deferred wipe. Scrub(nil) is a no-op.
func ScrubErr ¶
ScrubErr is Scrub for a fn that returns an error. ScrubErr(nil) is a no-op that returns nil.
func SecureWipe ¶
func SecureWipe(b []byte)
SecureWipe zeroes the bytes of b using the platform's best available wiping primitive (assembly-backed where present, compiler-barrier otherwise).
It is the package's only exported wipe, available in every build and on every platform. Use it for transient secret intermediates that are not held in a SecureBuffer; for mmap'd SecureBuffer memory, Destroy applies the full architectural wipe.
func WipeAllSecrets ¶
func WipeAllSecrets() error
WipeAllSecrets immediately wipes the contents of every live SecureBuffer and SecureArena registered in this process, then returns. It is an emergency / pre-termination wipe you can call from your OWN shutdown or panic-recovery handler:
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGTERM, os.Interrupt)
go func() { <-sig; _ = secmem.WipeAllSecrets(); os.Exit(0) }()
Semantics:
- Regions are wiped in place but NOT unmapped: the process is assumed to be terminating and the kernel reclaims the mappings on exit. Unmapping while another goroutine might still hold a buffer would risk a use-after-munmap fault, so a read of an already-wiped buffer returns zeroed bytes, never a fault.
- After this call every affected buffer is dead — its secret is gone. If the process keeps running, the wiped mappings linger until exit rather than being freed; this is a one-way emergency wipe, not a reusable clear.
- Safe to call concurrently with Destroy and from multiple goroutines; each region is wiped exactly once.
secmem installs NO signal handler on its own. For automatic wiping on termination signals, call InstallTerminationWipe once at startup, or wire WipeAllSecrets into your own handler as above.
Types ¶
type ArenaSlot ¶
type ArenaSlot struct {
// contains filtered or unexported fields
}
ArenaSlot is a handle to one fixed-size slot in a SecureArena.
Access secret data via ArenaSlot.WithBytes or ArenaSlot.WithBytesErr. Return the slot to the pool with ArenaSlot.Release.
A slot should be owned by a single goroutine at a time; concurrent access to the same slot from multiple goroutines is not internally synchronized.
func (ArenaSlot) Format ¶
Format implements fmt.Formatter, so every verb redacts.
func (ArenaSlot) GoString ¶
GoString implements fmt.GoStringer, so %#v also redacts.
func (*ArenaSlot) IsLive ¶
IsLive reports whether the slot is currently acquired (not yet released).
func (ArenaSlot) LogValue ¶
LogValue implements slog.LogValuer, so slog.Any("key", slot) redacts.
func (*ArenaSlot) Release ¶
Release wipes the slot's byte region and returns it to the arena pool.
After Release, all subsequent WithBytes/WithBytesErr calls return ErrSlotReleased. Calling Release again is a no-op (idempotent).
The wipe happens BEFORE the slot is marked free (SA-1 fix): this ensures the next Acquire cannot read stale secret data from this slot.
Release also verifies the slot's trailing canary strip. If code overflowed this slot, Release returns ErrCanaryViolation — the wipe, the re-arming of the strip, and the return of the slot to the pool all complete regardless; the error is a bug report, not a refusal.
If the arena is read-only (SecureArena.ReadOnly), Release returns ErrReadOnly without wiping — the wipe is a write the PROT_READ slab would fault on. The slot stays in use; call SecureArena.ReadWrite first, or let SecureArena.Destroy wipe it (it makes the slab writable internally).
func (ArenaSlot) String ¶
String implements fmt.Stringer. It always returns "[REDACTED]".
func (*ArenaSlot) WithBytes ¶
WithBytes calls fn with the slot's byte region.
The slice is valid ONLY for the duration of fn. Never store or pass it to a goroutine. Returns ErrSlotReleased if the slot has been released. Returns ErrArenaDestroyed if the arena has been destroyed.
func (*ArenaSlot) WithBytesErr ¶
WithBytesErr is like ArenaSlot.WithBytes but fn may return an error.
type Capabilities ¶
type Capabilities struct {
// GOOS and GOARCH identify the build this report describes.
GOOS, GOARCH string
// OffHeap reports that the memory lives outside the Go heap (mmap or
// VirtualAlloc, not make([]byte)) — the GC never scans, moves, or copies it.
OffHeap bool
// Mlocked reports that the pages are excluded from swap (mlock /
// VirtualLock, or memfd_secret's kernel-enforced equivalent).
Mlocked bool
// MemfdSecret reports kernel isolation: the pages are invisible to
// /proc/<pid>/mem, ptrace, and other readers of process memory.
// Linux amd64/arm64 with kernel 5.14+ and CONFIG_SECRETMEM only, and can
// fail per-allocation.
MemfdSecret bool
// NoDump reports the allocation is excluded from crash dumps: on Linux,
// MADV_DONTDUMP took effect or the memory is memfd_secret-backed; on
// Windows, WerRegisterExcludedMemoryBlock succeeded — which covers
// WER-generated dumps only (a debugger-driven dump still captures the
// pages; Seal's cipher covers the dormant window there). Process-wide
// exclusion ([HardenProcess], [DisableCoreDumps]) is NOT reflected here.
NoDump bool
// NoFork reports MADV_DONTFORK took effect: forked children do not
// inherit the mapping.
NoFork bool
// FlushedWipe reports the destroy-time wipe is architecture assembly
// with a cache-line flush (amd64 CLFLUSH/CLFLUSHOPT, arm64 DC CIVAC).
// When false the wipe is a constant-time store loop only — the zeros are
// written, but lines may linger in cache.
FlushedWipe bool
// RegisterScrub reports runtime/secret erasure is active
// (GOEXPERIMENT=runtimesecret on a supported platform): [Scrub] erases
// the registers, stack, and heap of its callback's entire call tree.
// When false, Scrub is a best-effort stack-frame wipe.
RegisterScrub bool
// GuardPages reports PROT_NONE guard pages bracket the mapping so a
// linear over/under-flow traps (SIGSEGV / access violation) instead of
// silently touching adjacent memory. A memory-safety bug-catcher, not a
// secrecy mechanism. False only on the insecure heap fallback.
GuardPages bool
// Insecure reports the memory is plain heap — NO protection is in force.
// True only on platforms with no lockable off-heap memory.
Insecure bool
}
Capabilities describes which memory protections are in force, either for the platform as a whole (Probe) or for one specific allocation (SecureBuffer.Capabilities, SecureArena.Capabilities).
Every field is stated per the platform guarantee matrix in the package documentation. A false field is not an error — it is the truth about what this build, kernel, and allocation provide. Use Capabilities.Warnings to enumerate the protections NOT in force.
func Probe ¶
func Probe() Capabilities
Probe reports what this platform can do, by performing one real one-byte allocation through the same path the constructors use and reporting what it received. Call it once at startup to log or gate on the protections in force; per-allocation truth is SecureBuffer.Capabilities.
Probe is not cached: it reflects the kernel and limits at the moment of the call. If even the probe allocation fails, the report is fully degraded (every protection false) — treat that as this platform providing nothing.
Example ¶
Probe reports what the running platform can do; use it once at startup to log or gate on the protections in force. The output varies by platform, so this example only demonstrates the call.
package main
import (
"fmt"
"github.com/deadpoets/secmem"
)
func main() {
caps := secmem.Probe()
if caps.Insecure {
// Refuse to start, or accept the risk deliberately.
fmt.Println("no secure memory on this platform")
}
for _, w := range caps.Warnings() {
_ = w // e.g. slog.Warn("secmem", "degradation", w)
}
}
Output:
func (Capabilities) String ¶
func (c Capabilities) String() string
String returns a compact one-line summary: the build identity, the protections in force, and the protections missing. It never prints secret material — Capabilities holds none.
func (Capabilities) Warnings ¶
func (c Capabilities) Warnings() []string
Warnings returns a human-readable line for every protection NOT in force, most severe first. An empty slice means every protection this library can provide is active. Intended for startup logging:
for _, w := range secmem.Probe().Warnings() {
slog.Warn("secmem", "degradation", w)
}
type HardenLevel ¶
type HardenLevel int
HardenLevel is a bitmask describing which hardening mitigations were applied.
const ( // HardenNone means no hardening was applied (unsupported platform or unavailable). HardenNone HardenLevel = 0 // HardenNoDump indicates PR_SET_DUMPABLE=0 was set — core dumps disabled. HardenNoDump HardenLevel = 1 << iota // HardenNoNewPriv indicates PR_SET_NO_NEW_PRIVS=1 was set — no privilege escalation. HardenNoNewPriv // HardenSeccomp indicates a seccomp BPF filter was loaded (reserved; not yet implemented). HardenSeccomp // HardenStrictHandles indicates Windows strict handle checking is in // force: use of a stale or invalid handle raises an exception instead of // silently succeeding against whatever object now owns the handle value. HardenStrictHandles // HardenNoDynamicCode indicates Windows Arbitrary Code Guard is in force: // the process can no longer create executable memory or make writable // memory executable. Pure-Go binaries never need either; JIT-based cgo // dependencies would break, which is why HardenProcess is opt-in. HardenNoDynamicCode )
func HardenProcess ¶
func HardenProcess(_ context.Context) (HardenLevel, error)
HardenProcess applies process-level hardening mitigations. Returns the bitmask of mitigations that were successfully applied.
Call this early in main() before any secret loading or privilege acquisition. Applied per platform:
- Linux: PR_SET_DUMPABLE=0 (no core dumps, no ptrace attach by non-privileged peers) and PR_SET_NO_NEW_PRIVS=1.
- Windows: strict handle checks and Arbitrary Code Guard via SetProcessMitigationPolicy. Both are IRREVERSIBLE for the process lifetime — that is their value. ACG is incompatible with anything that generates code at runtime (pure Go never does; a JIT inside a cgo dependency would).
- Elsewhere: no-op returning (HardenNone, nil).
ctx is reserved for future cancellation; currently unused but required by convention.
type Option ¶
type Option func(*config)
Option configures a constructor (NewBuffer, NewEmptyBuffer, NewSyscallSafeBuffer, NewBufferFromReader, NewArena).
func WithInsecureFallback ¶
func WithInsecureFallback() Option
WithInsecureFallback permits a constructor to fall back to plain Go heap memory on platforms with no lockable off-heap memory (everything except linux, darwin, and windows).
It is a permission, not a demand: on supported platforms it changes nothing. On unsupported platforms it converts the constructor's ErrNoSecureMemory failure into a heap allocation with NO protection — not locked, swappable, GC-visible, included in core dumps. The resulting Capabilities report Insecure=true, Warnings() leads with the exposure, and a one-time slog warning fires on first use.
type Secret ¶
type Secret struct {
// contains filtered or unexported fields
}
Secret wraps a SecureBuffer as a value that is safe to embed in structs that get logged, formatted, or marshalled: every such path produces "[REDACTED]" instead of the contents. Access the real bytes only through Secret.WithBytes.
Methods use a value receiver so fmt and encoding/json pick up the redaction whether a field is Secret or *Secret. The value holds a pointer to the underlying buffer, so COPIES SHARE ONE BACKING STORE AND ONE DESTROY: destroying any copy destroys them all. This is the sharp edge of the design — treat a Secret like the *SecureBuffer it wraps, not like a self-contained value.
The zero value behaves like a destroyed Secret: access returns ErrDestroyed, redaction still works, nothing panics.
UnmarshalJSON and UnmarshalText are intentionally NOT provided: they would silently land plaintext on the GC heap during decoding. To ingest a secret from a decoded document, unmarshal into a []byte and hand it to NewSecret, which wipes the input — the transient heap copy is then explicit at the call site instead of hidden inside a decoder.
Example ¶
Secret is safe to embed in structs that get logged, formatted, or marshalled: every such path yields the redaction sentinel, never the contents.
package main
import (
"fmt"
"github.com/deadpoets/secmem"
)
func main() {
token, err := secmem.NewSecret([]byte("ghp_realtokenvalue"))
if err != nil {
fmt.Println("secret:", err)
return
}
defer func() { _ = token.Destroy() }()
type Config struct {
User string
Token secmem.Secret
}
cfg := Config{User: "svc-account", Token: token}
// Formatting the whole struct cannot leak the token.
fmt.Printf("%v\n", cfg)
}
Output: {svc-account [REDACTED]}
func NewSecret ¶
NewSecret copies b into hardened memory and returns the Secret. b is wiped after the copy (defense-in-depth) — the caller must not reuse it.
Errors are those of NewBuffer: empty input, allocation or mlock failure, and ErrNoSecureMemory on platforms without secure memory unless WithInsecureFallback is passed.
func (Secret) ConstantTimeEqual ¶
ConstantTimeEqual reports whether s and other hold the same bytes, in constant time with respect to the contents — the same name and guarantee as SecureBuffer.ConstantTimeEqual. Differing lengths return false immediately — the length itself is not concealed. Two Secrets sharing one backing store (value copies of each other) are equal without comparing bytes.
A zero-value or destroyed Secret is equal to nothing, including another zero-value Secret: there are no bytes to compare, and false is the conservative answer.
func (Secret) Destroy ¶
Destroy wipes and releases the underlying buffer. Every value copy of this Secret shares that buffer, so all of them are destroyed with it. Destroying a zero-value or already-destroyed Secret is a no-op returning nil (idempotent, matching SecureBuffer.Destroy).
func (Secret) GoString ¶
GoString implements fmt.GoStringer, covering the %#v verb. It always returns "[REDACTED]".
func (Secret) LogValue ¶
LogValue implements slog.LogValuer. It always returns "[REDACTED]".
func (Secret) MarshalJSON ¶
MarshalJSON implements json.Marshaler. It always returns the JSON string "[REDACTED]".
func (Secret) MarshalText ¶
MarshalText implements encoding.TextMarshaler. It always returns "[REDACTED]" — a Secret never serializes its contents.
func (Secret) String ¶
String implements fmt.Stringer. It always returns "[REDACTED]".
func (Secret) WithBytes ¶
WithBytes calls fn with the secret's bytes. The slice is valid only for the duration of fn and must not be retained — see SecureBuffer.WithBytes for the full borrowing contract (locking, non-reentrancy).
Returns ErrDestroyed on a zero-value or destroyed Secret.
func (Secret) WriteTo ¶
WriteTo implements io.WriterTo. It writes the PLAINTEXT secret to w — this is the deliberate egress path, for handing the secret to a socket, pipe, or child process stdin. See SecureBuffer.WriteTo for the locking and wiping contract. Do not point it at a log.
type SecureArena ¶
type SecureArena struct {
// contains filtered or unexported fields
}
SecureArena is a single mmap'd slab providing N fixed-size secret slots.
Create with NewArena. Acquire slots with SecureArena.Acquire. Release individual slots with ArenaSlot.Release. Wipe and free the entire slab with SecureArena.Destroy.
Destroy is idempotent and goroutine-safe. After Destroy, all subsequent Acquire calls return ErrArenaDestroyed.
Example ¶
SecureArena pools many small same-size secrets in one locked slab — O(1) OS overhead where one buffer per secret would exhaust RLIMIT_MEMLOCK.
package main
import (
"fmt"
"github.com/deadpoets/secmem"
)
func main() {
arena, err := secmem.NewArena(32, 128) // 128 slots of 32 bytes
if err != nil {
fmt.Println("arena:", err)
return
}
defer func() { _ = arena.Destroy() }()
slot, err := arena.Acquire()
if err != nil {
fmt.Println("acquire:", err)
return
}
_ = slot.WithBytes(func(b []byte) {
copy(b, []byte("per-session-key"))
})
fmt.Println("live slots:", arena.LiveCount())
_ = slot.Release() // wiped and returned to the pool
fmt.Println("live slots:", arena.LiveCount())
}
Output: live slots: 1 live slots: 0
func NewArena ¶
func NewArena(slotSize, count int, opts ...Option) (*SecureArena, error)
NewArena creates a SecureArena with count fixed-size slots, each of slotSize bytes.
The underlying slab is one contiguous guarded mmap region: PROT_NONE guard pages bracket the slab's two outer edges, and each slot is followed by a canaryLen-byte canary strip, verified on ArenaSlot.Release and on SecureArena.Destroy. There are deliberately NO guard pages between slots (a page per gap would defeat the slab's O(1)-OS-overhead purpose); the strips detect inter-slot overflows instead of trapping them. A single emergency janitor registration covers all slots.
slotSize and count must both be > 0.
Common errors: EPERM / ENOMEM from mlock (RLIMIT_MEMLOCK exceeded). On platforms with no lockable off-heap memory the error is ErrNoSecureMemory unless WithInsecureFallback is passed.
func (*SecureArena) Acquire ¶
func (a *SecureArena) Acquire() (*ArenaSlot, error)
Acquire returns the next free slot for exclusive use by the caller.
Returns ErrArenaFull if all slots are occupied. Returns ErrArenaDestroyed if the arena has been destroyed.
func (*SecureArena) Cap ¶
func (a *SecureArena) Cap() int
Cap returns the total slot capacity of the arena.
func (*SecureArena) Capabilities ¶
func (a *SecureArena) Capabilities() Capabilities
Capabilities reports how this arena's slab is actually backed. Every slot shares the slab's single allocation, so one report covers them all.
The report is fixed at construction and remains valid after Destroy. A nil receiver reports a fully degraded posture.
func (*SecureArena) Destroy ¶
func (a *SecureArena) Destroy() error
Destroy wipes the entire slab and releases the mmap'd region.
Steps:
- Mark arena destroyed under alloc (blocks new Acquire).
- Acquire exclusive mu lock (waits for all in-flight callbacks to return).
- Wipe full raw region (REP STOSB + CLFLUSH on amd64).
- Madvise DONTNEED.
- Munlock + Munmap.
- Nil raw — makes IsDestroyed() = true and Destroy idempotent.
Destroy is idempotent and goroutine-safe.
func (*SecureArena) IsDestroyed ¶
func (a *SecureArena) IsDestroyed() bool
IsDestroyed reports whether the arena has been destroyed.
func (*SecureArena) LiveCount ¶
func (a *SecureArena) LiveCount() int
LiveCount returns the number of currently acquired (live) slots.
func (*SecureArena) ReadOnly ¶
func (a *SecureArena) ReadOnly() error
ReadOnly sets the entire slab to read-only (PROT_READ). Affects ALL slots — sub-page mprotect is not possible.
Call ReadWrite before releasing a slot: ArenaSlot.Release wipes the slot, which is a write, so it returns ErrReadOnly while the slab is read-only rather than faulting. SecureArena.Destroy needs no such call — its wipe path makes the slab writable first.
The exclusive lock is held to drain all in-flight WithBytes callbacks before the mprotect, preventing a SIGSEGV from a concurrent write hitting a PROT_READ page.
func (*SecureArena) ReadWrite ¶
func (a *SecureArena) ReadWrite() error
ReadWrite restores read-write access to the entire slab.
The exclusive lock is held to drain all in-flight callbacks before the mprotect (arena SB-3 equivalent fix).
func (*SecureArena) SlotSize ¶
func (a *SecureArena) SlotSize() int
SlotSize returns the usable bytes per slot.
type SecureBuffer ¶
type SecureBuffer struct {
// contains filtered or unexported fields
}
SecureBuffer holds sensitive data in a page-aligned, mlock'd, off-heap memory region. The region is allocated via mmap(MAP_ANON|MAP_PRIVATE) (or higher- privilege equivalents on supported platforms) and is invisible to the Go GC.
Callers MUST call SecureBuffer.Destroy explicitly. The AddCleanup fallback is a safety net only — it runs non-deterministically at GC time.
WARNING: Never retain a reference from SecureBuffer.WithBytes beyond the buffer's lifetime. After Destroy, the backing memory is unmapped; any retained slice becomes a dangling pointer.
func NewBuffer ¶
func NewBuffer(raw []byte, opts ...Option) (*SecureBuffer, error)
NewBuffer allocates a hardened memory region and copies raw into it.
The backing allocation is page-rounded (typically 4096 bytes), mlock'd, and invisible to the Go GC. raw is zeroed after the copy (defense-in-depth).
WARNING: raw is zeroed after copying. The caller must not reuse raw after this call. If the same secret must be used multiple times, copy it first.
Common errors: EPERM / ENOMEM from mlock (RLIMIT_MEMLOCK exceeded — check `ulimit -l` or systemd LimitMEMLOCK=). On platforms with no lockable off-heap memory the error is ErrNoSecureMemory unless WithInsecureFallback is passed.
func NewBufferFromReader ¶
NewBufferFromReader allocates a SecureBuffer of size bytes and fills it from r. The returned buffer may be partially filled if r returns fewer than size bytes; the returned count reports how many bytes were read.
func NewEmptyBuffer ¶
func NewEmptyBuffer(size int, opts ...Option) (*SecureBuffer, error)
NewEmptyBuffer allocates an mlock'd zero-filled region of exactly size bytes. Equivalent to NewBuffer(make([]byte, size)) without the intermediate heap copy.
func NewSyscallSafeBuffer ¶
func NewSyscallSafeBuffer(raw []byte, opts ...Option) (*SecureBuffer, error)
NewSyscallSafeBuffer allocates via MAP_ANON only (no memfd_secret attempt). Use this for ingestion paths where syscall arguments are read directly into the buffer — memfd_secret's extra isolation is not needed because the data arrives from a kernel-controlled channel.
func (*SecureBuffer) ByteAt ¶
func (s *SecureBuffer) ByteAt(i int) (byte, error)
ByteAt returns the byte at index i. Returns an error if i is out of range or the buffer has been destroyed.
Design note: a conventional implementation panics on a bad index; secmem returns an error instead, honoring the library's "no panics in library code" policy.
func (*SecureBuffer) Capabilities ¶
func (s *SecureBuffer) Capabilities() Capabilities
Capabilities reports how THIS buffer's allocation is actually backed — the honest per-allocation counterpart to Probe (memfd_secret can fail per-call and fall through to mmap+mlock; this reflects what happened).
The report is fixed at construction and remains valid after Destroy. A nil receiver reports a fully degraded posture.
func (*SecureBuffer) ConstantTimeEqual ¶
func (s *SecureBuffer) ConstantTimeEqual(other []byte) (bool, error)
ConstantTimeEqual performs a constant-time comparison of the buffer contents against other. Returns (false, nil) when lengths differ. Returns (false, ErrDestroyed) if the buffer has been destroyed.
func (*SecureBuffer) CopyIn ¶
func (s *SecureBuffer) CopyIn(src []byte, dstOffset int) (int, error)
CopyIn copies bytes from src into the buffer, starting at dstOffset. Returns the number of bytes written (min of len(src) and available space).
It is a random-access copy, not an io.Writer; the streaming counterpart is SecureBuffer.ReadFrom. The exclusive lock is held for the copy, serializing all concurrent writes and preventing races with ReadOnly/ReadWrite page-protection changes.
func (*SecureBuffer) CopyOut ¶
func (s *SecureBuffer) CopyOut(dst []byte, srcOffset int) (int, error)
CopyOut copies bytes from the buffer into dst, starting at srcOffset. Returns the number of bytes copied (min of len(dst) and available bytes).
It is a random-access copy, not an io.Reader; the streaming counterpart is SecureBuffer.WriteTo. The RLock is held for the entire copy. If dst is itself an off-heap region, no heap copy occurs. If dst is heap-allocated, the caller is responsible for wiping it.
func (*SecureBuffer) Destroy ¶
func (s *SecureBuffer) Destroy() error
Destroy performs an architectural wipe of the entire mapped region:
- Stop the AddCleanup fallback (prevents double-free).
- Mprotect(RW) — ensure the page is writable before wiping.
- secureWipeSlice — zero + CLFLUSH/CLFLUSHOPT + SFENCE/LFENCE.
- Madvise(DONTNEED) — release physical frames immediately.
- freeSecretMem — Munlock + Munmap / VirtualUnlock + VirtualFree.
- Nil out data and raw — makes Destroy idempotent.
- runtime.KeepAlive(s) — ensures the GC does not run the cleanup concurrently between Stop() and the wipe.
Destroy is idempotent and goroutine-safe. After Destroy, IsDestroyed() returns true and all subsequent method calls return ErrDestroyed.
func (*SecureBuffer) ExposeString ¶
func (s *SecureBuffer) ExposeString() (string, error)
ExposeString returns the buffer contents as a Go string, for third-party APIs that accept only strings. It is the weakest accessor in the package; prefer SecureBuffer.WithBytes or SecureBuffer.WithBytesErr whenever the consumer can take a []byte.
Because Go strings are immutable, the returned string is a copy of the secret that secmem can neither lock, exclude from core dumps, nor wipe. A fresh copy is made on every call. It is unaffected by Destroy and lives until the garbage collector reclaims it — and the GC does not zero reclaimed memory, so the bytes may linger until that memory is reused.
Keeping the returned string is safe — connection pools, loggers, and caches routinely do — but it extends the secret's lifetime beyond secmem's control. Do not use ExposeString for material whose exposure must end at Destroy.
The copy is taken under the read lock. On GOEXPERIMENT=runtimesecret builds it is a garbage-collector-tracked allocation, erased once nothing references it — best-effort timing, never a guarantee; a string you keep is never erased.
Returns ErrDestroyed if the buffer has been destroyed and ErrSealed if it is sealed.
func (SecureBuffer) Format ¶
func (s SecureBuffer) Format(f fmt.State, _ rune)
Format implements fmt.Formatter, so every verb (%v, %s, %x, %+v, %#v, ...) emits the redaction instead of reflecting into the guarded struct.
func (SecureBuffer) GoString ¶
func (s SecureBuffer) GoString() string
GoString implements fmt.GoStringer, so %#v also redacts.
func (*SecureBuffer) IsDestroyed ¶
func (s *SecureBuffer) IsDestroyed() bool
IsDestroyed reports whether the buffer has been destroyed.
func (*SecureBuffer) IsSealed ¶
func (s *SecureBuffer) IsSealed() bool
IsSealed reports whether the buffer is currently in the sealed (PROT_NONE) state.
func (*SecureBuffer) Len ¶
func (s *SecureBuffer) Len() int
Len returns the usable size of the buffer (the size requested by the caller). May be smaller than [MappedLen] due to page-rounding.
func (SecureBuffer) LogValue ¶
func (s SecureBuffer) LogValue() slog.Value
LogValue implements slog.LogValuer, so slog.Any("key", buf) redacts.
func (*SecureBuffer) MappedLen ¶
func (s *SecureBuffer) MappedLen() int
MappedLen returns the size of the locked secret area (the page-rounded region holding the data and its canary slack). Always a multiple of the OS page size (≥ Len). The PROT_NONE guard pages bracketing the area are NOT counted — they are reserved address space, not lockable memory.
func (*SecureBuffer) ReadFrom ¶
func (s *SecureBuffer) ReadFrom(r io.Reader) (int64, error)
ReadFrom implements io.ReaderFrom. Reads up to Len() bytes from r into the buffer. The exclusive lock is NOT held during io.ReadFull: data is read into a temporary heap slice first, then copied into secure memory under the lock.
This prevents a stalled network peer or slow pipe from blocking Destroy (including the emergency-wipe path). The temporary slice is wiped via secureWipeSlice after the copy regardless of outcome.
func (*SecureBuffer) ReadOnly ¶
func (s *SecureBuffer) ReadOnly() error
ReadOnly sets the buffer's memory protection to read-only. This prevents accidental overwrites once a secret is fully loaded. Call [ReadWrite] before [Destroy] to restore write access.
The exclusive lock is held to drain all in-flight Write/SetByteAt calls before the mprotect, preventing a SIGSEGV from a concurrent write hitting a PROT_READ page.
NOTE: Operates on the full page-rounded region; sub-page protection is not possible on any supported OS.
func (*SecureBuffer) ReadWrite ¶
func (s *SecureBuffer) ReadWrite() error
ReadWrite restores read-write access to the buffer. Must be called before [Destroy] if [ReadOnly] was previously applied.
The exclusive lock is held to drain all in-flight access before the mprotect.
func (*SecureBuffer) Seal ¶
func (s *SecureBuffer) Seal() error
Seal sets the buffer's memory protection to PROT_NONE, making any access (including speculative reads) cause a hardware fault. This is the hardened dormant state for long-lived secrets that are not actively being used.
On Windows, Seal additionally encrypts the contents in place with a KERNEL-HELD per-boot key (CryptProtectMemory): a full process memory dump taken while the buffer is sealed — procdump, Task Manager, a WER full dump, the hibernation file — contains ciphertext, and the key is not in the dump. This protects the sealed window only, and is not a defense against code executing inside the process (which can call CryptUnprotectMemory itself) nor against cold-boot RAM capture (the kernel's key is in RAM too). On other platforms Seal is page protection only; on Linux the allocation-time protections (memfd_secret, MADV_DONTDUMP) are the dump defenses.
While sealed, all access methods (WithBytes, WithBytesErr, CopyOut, CopyIn, etc.) return ErrSealed. Call SecureBuffer.Unseal before accessing the buffer.
SecureBuffer.Destroy works correctly on sealed buffers — it lifts the PROT_NONE restriction (and decrypts) internally before wiping.
Note: [ReadOnly] and [ReadWrite] return ErrSealed while sealed. To transition from Sealed to ReadOnly, call Unseal then ReadOnly. A buffer that was read-only before Seal stays read-only after SecureBuffer.Unseal — the protection is preserved across the seal cycle.
Seal is idempotent: calling it on an already-sealed buffer is a no-op.
func (*SecureBuffer) SetByteAt ¶
func (s *SecureBuffer) SetByteAt(i int, v byte) error
SetByteAt sets the byte at index i to v. Returns an error if i is out of range or the buffer has been destroyed.
The exclusive lock is held to prevent races with ReadOnly/ReadWrite and concurrent Write calls.
func (SecureBuffer) String ¶
func (s SecureBuffer) String() string
String implements fmt.Stringer. It always returns "[REDACTED]".
func (*SecureBuffer) Truncate ¶
func (s *SecureBuffer) Truncate(n int) error
Truncate re-slices data to n bytes and wipes the freed tail [n:].
Invariant: raw is NEVER modified. Only data is re-sliced. This ensures the AddCleanup finalization closure always holds the correct full-page allocation regardless of Truncate calls.
func (*SecureBuffer) Unseal ¶
func (s *SecureBuffer) Unseal() error
Unseal lifts the PROT_NONE protection applied by SecureBuffer.Seal, restoring PROT_READ|PROT_WRITE access to the buffer.
After Unseal, all access methods work normally. To re-protect after use, call Seal again.
Unseal is idempotent: calling it on an already-unsealed buffer is a no-op.
func (*SecureBuffer) WithBytes ¶
func (s *SecureBuffer) WithBytes(fn func([]byte)) error
WithBytes calls fn with the underlying byte slice. The slice is valid ONLY for the duration of fn — it MUST NOT be stored or referenced after fn returns.
The RLock is held for the entire callback, so Destroy blocks until fn returns. This is the preferred access pattern; use WithBytesErr when fn returns an error.
NOT REENTRANT: fn MUST NOT call any access method on the SAME buffer (WithBytes, WithBytesErr, CopyOut, ConstantTimeEqual, …). The lock is writer- preferring, so if another goroutine calls Destroy/CopyIn/Seal/ReadOnly while fn holds the read lock, a nested same-buffer read lock would block on the waiting writer while that writer blocks on fn's outstanding read lock — a deadlock. Nesting access to a DIFFERENT buffer (e.g. the decrypt-into pattern: key.WithBytesErr → out.WithBytesErr) is safe and expected.
Returns ErrDestroyed if the buffer has been destroyed.
func (*SecureBuffer) WithBytesErr ¶
func (s *SecureBuffer) WithBytesErr(fn func([]byte) error) error
WithBytesErr is like WithBytes but fn may return an error, which is propagated. Returns ErrDestroyed if the buffer has been destroyed; fn is not called in that case.
NOT REENTRANT: as with SecureBuffer.WithBytes, fn must not call another access method on the same buffer (deadlock risk under a concurrent writer); nesting onto a different buffer is safe.
func (*SecureBuffer) WriteTo ¶
func (s *SecureBuffer) WriteTo(w io.Writer) (int64, error)
WriteTo implements io.WriterTo. Copies the buffer contents into a temporary heap slice under the read lock, releases the lock, then writes the copy to w.
This decouples Destroy from any I/O latency: a stalled network peer or slow pipe no longer prevents Destroy (including the emergency-wipe path) from proceeding. The temporary copy is wiped via secureWipeSlice after the write.
NOTE: For network or pipe targets, wrap w with a write deadline before calling WriteTo to bound the lifetime of the in-flight copy.
Source Files
¶
- buflock.go
- canary.go
- capabilities.go
- doc.go
- errors.go
- harden.go
- harden_linux.go
- harden_rlimit_unix.go
- mlock_linux.go
- options.go
- redact.go
- region.go
- registry.go
- runtimesecret_enforce_production.go
- scrub.go
- scrub_legacy.go
- scrubframe_amd64.go
- sealcipher_other.go
- secret.go
- secret_platform_supported.go
- securearena.go
- securebuf.go
- securebuf_access.go
- securebuf_scope.go
- terminationwipe.go
- wipe_amd64.go
- wipe_export.go
- wipe_init_amd64.go
Directories
¶
| Path | Synopsis |
|---|---|
|
Package redact sanitizes strings destined for logs and other text sinks: it masks credential-shaped substrings and neutralizes log-injection sequences (CWE-117).
|
Package redact sanitizes strings destined for logs and other text sinks: it masks credential-shaped substrings and neutralizes log-injection sequences (CWE-117). |
|
secmem-crypto
module
|
|
|
secmem-lint
module
|