tailscale/tailscale · error
remove of %q failed (%w) and so did truncate: %v
Error message
remove of %q failed (%w) and so did truncate: %v
What it means
The copy+delete fallback wrote the new file, then Remove(old) failed AND the last-resort Truncate(old) also failed. Both wrapped errors are reported: remove error %w plus truncate error %v. The system may now have two copies of resolver config; the function returns an error so the caller knows DNS config is in an inconsistent state.
Source
Thrown at net/dns/direct.go:314
if err != nil {
return fmt.Errorf("reading %q to rename: %w", old, err)
}
if err := m.fs.WriteFile(new, bs, 0644); err != nil {
return fmt.Errorf("writing to %q in rename of %q: %w", new, old, err)
}
// Explicitly set the permissions on the new file. This ensures that
// if we have a umask set which prevents creating world-readable files,
// the file will still have the correct permissions once it's renamed
// into place. See #12609.
if err := m.fs.Chmod(new, 0644); err != nil {
return fmt.Errorf("chmod %q in rename of %q: %w", new, old, err)
}
if err := m.fs.Remove(old); err != nil {
err2 := m.fs.Truncate(old)
if err2 != nil {
return fmt.Errorf("remove of %q failed (%w) and so did truncate: %v", old, err, err2)
}
}
if new == resolvConf {
m.relabelResolvConf()
}
return nil
}
var restoreconPath lazy.SyncValue[string] // path to restorecon, or "" if absent
// relabelResolvConf restores the policy-default SELinux context on
// /etc/resolv.conf. Best effort: only runs when SELinux is enforcing. See:
//
// https://github.com/tailscale/tailscale/issues/20149.
func (m *directManager) relabelResolvConf() {
if runtime.GOOS != "linux" {
return
}View on GitHub (pinned to 6e0912f979)
Solutions
- lsattr the old path and clear immutability: chattr -i /etc/resolv.conf (or the reported path)
- Ensure write permission on the containing directory, not just the file: chmod u+w /etc
- Verify the fs supports unlink and truncate (stat -f /etc)
- If a stale duplicate file remains, remove it manually once the fs is fixed; then re-apply DNS with 'tailscale up --accept-dns=true'
Defensive patterns
Strategy: try-catch
Validate before calling
if fi, err := os.Stat(oldPath); err == nil && fi.Mode()&os.ModeImmutable != 0 {
return errors.New("old file is immutable; clear with chattr -i")
} Try / catch
err := mgr.SetDNS(cfg)
if err != nil && strings.Contains(err.Error(), "so did truncate") {
// stale duplicate may exist; clean up manually then re-apply
log.Printf("cleanup needed: %v", err)
} Prevention
- Never chattr +i /etc/resolv.conf
- Ensure write permission on the /etc directory itself
- Check for stale copies after this error
When it happens
Trigger: fs.Remove returns EBUSY/EISDIR/EPERM and fs.Truncate returns EACCES/EROFS/EINVAL. Classic on systems where the old path is a hardlink or bind-mount, where the file is held open with mandatory locks, or where the directory is not writable even though the file is.
Common situations: Immutable/append-only attributes (chattr +i) on the old resolv.conf, read-only /etc, an immutable OS image (Container Linux-style) or a NAS appliance (Synology fails fast earlier, others do not), or a custom wholeFileFS whose Remove/Truncate are stubs.
Related errors
- NewDirWatcher: %w
- reading %q to rename: %w
- writing to %q in rename of %q: %w
- chmod %q in rename of %q: %w
- atomicWriteFile: Chmod: %w
AI-assisted analysis of tailscale/tailscale@6e0912f979 (2026-08-18).
Data as JSON: /api/errors/8bd6a3f7ed767b1d.
Report an issue: GitHub.