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

  1. lsattr the old path and clear immutability: chattr -i /etc/resolv.conf (or the reported path)
  2. Ensure write permission on the containing directory, not just the file: chmod u+w /etc
  3. Verify the fs supports unlink and truncate (stat -f /etc)
  4. 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

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


AI-assisted analysis of tailscale/tailscale@6e0912f979 (2026-08-18). Data as JSON: /api/errors/8bd6a3f7ed767b1d. Report an issue: GitHub.