1 Commits
Author SHA1 Message Date
sneak 6a6034f4a6 Log the audit fixes in TODO.md (closes #173)
check / check (push) Canceled after 0s
Completed Steps gets one entry for each fix merged into next since
v1.0.0-rc2 that had none, newest first in landing order. Each entry says
what its landing commit says and carries that commit's date in UTC. The
five fixes that logged themselves before this issue's decision keep
their entries and get no second one. Status no longer says there are no
git tags.

Model: opus-5-5
2026-10-15 14:59:14 +00:00
4 changed files with 22 additions and 71 deletions
+5 -14
View File
@@ -17,15 +17,6 @@ https://git.eeqj.de/sneak/secret/milestone/12
# Completed Steps
- 2026-10-15: `make test` no longer fails on some runs in `internal/cli`
(https://git.eeqj.de/sneak/secret/issues/192). `cli.Entry()` set `time.Local`
to UTC and ended by wiping every memguard buffer, and a test ran it in the
same process as the other tests. A timer left by an earlier test could read
`time.Local` as it changed, which the race detector reported, and the wipe
could destroy a buffer still in use, the likeliest cause of a crash of the
test process on another run. `main()` in `cmd/secret` now sets `time.Local`
before it calls `cli.Entry()`, and that test runs `cli.Entry()` in a process
of its own.
- 2026-10-15: `secret mv` from one vault to another unlocks both vaults before
it takes the state directory lock
(https://git.eeqj.de/sneak/secret/issues/189). It unlocked them while holding
@@ -40,11 +31,11 @@ https://git.eeqj.de/sneak/secret/milestone/12
`secret unlocker add` shares, now ends "nothing was changed".
- 2026-10-15: `secret init`, `secret vault create`, `secret unlocker add`, and
`secret add`, `secret import` and `secret generate secret` with `--force` ask
for the mnemonic or passphrase, or unlock the vault, before they take the
state directory lock (https://git.eeqj.de/sneak/secret/issues/152). They did
so while holding the lock, so every other command that changes anything waited
at the prompt. Under the lock, `secret unlocker add` fails if the vault was
removed, or created again with another key, and refuses a GPG key added
for the mnemonic or passphrase and unlock the vault before they take the state
directory lock (https://git.eeqj.de/sneak/secret/issues/152). They asked and
unlocked while holding the lock, so every other command that changes anything
waited at the prompt. Under the lock, `secret unlocker add` fails if the vault
was removed, or created again with another key, and refuses a GPG key added
meanwhile; a forced add of a secret created meanwhile lets go of the lock,
gets the key and takes the lock again. `secret.CreatePGPUnlocker` takes the
unlocked vault; `SetMnemonic` and `SetUnlockPassphrase` are gone.
-6
View File
@@ -3,16 +3,10 @@ package main
import (
"os"
"time"
"sneak.berlin/go/secret/internal/cli"
)
func main() {
// Every time secret prints or writes is in UTC, whatever TZ or the
// system's time zone is set to. It is set here, before cli.Entry starts
// any goroutine that could read time.Local while it changes.
time.Local = time.UTC //nolint:gosmopolitan // this makes local time UTC
os.Exit(cli.Entry())
}
+12 -51
View File
@@ -3,7 +3,6 @@ package cli_test
import (
"bufio"
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
@@ -17,67 +16,29 @@ import (
"sneak.berlin/go/secret/internal/secret"
)
const (
// envRunEntry, when set, makes this test binary run cli.Entry with its
// own arguments and exit, instead of running the tests.
envRunEntry = "SECRET_TEST_RUN_ENTRY"
// bufferWiped is what the test binary prints, run with envRunEntry set,
// when cli.Entry wiped the buffer made before it ran.
bufferWiped = "Entry wiped the buffer made before it ran"
)
// With envRunEntry set, the test binary makes a buffer, runs cli.Entry,
// says whether the buffer is wiped, and exits with Entry's exit code. This
// is done in init, not in a test, so that TestMain does not run in that
// process: it would build the secret binary again, and remove it at the end,
// while the other tests run it.
//
//nolint:gochecknoinits // must run before TestMain, as above
func init() {
if os.Getenv(envRunEntry) == "" {
return
}
buf := memguard.NewBufferFromBytes([]byte("key material"))
code := cli.Entry()
if !buf.IsAlive() {
fmt.Fprintln(os.Stderr, bufferWiped)
}
os.Exit(code)
}
// Entry must return its exit code rather than exit, so that its deferred
// memguard purge runs on the success and the error path alike. Entry wipes
// every memguard buffer in the process, including those other tests are
// using, so it runs in a process of its own: this test binary, run again
// with envRunEntry set.
// memguard purge runs on the success and the error path alike.
//
//nolint:paralleltest // sets os.Args, and Entry wipes every buffer in the process
func TestEntryWipesBuffersAndReturnsExitCode(t *testing.T) {
t.Parallel()
savedArgs := os.Args
t.Cleanup(func() { os.Args = savedArgs })
tests := []struct {
args []string
exitCode int
}{
{args: []string{"--help"}, exitCode: 0},
{args: []string{"no-such-command"}, exitCode: 1},
{args: []string{"secret", "--help"}, exitCode: 0},
{args: []string{"secret", "no-such-command"}, exitCode: 1},
}
for _, tt := range tests {
//nolint:gosec // G204: test runs its own test binary
cmd := exec.CommandContext(t.Context(), os.Args[0], tt.args...)
buf := memguard.NewBufferFromBytes([]byte("key material"))
os.Args = tt.args
cmd.Env = append(os.Environ(), envRunEntry+"=1")
output, err := cmd.CombinedOutput()
require.NotNil(t, cmd.ProcessState, "%v did not run: %v", tt.args, err)
assert.Equal(t, tt.exitCode, cmd.ProcessState.ExitCode(),
"exit code for %v: %s", tt.args, output)
assert.Contains(t, string(output), bufferWiped,
"Entry left a buffer unwiped for %v", tt.args)
assert.Equal(t, tt.exitCode, cli.Entry(), "exit code for %v", tt.args)
assert.False(t, buf.IsAlive(), "Entry left a buffer unwiped for %v", tt.args)
}
}
+5
View File
@@ -2,6 +2,7 @@ package cli
import (
"os"
"time"
"github.com/awnumar/memguard"
"github.com/spf13/cobra"
@@ -14,6 +15,10 @@ import (
// every memguard buffer before it returns, so the caller must do nothing
// but exit with the code.
func Entry() int {
// Every time secret prints or writes is in UTC, whatever TZ or the
// system's time zone is set to.
time.Local = time.UTC //nolint:gosmopolitan // this makes local time UTC
raiseLockedMemoryLimit()
// On SIGINT or SIGTERM memguard runs this function, wipes every buffer