tailscale/tailscale · error
reading %d (%x): %w
Error message
reading %d (%x): %w
What it means
Thrown by FS.PurgeAUMs while marking AUMs for deletion. Before setting the PurgedUnix flag, the chonk re-reads the stored record for each hash (a CBOR file named by hash under a 2-hex-char prefix directory). The error means the record for the i-th hash could not be read back. Note that os.ErrNotExist is NOT tolerated on this path: purging a hash that was never committed to this chonk, or whose file was deleted out-of-band, fails with this exact message.
Source
Thrown at tka/tailchonk.go:691
return nil
}
// PurgeAUMs marks the specified AUMs for deletion from storage.
func (c *FS) PurgeAUMs(hashes []AUMHash) error {
if len(hashes) == 0 {
return nil
}
c.mu.Lock()
defer c.mu.Unlock()
c.invalidateIndexLocked()
now := time.Now()
for i, h := range hashes {
stored, err := c.get(h)
if err != nil {
return fmt.Errorf("reading %d (%x): %w", i, h, err)
}
if stored.AUM == nil || stored.PurgedUnix > 0 {
continue
}
err = c.commit(h, func(info *fsHashInfo) {
info.PurgedUnix = now.Unix()
})
if err != nil {
return fmt.Errorf("committing purge[%d] (%x): %w", i, h, err)
}
}
return nil
}
// commit calls the provided updater function to record changes relevant
// to the given hash. The caller is expected to update the AUM and
// Children fields, as relevant.View on GitHub (pinned to 6e0912f979)
Solutions
- If the wrapped error is os.ErrNotExist the purge goal is already met: drop that hash from the list and call PurgeAUMs again
- Ensure no second process is writing or deleting files in the chonk directory; stop the duplicate
- If the file exists but will not decode, move it aside (filename = the hex hash in the message) and drop the hash from the list
- Always recompute the delete list from this chonk's own AllAUMs(), never from a foreign node's state
Example fix
// before
if err := chonk.PurgeAUMs(toDelete); err != nil {
return err // reading 3 (3f9a..): file does not exist
}
// after: only purge hashes that actually exist here
keep := toDelete[:0]
for _, h := range toDelete {
if _, err := chonk.AUM(h); err == nil {
keep = append(keep, h)
}
}
return chonk.PurgeAUMs(keep) Defensive patterns
Strategy: validation
Validate before calling
known, err := chonk.AllAUMs()
if err != nil { return err }
have := make(map[tka.AUMHash]bool, len(known))
for _, h := range known { have[h] = true }
purge := input[:0]
for _, h := range input {
if have[h] { purge = append(purge, h) }
} Type guard
func isStored(chonk *tka.FS, h tka.AUMHash) bool {
_, err := chonk.AUM(h)
return err == nil
} Try / catch
if err := chonk.PurgeAUMs(hashes); err != nil {
if errors.Is(err, os.ErrNotExist) {
// target already removed; purge goal already met — safe to continue
return nil
}
return err
} Prevention
- Derive purge lists from the same chonk's AllAUMs(), never from another node's state
- Keep a single writer process per state dir
- Never delete files inside the tka directory by hand
When it happens
Trigger: Calling PurgeAUMs with a delete list derived from another node's storage containing hashes this chonk never stored; the AUM file was removed by a second process or by hand between listing and purge; the stored file is corrupt so c.get()'s CBOR decode fails; permission changes prevent os.Open.
Common situations: tka.Compact computing a toDelete list while another process mutates the same state dir; state dir partially restored from backup; scripts pruning 'old' files in the tka directory by mtime.
Understand the failure class
Background: "failed to read file", EACCES, ENOENT and "could not read <path>" errors: when a program can't read a file from disk — this error's family across 49 libraries.
Related errors
- committing purge[%d] (%x): %w
- committing update[%d] (%x): %v
- creating directory: %v
- creating chonk root dir: %v
- chonk directory %q is a file
AI-assisted analysis of tailscale/tailscale@6e0912f979 (2026-08-18).
Data as JSON: /api/errors/d3d9e97f163c62c7.
Report an issue: GitHub.