Skip to content

fix(pass): copy bytes in PassValue.Unmarshal to survive backend zeroing#536

Merged
Benehiko merged 1 commit into
mainfrom
fix/passvalue-unmarshal-alias
May 28, 2026
Merged

fix(pass): copy bytes in PassValue.Unmarshal to survive backend zeroing#536
Benehiko merged 1 commit into
mainfrom
fix/passvalue-unmarshal-alias

Conversation

@Benehiko
Copy link
Copy Markdown
Member

Store backends defer clear() on the source buffer after calling Secret.Unmarshal (commit cd2a257). PassValue was aliasing the slice (m.value = data), so its value got zeroed under it. The daemon then returned base64-encoded zeros (e.g. "AAAA…" / "AAA") instead of the secret.

macOS keychain tests masked this because MockCredential.Unmarshal copies via string() conversion.

Fix: copy bytes in PassValue.Unmarshal. Also document the ownership contract on store.Secret.Unmarshal and add a regression test that zeros the source buffer after Unmarshal.

Store backends defer clear() on the source buffer after calling
Secret.Unmarshal (commit cd2a257). PassValue was aliasing the slice
(m.value = data), so its value got zeroed under it. The daemon then
returned base64-encoded zeros (e.g. "AAAA…" / "AAA") instead of the
secret.

macOS keychain tests masked this because MockCredential.Unmarshal
copies via string() conversion.

Fix: copy bytes in PassValue.Unmarshal. Also document the ownership
contract on store.Secret.Unmarshal and add a regression test that
zeros the source buffer after Unmarshal.

Signed-off-by: Alano Terblanche <alano.terblanche@docker.com>

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Benehiko Benehiko merged commit 571241b into main May 28, 2026
22 checks passed
@Benehiko Benehiko deleted the fix/passvalue-unmarshal-alias branch May 28, 2026 11:24
Copy link
Copy Markdown

@docker-agent docker-agent Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assessment: 🔴 CRITICAL

Note: Verification agent returned an inconclusive response; findings from the drafter are surfaced for author evaluation.

Context: The Unmarshal fix (copying bytes via append([]byte(nil), data...)) is correct and well-motivated. However, the PR leaves a symmetric aliasing issue in PassValue.Marshal(), and the new interface contract documentation only covers the Unmarshal direction.

Comment thread store/store.go
@@ -59,7 +59,9 @@ var (
type Secret interface {
// Marshal the secret into a slice of bytes
Marshal() ([]byte, error)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] Incomplete interface contract: Marshal() return value is also zeroed by backends

All four store backends call defer clear(data) on the slice returned by Marshal():

// keychain_darwin.go:183, keychain_linux.go:322, keychain_windows.go:63, posixage/store.go:351
data, err := secret.Marshal()
defer clear(data)  // ← zeroes m.value in PassValue via the alias

Since PassValue.Marshal() returns m.value directly (return m.value, nil) without copying, every Save()/Upsert() call silently zeroes the PassValue's internal state. Any subsequent Marshal() call on the same instance returns all-zero bytes.

The new comment added in this PR documents that Unmarshal implementations must copy retained bytes, but misses the symmetric contract for Marshal: backends zero the returned buffer, so Marshal() must return a fresh copy (or the contract must state that Marshal() output will be zeroed and callers must not reuse the instance).

Suggested fixes:

Option A — Copy in PassValue.Marshal():

func (m *PassValue) Marshal() ([]byte, error) {
    return append([]byte(nil), m.value...), nil
}

Option B — Document the contract on the Marshal interface comment (alongside the Unmarshal note already added):

// Marshal the secret into a slice of bytes.
// Callers (store backends) may zero the returned buffer after use;
// implementations that need to retain state must not return an alias.
Marshal() ([]byte, error)

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.

2 participants