Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 69 additions & 4 deletions guest/boot/fixhome.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,82 @@ import (
"os"
"path/filepath"
"strings"
"syscall"
)

// fixHomeOwnership recursively chowns the user's home directory so that
// files injected by rootfs hooks (which may have been written by a non-root
// host user) are owned by the sandbox user. It also enforces strict SSH
// directory permissions (0700 for .ssh/, 0600 for files inside .ssh/).
// fixHomeOwnership ensures the user's home directory and critical
// subdirectories (.ssh/) have correct ownership and permissions.
//
// When the home directory is already owned by uid:gid (the common case
// on Linux with user-namespace-backed virtiofs), only the .ssh/
// subtree is walked to enforce strict SSH permissions. This avoids a
// costly recursive chown of the entire home directory which can contain
// hundreds of thousands of files from the OCI image.
//
// A full recursive chown is only performed when the home directory
// itself has wrong ownership (e.g. macOS hosts without user namespaces
// where rootfs hooks cannot chown to the sandbox UID).
//
// This runs as PID 1 (root) inside the guest, so chown always succeeds.
func fixHomeOwnership(logger *slog.Logger, home string, uid, gid int) {
logger.Info("fixing home directory ownership", "home", home, "uid", uid, "gid", gid)

if homeAlreadyOwned(home, uid, gid) {
logger.Info("home directory already owned correctly, fixing .ssh only")
fixSSHPermissions(logger, home, uid, gid)
return
}

logger.Info("home directory has wrong ownership, running full recursive chown")
fullRecursiveChown(logger, home, uid, gid)
}

// homeAlreadyOwned checks whether the home directory itself is owned by
// the expected uid and gid.
func homeAlreadyOwned(home string, uid, gid int) bool {
info, err := os.Lstat(home)
if err != nil {
return false
}
stat, ok := info.Sys().(*syscall.Stat_t)
if !ok {
return false
}
return int(stat.Uid) == uid && int(stat.Gid) == gid
}

// fixSSHPermissions walks only the .ssh/ subtree under home, chowning
// and enforcing strict permissions (0700 dirs, 0600 files).
func fixSSHPermissions(logger *slog.Logger, home string, uid, gid int) {
sshDir := filepath.Join(home, ".ssh")
if _, err := os.Stat(sshDir); os.IsNotExist(err) {
return
}

err := filepath.WalkDir(sshDir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.Type()&fs.ModeSymlink != 0 {
return nil
}
if chownErr := os.Lchown(path, uid, gid); chownErr != nil {
logger.Warn("chown failed", "path", path, "error", chownErr)
}
perm := sshPermission(d.IsDir())
if chmodErr := os.Chmod(path, perm); chmodErr != nil {
logger.Warn("chmod failed", "path", path, "perm", perm, "error", chmodErr)
}
return nil
})
if err != nil {
logger.Warn(".ssh permission fixup incomplete", "error", err)
}
}

// fullRecursiveChown walks the entire home tree, chowning every entry
// and enforcing SSH permissions on .ssh/ paths.
func fullRecursiveChown(logger *slog.Logger, home string, uid, gid int) {
err := filepath.WalkDir(home, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
Expand Down
Loading