Skip to content

Fix segfault on writes to a path naming a key - #14

Merged
wayneeseguin merged 2 commits into
developfrom
fix-nil-secret-on-key-miss
Jul 29, 2026
Merged

Fix segfault on writes to a path naming a key#14
wayneeseguin merged 2 commits into
developfrom
fix-nil-secret-on-key-miss

Conversation

@wayneeseguin

Copy link
Copy Markdown
Contributor

safe set secret/foo:bar k=v segfaults. So do paste, ask, ssh,
rsa, and dhparam when the path names a key that does not yet exist.

Cause

Vault.Read returns a nil *Secret alongside KeyNotFoundError
(vault.go:158), while the missing-secret branch returns an empty one.
IsNotFound covers both (errors.go:29), so the caller shape

s, err := v.Read(path)
if err != nil && !vault.IsNotFound(err) {
	return err
}

falls through holding nil, and every Secret accessor dereferences its
receiver.

panic: runtime error: invalid memory address or nil pointer dereference
vault.(*Secret).Has(...)          pkg/vault/secret.go:38
vault.(*Secret).Set(0x0, ...)     pkg/vault/secret.go:60
cli.(*CLI).writeHelper            internal/cli/secrets.go:57

The doc comment above Read claimed the opposite of what the code did —
"a nil *Secret will be returned, with no error" — on both clauses.

The tell is that the same command works when the key does exist: Read
succeeds, and Write rejects the syntax properly. The correct behavior
was already there; only the not-found path skipped it.

Measured, Vault 1.13.2 KV v2

Seed safe set secret/foo user=admin, then:

Command Before After
set secret/foo:bar k=v SIGSEGV cannot write to paths in /path:key notation, rc=1
paste secret/foo:nope k=v SIGSEGV same
ssh secret/foo:private SIGSEGV same
rsa secret/foo:private SIGSEGV same
dhparam secret/foo:dhparam-pem SIGSEGV, after generating the params same, before generating them
set secret/foo:user k=v (key exists) clean error unchanged
gen / uuid on path:key rc=0 unchanged — #12 already strips the key

What this does

Commit 1 returns the already-allocated empty secret instead of nil, so
both not-found branches agree, and corrects the doc comment. This alone
removes all five crashes.

Commit 2 is not a crash fix — it moves the complaint earlier. Without
it the error still arrives from Write, but only after the value has been
prompted for and the key material generated. safe dhparam 4096 spun for
the full generation before failing; it now returns immediately. The guard
runs before connect(), so no Vault round-trip happens either.

Escaped colons and carets stay part of the path: safe set 'secret/we\:ird' user=admin still writes, verified against a live server with the vault
CLI confirming the literal we:ird path.

Verification

  • make check green. Three new pkg/vault tests pin the never-nil
    invariant; one reproduces the production crash in-process and fails with
    the identical stack before the fix.
  • Eight new internal/cli tests drive the real handlers, which is what
    pins the guard ahead of connect().
  • Integration suite, Vault 1.13.2: 1725 passing, 0 failing — unchanged
    from develop.
  • No caller anywhere depends on the nil return; nothing in the repo
    compares a *Secret against nil, and no test asserted it.

Note for reviewers

IsNotFound deliberately keeps its umbrella meaning. Narrowing it would
change the -f delete paths (secrets.go:542,549) and the tree walks
(tree.go:411,427,798,905), which rely on it covering both cases.

CreateSignedCertificate (vault.go:1044) has the same hazardous shape and
is exported, but has no caller in the repo. Commit 1 makes it safe without
touching it.

Read returned a nil *Secret alongside KeyNotFoundError, while the
missing-secret branch returned an empty one. Callers that tolerate a
not-found error and keep using the value therefore segfaulted on the
first Secret method, since every accessor dereferences its receiver.

Return the already-allocated empty secret so both not-found branches
agree, and correct the doc comment, which claimed a nil secret and no
error where the code does the opposite.
set, paste, ask, ssh, rsa, and dhparam write a whole secret and cannot
honour a path naming a key or a version. Vault.Write said so, but only
once it was reached: the value had already been prompted for and the
key material already generated by then, and the work was discarded.

Check the path before connecting, reusing Write's wording. Escaped
colons and carets stay part of the path, so a secret whose name
contains one is still writable.
@wayneeseguin
wayneeseguin merged commit 3e94720 into develop Jul 29, 2026
@wayneeseguin
wayneeseguin deleted the fix-nil-secret-on-key-miss branch July 29, 2026 01:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant