fix restore preserve Unix file ownership
This commit is contained in:
@@ -19,6 +19,8 @@ type restoreTargetIdentity struct {
|
|||||||
exists bool
|
exists bool
|
||||||
device uint64
|
device uint64
|
||||||
inode uint64
|
inode uint64
|
||||||
|
uid uint32
|
||||||
|
gid uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
func replaceRestoreFile(target string, contents []byte, mode os.FileMode) error {
|
func replaceRestoreFile(target string, contents []byte, mode os.FileMode) error {
|
||||||
@@ -47,7 +49,11 @@ func replaceRestoreFile(target string, contents []byte, mode os.FileMode) error
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return safeio.ErrUnsafeFile
|
return safeio.ErrUnsafeFile
|
||||||
}
|
}
|
||||||
temporary, err := writeRestoreTemporaryAt(directory, contents, mode.Perm())
|
uid, gid, err := restoreTargetOwnerAt(directory, identity)
|
||||||
|
if err != nil {
|
||||||
|
return safeio.ErrUnsafeFile
|
||||||
|
}
|
||||||
|
temporary, err := writeRestoreTemporaryAt(directory, contents, mode.Perm(), uid, gid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return safeio.ErrUnsafeFile
|
return safeio.ErrUnsafeFile
|
||||||
}
|
}
|
||||||
@@ -75,10 +81,27 @@ func inspectRestoreTargetAt(directory int, name string) (restoreTargetIdentity,
|
|||||||
if err != nil || status.Mode&unix.S_IFMT != unix.S_IFREG || status.Nlink != 1 {
|
if err != nil || status.Mode&unix.S_IFMT != unix.S_IFREG || status.Nlink != 1 {
|
||||||
return restoreTargetIdentity{}, safeio.ErrUnsafeFile
|
return restoreTargetIdentity{}, safeio.ErrUnsafeFile
|
||||||
}
|
}
|
||||||
return restoreTargetIdentity{exists: true, device: uint64(status.Dev), inode: status.Ino}, nil
|
return restoreTargetIdentity{
|
||||||
|
exists: true,
|
||||||
|
device: uint64(status.Dev),
|
||||||
|
inode: status.Ino,
|
||||||
|
uid: status.Uid,
|
||||||
|
gid: status.Gid,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeRestoreTemporaryAt(directory int, contents []byte, mode os.FileMode) (string, error) {
|
func restoreTargetOwnerAt(directory int, identity restoreTargetIdentity) (uint32, uint32, error) {
|
||||||
|
if identity.exists {
|
||||||
|
return identity.uid, identity.gid, nil
|
||||||
|
}
|
||||||
|
var status unix.Stat_t
|
||||||
|
if err := unix.Fstat(directory, &status); err != nil || status.Mode&unix.S_IFMT != unix.S_IFDIR {
|
||||||
|
return 0, 0, safeio.ErrUnsafeFile
|
||||||
|
}
|
||||||
|
return status.Uid, status.Gid, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeRestoreTemporaryAt(directory int, contents []byte, mode os.FileMode, uid, gid uint32) (string, error) {
|
||||||
for attempt := 0; attempt < 16; attempt++ {
|
for attempt := 0; attempt < 16; attempt++ {
|
||||||
random := make([]byte, 8)
|
random := make([]byte, 8)
|
||||||
if _, err := rand.Read(random); err != nil {
|
if _, err := rand.Read(random); err != nil {
|
||||||
@@ -97,7 +120,10 @@ func writeRestoreTemporaryAt(directory int, contents []byte, mode os.FileMode) (
|
|||||||
unix.Close(descriptor)
|
unix.Close(descriptor)
|
||||||
return "", safeio.ErrUnsafeFile
|
return "", safeio.ErrUnsafeFile
|
||||||
}
|
}
|
||||||
if err := file.Chmod(mode); err == nil {
|
if err := file.Chown(int(uid), int(gid)); err == nil {
|
||||||
|
err = file.Chmod(mode)
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
var written int
|
var written int
|
||||||
written, err = file.Write(contents)
|
written, err = file.Write(contents)
|
||||||
if err == nil && written != len(contents) {
|
if err == nil && written != len(contents) {
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package backup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"syscall"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
const restoreRuntimeUID = 10001
|
||||||
|
|
||||||
|
func TestReplaceRestoreFilePreservesExistingOwner(t *testing.T) {
|
||||||
|
requireRootForRestoreOwnershipTest(t)
|
||||||
|
|
||||||
|
target := filepath.Join(t.TempDir(), "trust.json")
|
||||||
|
if err := os.WriteFile(target, []byte("before"), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.Chown(target, restoreRuntimeUID, restoreRuntimeUID); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := replaceRestoreFile(target, []byte("after"), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
assertRestoreOwner(t, target, restoreRuntimeUID, restoreRuntimeUID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReplaceRestoreFileUsesParentOwnerForNewTarget(t *testing.T) {
|
||||||
|
requireRootForRestoreOwnershipTest(t)
|
||||||
|
|
||||||
|
parent := filepath.Join(t.TempDir(), "agent")
|
||||||
|
if err := os.Mkdir(parent, 0o700); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.Chown(parent, restoreRuntimeUID, restoreRuntimeUID); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
target := filepath.Join(parent, "trust.json")
|
||||||
|
|
||||||
|
if err := replaceRestoreFile(target, []byte("restored"), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
assertRestoreOwner(t, target, restoreRuntimeUID, restoreRuntimeUID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func requireRootForRestoreOwnershipTest(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
if os.Geteuid() != 0 {
|
||||||
|
t.Skip("numeric ownership assertions require root")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertRestoreOwner(t *testing.T, path string, uid, gid uint32) {
|
||||||
|
t.Helper()
|
||||||
|
info, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
status, ok := info.Sys().(*syscall.Stat_t)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("restored file has no Unix stat metadata")
|
||||||
|
}
|
||||||
|
if status.Uid != uid || status.Gid != gid {
|
||||||
|
t.Fatalf("restored owner = %d:%d, want %d:%d", status.Uid, status.Gid, uid, gid)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user