{"record":{"id":"8bd6a3f7ed767b1d","repo":"tailscale/tailscale","slug":"remove-of-q-failed-w-and-so-did-truncate-v","errorCode":null,"errorMessage":"remove of %q failed (%w) and so did truncate: %v","messagePattern":"remove of %q failed \\(%w\\) and so did truncate: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"net/dns/direct.go","lineNumber":314,"sourceCode":"\tif err != nil {\n\t\treturn fmt.Errorf(\"reading %q to rename: %w\", old, err)\n\t}\n\tif err := m.fs.WriteFile(new, bs, 0644); err != nil {\n\t\treturn fmt.Errorf(\"writing to %q in rename of %q: %w\", new, old, err)\n\t}\n\n\t// Explicitly set the permissions on the new file. This ensures that\n\t// if we have a umask set which prevents creating world-readable files,\n\t// the file will still have the correct permissions once it's renamed\n\t// into place. See #12609.\n\tif err := m.fs.Chmod(new, 0644); err != nil {\n\t\treturn fmt.Errorf(\"chmod %q in rename of %q: %w\", new, old, err)\n\t}\n\n\tif err := m.fs.Remove(old); err != nil {\n\t\terr2 := m.fs.Truncate(old)\n\t\tif err2 != nil {\n\t\t\treturn fmt.Errorf(\"remove of %q failed (%w) and so did truncate: %v\", old, err, err2)\n\t\t}\n\t}\n\tif new == resolvConf {\n\t\tm.relabelResolvConf()\n\t}\n\treturn nil\n}\n\nvar restoreconPath lazy.SyncValue[string] // path to restorecon, or \"\" if absent\n\n// relabelResolvConf restores the policy-default SELinux context on\n// /etc/resolv.conf. Best effort: only runs when SELinux is enforcing. See:\n//\n//\thttps://github.com/tailscale/tailscale/issues/20149.\nfunc (m *directManager) relabelResolvConf() {\n\tif runtime.GOOS != \"linux\" {\n\t\treturn\n\t}","sourceCodeStart":296,"sourceCodeEnd":332,"githubUrl":"https://github.com/tailscale/tailscale/blob/6e0912f97994f927632b34ae9e63b53d6516a6ac/net/dns/direct.go#L296-L332","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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'"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"if fi, err := os.Stat(oldPath); err == nil && fi.Mode()&os.ModeImmutable != 0 {\n\treturn errors.New(\"old file is immutable; clear with chattr -i\")\n}","typeGuard":null,"tryCatchPattern":"err := mgr.SetDNS(cfg)\nif err != nil && strings.Contains(err.Error(), \"so did truncate\") {\n\t// stale duplicate may exist; clean up manually then re-apply\n\tlog.Printf(\"cleanup needed: %v\", err)\n}","preventionTips":["Never chattr +i /etc/resolv.conf","Ensure write permission on the /etc directory itself","Check for stale copies after this error"],"tags":["go","tailscale","dns","filesystem","file-removal"],"backgroundTag":"resolv-conf-write-failed","analyzedSha":"6e0912f97994f927632b34ae9e63b53d6516a6ac","analyzedAt":"2026-08-18T08:17:25.280Z","contentChangedAt":"2026-08-18T08:17:25.280Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}