From 37d64afc21163c5715f7aaf9de9f5a1d42151dff Mon Sep 17 00:00:00 2001 From: lciti Date: Wed, 7 Dec 2022 03:26:29 +0000 Subject: [PATCH] fix(snapshots): Partly fix #544 by supporting setuid/setgid/sticky bits when snapshotting, restoring and fuse-mounting. (#2597) Also modified an end-to-end test to also check that these extra mode flags work when snapshotting+restoring. Manually tested fuse-mount. Co-authored-by: Luca Citi --- fs/entry.go | 3 +++ internal/fusemount/fusefs.go | 20 +++++++++++++++++++- snapshot/restore/local_fs_output.go | 5 ++--- snapshot/snapshotfs/upload.go | 2 +- tests/end_to_end_test/restore_test.go | 15 ++++++++------- 5 files changed, 33 insertions(+), 12 deletions(-) diff --git a/fs/entry.go b/fs/entry.go index 20f1013408..2fb5a7ff1c 100644 --- a/fs/entry.go +++ b/fs/entry.go @@ -9,6 +9,9 @@ import ( "github.com/pkg/errors" ) +// ModBits is a bitmask representing the mode flags supported. +const ModBits = os.ModePerm | os.ModeSetgid | os.ModeSetuid | os.ModeSticky + // ErrUnknown is returned by ErrorEntry.ErrorInfo() to indicate that type of an entry is unknown. var ErrUnknown = errors.Errorf("unknown or unsupported entry type") diff --git a/internal/fusemount/fusefs.go b/internal/fusemount/fusefs.go index feed3adbb6..85e437dafb 100644 --- a/internal/fusemount/fusefs.go +++ b/internal/fusemount/fusefs.go @@ -30,8 +30,26 @@ type fuseNode struct { entry fs.Entry } +func goModeToUnixMode(mode os.FileMode) uint32 { + unixmode := uint32(mode.Perm()) + + if mode&os.ModeSetuid != 0 { + unixmode |= 0o4000 + } + + if mode&os.ModeSetgid != 0 { + unixmode |= 0o2000 + } + + if mode&os.ModeSticky != 0 { + unixmode |= 0o1000 + } + + return unixmode +} + func populateAttributes(a *fuse.Attr, e fs.Entry) { - a.Mode = uint32(e.Mode()) & uint32(os.ModePerm) + a.Mode = goModeToUnixMode(e.Mode()) a.Size = uint64(e.Size()) a.Mtime = uint64(e.ModTime().Unix()) a.Ctime = a.Mtime diff --git a/snapshot/restore/local_fs_output.go b/snapshot/restore/local_fs_output.go index a37a5e1325..37f937064e 100644 --- a/snapshot/restore/local_fs_output.go +++ b/snapshot/restore/local_fs_output.go @@ -21,7 +21,6 @@ import ( ) const ( - modBits = os.ModePerm | os.ModeSetgid | os.ModeSetuid | os.ModeSticky outputDirMode = 0o700 // default mode to create directories in before setting their ACLs maxTimeDeltaToConsiderFileTheSame = 2 * time.Second ) @@ -273,7 +272,7 @@ func (o *FilesystemOutput) setAttributes(targetPath string, e fs.Entry, modclear // Set file permissions from e if o.shouldUpdatePermissions(le, e, modclear) { - if err = o.maybeIgnorePermissionError(osChmod(targetPath, (e.Mode()&modBits)&^modclear)); err != nil { + if err = o.maybeIgnorePermissionError(osChmod(targetPath, (e.Mode()&fs.ModBits)&^modclear)); err != nil { return errors.Wrap(err, "could not change permissions on "+targetPath) } } @@ -317,7 +316,7 @@ func (o *FilesystemOutput) shouldUpdatePermissions(local, remote fs.Entry, modcl return false } - return ((local.Mode() & modBits) &^ modclear) != (remote.Mode() & modBits) + return ((local.Mode() & fs.ModBits) &^ modclear) != (remote.Mode() & fs.ModBits) } func (o *FilesystemOutput) shouldUpdateTimes(local, remote fs.Entry) bool { diff --git a/snapshot/snapshotfs/upload.go b/snapshot/snapshotfs/upload.go index efbb7eacbe..e08bcb8e4d 100644 --- a/snapshot/snapshotfs/upload.go +++ b/snapshot/snapshotfs/upload.go @@ -454,7 +454,7 @@ func newDirEntry(md fs.Entry, fname string, oid object.ID) (*snapshot.DirEntry, return &snapshot.DirEntry{ Name: fname, Type: entryType, - Permissions: snapshot.Permissions(md.Mode() & os.ModePerm), + Permissions: snapshot.Permissions(md.Mode() & fs.ModBits), FileSize: md.Size(), ModTime: fs.UTCTimestampFromTime(md.ModTime()), UserID: md.Owner().UserID, diff --git a/tests/end_to_end_test/restore_test.go b/tests/end_to_end_test/restore_test.go index 2e5f5781dc..c1b456ca0a 100644 --- a/tests/end_to_end_test/restore_test.go +++ b/tests/end_to_end_test/restore_test.go @@ -259,7 +259,7 @@ func TestSnapshotRestore(t *testing.T) { // restoring single file e.RunAndExpectSuccess(t, "snapshot", "restore", rootID+"/single-file", filepath.Join(restoreByOIDFile, "output-file")) - verifyFileMode(t, filepath.Join(restoreByOIDFile, "output-file"), os.FileMode(0o651)) + verifyFileMode(t, filepath.Join(restoreByOIDFile, "output-file"), overriddenFilePermissions) // Restore last snapshot using the snapshot ID e.RunAndExpectSuccess(t, "snapshot", "restore", snapID, restoreDir) @@ -461,21 +461,22 @@ func TestRestoreSnapshotOfSingleFile(t *testing.T) { verifyFileMode(t, filepath.Join(restoreDir, "restored-2"), os.FileMode(0o653)) if runtime.GOOS != windowsOSName { + overriddenFilePermissions := 0o654 | os.ModeSetuid + // change source file permissions and create one more snapshot // at this poing we will have multiple snapshot manifests with one root but different attributes. - os.Chmod(sourceFile, 0o654) + os.Chmod(sourceFile, overriddenFilePermissions) e.RunAndExpectSuccess(t, "snapshot", "create", sourceFile) // when restoring by root Kopia needs to pick which manifest to apply since they are conflicting // We're passing --consistent-attributes which causes it to fail, since otherwise we'd restore arbitrary // top-level object permissions. e.RunAndExpectFailure(t, "snapshot", "restore", rootID, "--consistent-attributes", filepath.Join(restoreDir, "restored-3")) - } - // Without the flag kopia picks the attributes from the latest snapshot. - e.RunAndExpectSuccess(t, "snapshot", "restore", rootID, filepath.Join(restoreDir, "restored-3")) - - verifyFileMode(t, filepath.Join(restoreDir, "restored-3"), os.FileMode(0o654)) + // Without the flag kopia picks the attributes from the latest snapshot. + e.RunAndExpectSuccess(t, "snapshot", "restore", rootID, filepath.Join(restoreDir, "restored-3")) + verifyFileMode(t, filepath.Join(restoreDir, "restored-3"), overriddenFilePermissions) + } // restoring using snapshot ID is unambiguous and always produces file with 0o653 e.RunAndExpectSuccess(t, "snapshot", "restore", snapID, filepath.Join(restoreDir, "restored-4"))