env: scheme: concurrent resolution loses the value (ai) #3
Labels
No labels
bug
duplicate
enhancement
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
heiko/secret#3
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
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?
env:scheme: concurrent resolution loses the value (ai)Summary
The
env:scheme insecret.Get()unsets the referenced environmentvariable after the first read. When two goroutines call
secret.Get("env:FOO")concurrently, one of them races into a"variable already unset" state and receives the empty string (or the
fallback, if the spec is chained). This is a data-loss footgun for any
caller that resolves the same env-backed secret from multiple
goroutines.
Reproducer (illustrative)
Impact
Downstream
dmarcwill resolve IMAP account passwords (and similarcredentials) from a long-running daemon. Workers retry on connection
failure; each retry re-resolves the spec. The current behavior means a
single transient IMAP fault followed by a re-resolve returns an empty
password, which then fails authentication permanently — even though
the env var was correctly set at process startup.
In dmarc we are working around this by wrapping every
secret.Getcall in a process-local
Resolverthat caches the first-success valueunder a mutex with singleflight-style inflight dedup. That works for
us, but every consumer of this library will need to reinvent the same
wrapper, and the destructive-by-default semantics is itself surprising.
Requested resolution
One of:
env:non-destructive by default. Introduceenv-once:(or
env!:) as the opt-in destructive variant. Existing callersthat depend on the unset-after-read behavior change one byte;
everyone else stops being footgunned.
env:internally concurrency-safe. A mutex around theenv access + return-cached-value semantics. Behavior change is
invisible to single-threaded callers, fixes the race for
multi-threaded ones.
godoc, with a recommended wrapper pattern, and accept that every
consumer reimplements it. Lowest-effort fix; worst long-term cost.
Strong preference for (1) — explicit semantics are friendlier to new
users than implicit-magic-mutex behavior.
Related observations (low priority, separate issues if you want)
Get()may include the spec string in themessage text (e.g.,
file:/etc/foo: no such file). Consumersredacting secrets in logs have to sanitize the error themselves.
A
GetSafe()returning sanitized errors, or typed errors, wouldremove that work.
Validate(spec) error(parse-only) lets callers reject malformed TOML at decode time.
Scheme(spec) (string, error)returningthe first scheme of a chain would let callers render
<scheme>:[redacted]without reimplementing the||parser.These are surfaced for context; only the
env:race is blockingproduction use.
Severity
Blocking prod use for any multi-goroutine consumer of
filesystem/env-backed credentials, which includes
dmarconce IMAPimport lands (planned for
feature/imap-import, afterfeature/config-and-loggingmerges).env: scheme: concurrent resolution loses the valueto env: scheme: concurrent resolution loses the value (ai)