secmem

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 20, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

README

secmem

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.

Honesty first

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. Call Probe once 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 with WithInsecureFallback().
  • Every claim below is exercised by a test. The guard pages actually fault; the memfd_secret isolation is checked against /proc/self/mem; the wipe, redaction, and no-panic promises are fuzzed. See KERNELS.md for the kernels the suite has been executed on.

Install

go get github.com/deadpoets/secmem

Quick start

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.

The platform guarantee matrix

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) linux (older / 32-bit) darwin windows other
Off the Go heap ✓ memfd_secret ✓ mmap ✓ mmap ✓ VirtualAlloc LOUD heap only
No swap (locked) ✓ mlock ✓ mlock ✓ VirtualLock
Kernel isolation (invisible to root / ptrace / /proc/<pid>/mem) ✓ memfd_secret ✗ (falls to mlock)
Excluded from crash dumps ⚠ MADV_DONTDUMP ⚠ MADV_DONTDUMP ⚠ WER exclusion
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 ✓ (amd64/arm64 asm; else ⚠ constant-time) ✓ asm ✓ asm (amd64) ⚠ constant-time store
Guard pages + overflow canary ✗ (heap fallback)
Register + stack + heap scrub (Scrub) ✓ with GOEXPERIMENT=runtimesecret ✓ if set (amd64/arm64); else frame-scrub frame-scrub only frame-scrub only frame-scrub / ✗
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. Whether memfd_secret is live depends on the kernel's CONFIG_SECRETMEM, not the version alone — where it is absent, secmem reports "fallback" and uses mmap+mlock, honestly, per allocation.

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.

Modules

  • secmem (this module) — SecureBuffer, SecureArena, Secret, Capabilities/Probe, Scrub, and the process-hardening helpers. Depends only on golang.org/x/sys.
  • secmem/redactSanitizer and an slog.Handler for boundary-level log scrubbing. Standard library only.

Documentation

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, 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.

Contributing

Bug fixes, hardening, and speedups-without-regression are welcome — see CONTRIBUTING.md for the workflow (every PR, including the maintainer's, goes through review and CI). Found a vulnerability? See SECURITY.md — please don't file it as a public issue. Participation is governed by the Code of Conduct.

License

Apache-2.0. See LICENSE.

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

Examples

Constants

This section is empty.

Variables

View Source
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().

View Source
var ErrArenaFull = errors.New("secmem: secure arena is full — no free slots")

ErrArenaFull is returned by SecureArena.Acquire when all slots are in use.

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
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

func EnsureMemlockLimit(bytes uint64) (achieved uint64, err error)

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

func InstallTerminationWipe(signals ...os.Signal) (uninstall func())

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

func ScrubErr(fn func() error) (err error)

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

func (s ArenaSlot) Format(f fmt.State, _ rune)

Format implements fmt.Formatter, so every verb redacts.

func (ArenaSlot) GoString

func (s ArenaSlot) GoString() string

GoString implements fmt.GoStringer, so %#v also redacts.

func (*ArenaSlot) Index

func (s *ArenaSlot) Index() int

Index returns the slot's zero-based index within the arena.

func (*ArenaSlot) IsLive

func (s *ArenaSlot) IsLive() bool

IsLive reports whether the slot is currently acquired (not yet released).

func (ArenaSlot) LogValue

func (s ArenaSlot) LogValue() slog.Value

LogValue implements slog.LogValuer, so slog.Any("key", slot) redacts.

func (*ArenaSlot) Release

func (s *ArenaSlot) Release() error

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

func (s ArenaSlot) String() string

String implements fmt.Stringer. It always returns "[REDACTED]".

func (*ArenaSlot) WithBytes

func (s *ArenaSlot) WithBytes(fn func([]byte)) error

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

func (s *ArenaSlot) WithBytesErr(fn func([]byte) error) error

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)
	}
}

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

func NewSecret(b []byte, opts ...Option) (Secret, error)

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

func (s Secret) ConstantTimeEqual(other Secret) bool

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

func (s Secret) Destroy() error

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

func (s Secret) GoString() string

GoString implements fmt.GoStringer, covering the %#v verb. It always returns "[REDACTED]".

func (Secret) LogValue

func (s Secret) LogValue() slog.Value

LogValue implements slog.LogValuer. It always returns "[REDACTED]".

func (Secret) MarshalJSON

func (s Secret) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler. It always returns the JSON string "[REDACTED]".

func (Secret) MarshalText

func (s Secret) MarshalText() ([]byte, error)

MarshalText implements encoding.TextMarshaler. It always returns "[REDACTED]" — a Secret never serializes its contents.

func (Secret) String

func (s Secret) String() string

String implements fmt.Stringer. It always returns "[REDACTED]".

func (Secret) WithBytes

func (s Secret) WithBytes(fn func([]byte)) error

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

func (s Secret) WriteTo(w io.Writer) (int64, error)

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:

  1. Mark arena destroyed under alloc (blocks new Acquire).
  2. Acquire exclusive mu lock (waits for all in-flight callbacks to return).
  3. Wipe full raw region (REP STOSB + CLFLUSH on amd64).
  4. Madvise DONTNEED.
  5. Munlock + Munmap.
  6. Nil raw — makes IsDestroyed() = true and Destroy idempotent.

Destroy is idempotent and goroutine-safe.

func (SecureArena) Format

func (SecureArena) Format(f fmt.State, _ rune)

func (SecureArena) GoString

func (SecureArena) GoString() string

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) LogValue

func (SecureArena) LogValue() slog.Value

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.

func (SecureArena) String

func (SecureArena) String() string

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

func NewBufferFromReader(r io.Reader, size int, opts ...Option) (*SecureBuffer, int64, error)

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:

  1. Stop the AddCleanup fallback (prevents double-free).
  2. Mprotect(RW) — ensure the page is writable before wiping.
  3. secureWipeSlice — zero + CLFLUSH/CLFLUSHOPT + SFENCE/LFENCE.
  4. Madvise(DONTNEED) — release physical frames immediately.
  5. freeSecretMem — Munlock + Munmap / VirtualUnlock + VirtualFree.
  6. Nil out data and raw — makes Destroy idempotent.
  7. 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.

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-lint module

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL