From 1a97a80a81277b12d30cd74ee13da5b9f626ccb7 Mon Sep 17 00:00:00 2001 From: sneak Date: Sun, 28 Jun 2026 07:48:28 +0200 Subject: [PATCH] Skip chown when restore runs as non-root; warn at end chown(2) requires root on every Unix-ish kernel. Restoring 39k files as a non-root user produces 39k EPERM syscalls plus 39k matching debug log lines, all for an operation that can't possibly succeed. Skip the syscall entirely when euid != 0, and emit one warning at the end of the restore so the user knows the on-disk UID/GID will reflect the running user rather than the original owner. --- internal/vaultik/restore.go | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/internal/vaultik/restore.go b/internal/vaultik/restore.go index 3499e13..bb7715c 100644 --- a/internal/vaultik/restore.go +++ b/internal/vaultik/restore.go @@ -126,6 +126,10 @@ func (v *Vaultik) Restore(opts *RestoreOptions) error { v.UI.Duration(result.Duration), ) + if os.Geteuid() != 0 { + v.UI.Warning("Restore did not preserve file ownership: chown(2) requires root. Re-run as root (e.g. with sudo) if you need original UID/GID preserved.") + } + if result.FilesFailed > 0 { v.UI.Warning("%d file(s) failed to restore:", result.FilesFailed) for _, path := range result.FailedFiles { @@ -247,6 +251,7 @@ func (v *Vaultik) restoreAllFiles( blobCache: blobCache, sweeper: sweeper, result: result, + runningAsRoot: os.Geteuid() == 0, } // Periodic progress output, matching the snapshot create cadence. @@ -536,6 +541,13 @@ type restoreSession struct { blobCache *blobDiskCache sweeper *restoreSweeper result *RestoreResult + // runningAsRoot gates chown(2). On every Unix-ish kernel, only + // root can chown a file to an arbitrary UID/GID — non-root chown + // always fails with EPERM. Attempting it anyway produces N + // guaranteed-failed syscalls + N noisy debug lines, so we skip + // the call entirely as non-root and emit one warning at the end + // of the restore explaining that ownership was not preserved. + runningAsRoot bool } // restoreFile dispatches to the right per-kind restorer. @@ -580,9 +592,11 @@ func (s *restoreSession) restoreDirectory(file *database.File, targetPath string if err := s.v.Fs.Chmod(targetPath, os.FileMode(file.Mode)); err != nil { log.Debug("Failed to set directory permissions", "path", targetPath, "error", err) } - if _, ok := s.v.Fs.(*afero.OsFs); ok { - if err := os.Chown(targetPath, int(file.UID), int(file.GID)); err != nil { - log.Debug("Failed to set directory ownership", "path", targetPath, "error", err) + if s.runningAsRoot { + if _, ok := s.v.Fs.(*afero.OsFs); ok { + if err := os.Chown(targetPath, int(file.UID), int(file.GID)); err != nil { + log.Debug("Failed to set directory ownership", "path", targetPath, "error", err) + } } } if err := s.v.Fs.Chtimes(targetPath, file.MTime, file.MTime); err != nil { @@ -671,9 +685,11 @@ func (s *restoreSession) restoreRegularFile(file *database.File, targetPath stri if err := s.v.Fs.Chmod(targetPath, os.FileMode(file.Mode)); err != nil { log.Debug("Failed to set file permissions", "path", targetPath, "error", err) } - if _, ok := s.v.Fs.(*afero.OsFs); ok { - if err := os.Chown(targetPath, int(file.UID), int(file.GID)); err != nil { - log.Debug("Failed to set file ownership", "path", targetPath, "error", err) + if s.runningAsRoot { + if _, ok := s.v.Fs.(*afero.OsFs); ok { + if err := os.Chown(targetPath, int(file.UID), int(file.GID)); err != nil { + log.Debug("Failed to set file ownership", "path", targetPath, "error", err) + } } } if err := s.v.Fs.Chtimes(targetPath, file.MTime, file.MTime); err != nil {