R-124 recipe root namespace as PBS spells it; R-118 no root size for an absent drive; R-269 rotated-out token rejected at once; R-317 dnsmasq install probed by its unit (burn-down round 2)
gates / gates (push) Successful in 47s
gates / gates (push) Successful in 47s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -154,12 +154,15 @@ func (s *TokenStore) Mint(vmid int) (string, error) {
|
||||
// looks it up; the per-candidate comparison is constant-time to avoid a timing oracle on the
|
||||
// stored hash. ok is false for an unknown/empty token.
|
||||
//
|
||||
// Reload-on-miss (B3): the store FILE is shared across processes — the one-shot provisioner
|
||||
// (`--selftest=provision`) Mints into it while the long-lived daemon serves Lookup from an index
|
||||
// built at open. On a miss, re-read the file ONCE and re-check, so a token minted after this
|
||||
// process started authorizes without a daemon restart (the drill's fresh-install 401). The
|
||||
// append-only log makes an unchanged file size proof of no new records, so a genuinely unknown
|
||||
// token costs at most one stat once the index is current — never a reload loop.
|
||||
// Reload-on-change (B3, R-269): the store FILE is shared across processes — the one-shot
|
||||
// provisioner (`--selftest=provision`) Mints into it while the long-lived daemon serves Lookup from
|
||||
// an index built at open. Every Lookup stats the file first and re-reads it when the append-only
|
||||
// log has grown, BEFORE answering — so a token minted elsewhere authorizes without a restart AND a
|
||||
// token rotated out elsewhere stops authorizing on its very next presentation. (Before R-269 the
|
||||
// re-read ran only on a MISS, so a superseded token was a direct map hit and kept authorizing until
|
||||
// some unrelated miss forced the reload.) An unchanged size is proof of no new records, so the
|
||||
// steady state costs one stat per call and never a reload loop. Pinned by
|
||||
// TestTokenStore_RotatedOutTokenRejectedFirst.
|
||||
func (s *TokenStore) Lookup(token string) (int, bool) {
|
||||
if token == "" {
|
||||
return 0, false
|
||||
@@ -167,23 +170,34 @@ func (s *TokenStore) Lookup(token string) (int, bool) {
|
||||
want := hashToken(token)
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
// Direct map hit is the common path; the constant-time compare guards against a timing
|
||||
// side-channel by re-checking the matched key (map lookup itself is not the secret-bearing
|
||||
// comparison — the hash of a random 256-bit token is not feasibly guessable regardless).
|
||||
if vmid, ok := s.byHash[want]; ok {
|
||||
if subtle.ConstantTimeCompare([]byte(want), []byte(s.byVMID[vmid])) == 1 {
|
||||
return vmid, true
|
||||
st, statErr := os.Stat(s.path)
|
||||
if statErr == nil && st.Size() != s.loadedSize {
|
||||
// The log changed under us (another process minted/rotated): converge first, then answer.
|
||||
s.reloads++
|
||||
if err := s.reloadLocked(); err != nil {
|
||||
return 0, false // unreadable store: fail closed, never crash the auth path
|
||||
}
|
||||
return s.matchLocked(want)
|
||||
}
|
||||
// Miss: skip the re-read when the append-only log has not grown (nothing new to see).
|
||||
// A stat error falls through to the reload, which handles a missing file as empty.
|
||||
if st, err := os.Stat(s.path); err == nil && st.Size() == s.loadedSize {
|
||||
return 0, false
|
||||
if vmid, ok := s.matchLocked(want); ok {
|
||||
return vmid, true
|
||||
}
|
||||
if statErr == nil {
|
||||
return 0, false // file unchanged since the last (re)load: genuinely unknown
|
||||
}
|
||||
// Stat failed (e.g. the file vanished): reload, which treats a missing file as empty.
|
||||
s.reloads++
|
||||
if err := s.reloadLocked(); err != nil {
|
||||
return 0, false // unreadable store: fail closed, never crash the auth path
|
||||
return 0, false
|
||||
}
|
||||
return s.matchLocked(want)
|
||||
}
|
||||
|
||||
// matchLocked answers from the in-memory index. Direct map hit is the common path; the
|
||||
// constant-time compare re-checks the matched key against the guest's CURRENT hash (map lookup
|
||||
// itself is not the secret-bearing comparison — the hash of a random 256-bit token is not
|
||||
// feasibly guessable regardless). Caller holds the mutex.
|
||||
func (s *TokenStore) matchLocked(want string) (int, bool) {
|
||||
if vmid, ok := s.byHash[want]; ok {
|
||||
if subtle.ConstantTimeCompare([]byte(want), []byte(s.byVMID[vmid])) == 1 {
|
||||
return vmid, true
|
||||
|
||||
Reference in New Issue
Block a user