memguard lifecycle is not wired up: Ctrl-C and error exits leave key material in locked memory unwiped #35
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
From the 1.0 security survey. Highest value-per-line item on the milestone: roughly five lines of code closes it.
Threat
The repo uses
memguardthroughout, and per-allocationmlockis therefore already in effect for everyLockedBuffer. What is missing is the process-level lifecycle that memguard requires to actually guarantee wiping.cmd/secret/main.gois eight lines and callscli.Entry().internal/cli/root.go:19-26:There is no
memguard.CatchInterrupt()and nodefer memguard.Purge()anywhere in the repository. Three consequences, all of which leave plaintext key material in locked pages:Signals. Ctrl-C or
SIGTERMduring any operation terminates the process immediately. Every liveLockedBuffer— which at various moments holds the vault long-term private key, an unlocker passphrase, a version private key, or a decrypted secret value — is never destroyed.memguard.CatchInterrupt()exists precisely to install a handler that purges before exiting; without it the mlocked pages are released to the kernel unwiped. Interrupting asecret getthat is waiting on a Touch ID prompt or a GPG pinentry is an entirely ordinary user action.os.Exit(1)on the error path skips every deferredDestroy(). Go does not run deferred functions onos.Exit. The codebase relies ondefer buf.Destroy()as its wiping mechanism, so every failure that propagates an error up toEntry()— a wrong passphrase, a missing unlocker, an I/O error mid-decrypt — exits with buffers intact. The error paths are exactly the paths where a partially-decrypted key is most likely to be sitting in memory.No final
Purge()on the normal exit path either, so the guarantee depends entirely on every individualdeferbeing present and correct on every path.The exposure is the standard one for unwiped key material: swap, hibernation images, core dumps, and
/proc/<pid>/memfor same-uid processes. It is meaningfully worse here than in a typical program because the buffers hold the long-term vault key, from which every secret in the vault is recoverable, not a single session credential.Note that CI already runs with
--ulimit memlock=-1:-1(script/cibuild) because the suite exercises memguard, so the locking half of this is real and working. Only the teardown is absent.Definition of done
memguard.CatchInterrupt()is called once, early, before any secret-bearing work can begin.defer memguard.Purge()covers the full program lifetime.LockedBuffermay exist callsos.Exitwithout purging first. The currentos.Exit(1)inEntry()must either be preceded by an explicitmemguard.Purge()or restructured so the deferred purge runs — the usual shape is an inner function that returns an exit code, withos.Exitcalled only from the outermost frame after all defers have run.os.Exit,log.Fatal,log.Fatalf, orpanicin non-test code that could bypass the purge, and fix or justify each.log.Fatalcallsos.Exitand is just as skip-prone.Entry(or the extracted inner function) returns rather than exiting, so the deferred purge is reachable. Signal handling is awkward to test directly; if it cannot be tested reliably, say so on the issue rather than adding a flaky test.make checkgreen.TODO.mdupdated in the same commit.Implementation requirements
memguard.CatchInterrupt()installs a signal handler that terminates the process. Verify this does not break the interactive prompts — the passphrase and mnemonic readers put the terminal in a raw or no-echo mode, and a handler that exits without restoring it leaves the user's terminal with echo disabled. If that is a real risk, restore terminal state in the purge path and note it in the commit message.memguard.Purge()through the codebase. One place, at the top level. Purge is global and idempotent; calling it from many sites invites destroying buffers another frame still holds.cmd/secret/main.goandinternal/cli/root.go. Do not fold in unrelated memory hardening — the other memguard gaps are tracked separately, and this one should stay small enough to review at a glance.