2 Commits
Author SHA1 Message Date
clawbot b7b951a42b A secret too large for the locked-memory limit fails with an error, not a panic (closes #145)
check / check (push) Canceled after 0s
secret add, import and generate secret crashed with memguard's panic once a
value outgrew the locked-memory limit, and because the value was held twice,
half the usual 8 MiB limit was enough. secret now raises the limit to the hard
limit at startup. Each of the three commands refuses, before its first locked
allocation, a limit that leaves fewer than 8 pages for its keys and buffers.
The value is read a page at a time into a memguard.Stream, which keeps it
encrypted, and copied once into locked memory after its size is checked
against 100 MiB and the limit. It leaves the stream through Next, not Read:
Read re-encrypts a leftover piece, and memguard hangs if that runs out of
locked memory.

Model: opus-5-5
2026-10-15 01:05:09 +00:00
clawbot acb4b9cb6a version promote --help says the current file is rewritten (closes #153)
check / check (push) Canceled after 0s
The help text for `secret version promote` and a comment in
PromoteVersion said promote updates a "current symlink". `current` is a
plain file holding the version name, and promote rewrites it; the README
already says so. A new test runs `secret version promote --help` and
checks the output does not mention a symlink and does name the current
file.

Model: opus-5-5
2026-10-15 02:31:47 +02:00
3 changed files with 32 additions and 26 deletions
+11 -24
View File
@@ -214,23 +214,25 @@ func TestImportSecretVariousSizes(t *testing.T) {
}
}
// TestAddSecretBufferGrowth tests that our buffer growth strategy works correctly
// TestAddSecretAroundPageSize tests that secret add, which reads its input
// one memory page (usually 4 KiB) at a time, stores values just under, at and
// just over one and two pages unchanged.
//
//nolint:paralleltest // in parallel, size tests could exceed the memlock limit
func TestAddSecretBufferGrowth(t *testing.T) {
// Test various sizes that should trigger buffer growth
func TestAddSecretAroundPageSize(t *testing.T) {
// Sizes around one and two pages of 4 KiB, then larger ones up to 1 MiB
sizes := []int{
1, // Single byte
100, // Small
4095, // Just under initial 4KB
4095, // Just under 4KB
4096, // Exactly 4KB
4097, // Just over 4KB
8191, // Just under 8KB (first double)
8191, // Just under 8KB
8192, // Exactly 8KB
8193, // Just over 8KB
12288, // 12KB (should trigger second double)
12288, // 12KB
16384, // 16KB
32768, // 32KB (after more doublings)
32768, // 32KB
65536, // 64KB
131072, // 128KB
524288, // 512KB
@@ -391,8 +393,8 @@ func requireRefusedAtLockedMemoryLimit(
// locked-memory limit (ulimit -l) lowered to 1 MiB, where memguard would crash
// the process with a panic instead of returning an error. add must store the
// largest value that fits, which takes holding it in locked memory only once.
// add, import and generate secret must refuse a value one byte larger, and one
// over 100 MB, with exit status 1 and an error saying why.
// add, import and generate secret must refuse a value one byte larger with
// exit status 1 and an error saying why.
func TestSecretsAtLockedMemoryLimit(t *testing.T) {
t.Parallel()
@@ -405,12 +407,6 @@ func TestSecretsAtLockedMemoryLimit(t *testing.T) {
tooBigFile := filepath.Join(t.TempDir(), "too-big")
require.NoError(t, os.WriteFile(tooBigFile, make([]byte, fits+1), 0o600))
// Sparse, so that it takes no room on disk
overMaxSize, err := os.Create(filepath.Join(t.TempDir(), "over-max-size"))
require.NoError(t, err)
t.Cleanup(func() { _ = overMaxSize.Close() })
require.NoError(t, overMaxSize.Truncate(maxSecretSize+1))
output, err := runAtLockedMemoryLimit(t, stateDir, limitKiB,
bytes.NewReader(make([]byte, fits)), "add", "fits")
require.NoError(t, err, "the largest value that fits: %s", output)
@@ -421,15 +417,6 @@ func TestSecretsAtLockedMemoryLimit(t *testing.T) {
nil, "import", "too-big", "--source", tooBigFile)
requireRefusedAtLockedMemoryLimit(t, stateDir, limitKiB, limitError,
nil, "generate", "secret", "too-big", "--length", strconv.Itoa(fits+1))
requireRefusedAtLockedMemoryLimit(t, stateDir, limitKiB,
errSecretTooLarge.Error(), overMaxSize, "add", "too-big")
requireRefusedAtLockedMemoryLimit(t, stateDir, limitKiB,
errSecretTooLarge.Error(), nil,
"import", "too-big", "--source", overMaxSize.Name())
requireRefusedAtLockedMemoryLimit(t, stateDir, limitKiB,
errSecretTooLarge.Error(), nil,
"generate", "secret", "too-big", "--length", strconv.Itoa(maxSecretSize+1))
}
// TestSecretsUnderSmallLockedMemoryLimits runs add, import and generate
+2 -2
View File
@@ -64,7 +64,7 @@ func VersionCommands(cli *Instance) *cobra.Command {
promoteCmd := &cobra.Command{
Use: "promote <secret-name> <version>",
Short: "Promote a specific version to current",
Long: "Updates the current symlink to point to the specified " +
Long: "Rewrites the secret's current file to name the specified " +
"version. The versions' own times are left unchanged; secret " +
"list then gives the time of the promote as the secret's last " +
"update.",
@@ -293,7 +293,7 @@ func (cli *Instance) PromoteVersion(
version, vault.ErrVersionNotFound, secretName)
}
// Update the current symlink using the proper function
// Rewrite the secret's current file to name the version
err = secret.SetCurrentVersion(cli.fs, secretDir, version)
if err != nil {
return fmt.Errorf("failed to update current version: %w", err)
+19
View File
@@ -6,6 +6,8 @@
// - TestListVersionsNonExistentSecret: Tests error handling for missing secrets
// - TestPromoteVersionCommand: Tests `secret version promote` command
// - TestPromoteNonExistentVersion: Tests error handling for invalid promotion
// - TestPromoteHelpSaysCurrentFileIsRewritten: Tests `secret version promote
// --help` text
// - TestGetSecretWithVersion: Tests `secret get --version` flag functionality
// - TestGetSecretWritesBinaryValue: Tests `secret get` output of binary values
// - TestGetSecretReturnsWriteError: Tests `secret get` when stdout fails
@@ -270,6 +272,23 @@ func TestPromoteNonExistentVersion(t *testing.T) {
require.ErrorIs(t, err, vault.ErrVersionNotFound)
}
// TestPromoteHelpSaysCurrentFileIsRewritten is a regression test for
// https://git.eeqj.de/sneak/secret/issues/153: `secret version promote
// --help` said it updates a symlink, but promote rewrites the secret's
// `current` file, which is a plain file.
//
//nolint:paralleltest // executes the CLI in-process and sets the environment
func TestPromoteHelpSaysCurrentFileIsRewritten(t *testing.T) {
env := map[string]string{secret.EnvStateDir: t.TempDir()}
output, err := ExecuteCommandInProcess(
[]string{"version", "promote", "--help"}, "", env)
require.NoError(t, err)
assert.NotContains(t, output, "symlink")
assert.Contains(t, output, "current file")
}
func TestGetSecretWithVersion(t *testing.T) {
t.Parallel()