Skip to content

Commit 5de47e7

Browse files
JAORMXclaude
andcommitted
Tighten BestEffortLchown to only swallow permission errors
Previously, any non-permission error from os.Lchown was logged at warn level and then silently dropped — a failure mode where the file's owner silently ended up wrong while the caller saw no error. Tighten the contract so only permission-denied errors (the expected non-root case) are absorbed; everything else (ENOENT, EROFS, EIO, ...) propagates to the caller. Tests cover both paths: a non-existent path returning ENOENT surfaces an error; a non-root chown to a foreign UID returning EPERM returns nil. The EPERM test is skipped when running as root since root can chown to any UID. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 7783f0c commit 5de47e7

2 files changed

Lines changed: 37 additions & 8 deletions

File tree

hooks/hooks.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -211,20 +211,21 @@ func shellEscape(s string) string {
211211
return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'"
212212
}
213213

214-
// BestEffortLchown attempts os.Lchown and silently ignores permission errors,
215-
// returning nil. On macOS non-root users cannot chown to a different UID;
216-
// the guest init will fix ownership at boot time. Non-permission errors are
217-
// logged at warn level and also swallowed. Callers that need strict chown
218-
// should call os.Lchown directly instead.
214+
// BestEffortLchown attempts os.Lchown and swallows permission errors (EPERM
215+
// and EACCES). On non-root Linux and on macOS the hook process cannot lchown
216+
// to a different UID; in those cases the override_stat xattr carries the
217+
// intended ownership to the guest, and the guest init fixes up ownership at
218+
// boot. Errors other than permission denied (e.g. ENOENT, EROFS, EIO) are
219+
// returned to the caller rather than silently dropped.
219220
// Lchown is used instead of Chown to avoid following symlinks in the rootfs.
220221
func BestEffortLchown(path string, uid, gid int) error {
221222
if err := os.Lchown(path, uid, gid); err != nil {
222223
if !os.IsPermission(err) {
223-
slog.Warn("lchown failed", "path", path, "uid", uid, "gid", gid, "err", err)
224+
return fmt.Errorf("lchown %s: %w", path, err)
224225
}
226+
slog.Debug("lchown permission denied; relying on xattr + guest fixup",
227+
"path", path, "uid", uid, "gid", gid)
225228
}
226-
// On macOS, set the override_stat xattr so libkrun's virtiofs reports
227-
// correct ownership to the guest (non-root cannot Lchown to a different UID).
228229
xattr.SetOverrideStatFromPath(path, uid, gid)
229230
return nil
230231
}

hooks/hooks_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -658,6 +658,34 @@ func TestStageSymlink(t *testing.T) {
658658
assert.Equal(t, outside, dest)
659659
}
660660

661+
func TestBestEffortLchown_PropagatesNonPermissionErrors(t *testing.T) {
662+
t.Parallel()
663+
664+
// ENOENT from a non-existent path is not a permission error; the function
665+
// must return it rather than silently swallowing.
666+
missing := filepath.Join(t.TempDir(), "does-not-exist")
667+
err := BestEffortLchown(missing, 1000, 1000)
668+
require.Error(t, err)
669+
assert.Contains(t, err.Error(), "lchown")
670+
}
671+
672+
func TestBestEffortLchown_SwallowsPermissionErrors(t *testing.T) {
673+
t.Parallel()
674+
675+
if os.Geteuid() == 0 {
676+
t.Skip("requires non-root: root can chown to any UID, so no EPERM")
677+
}
678+
679+
// Create a file we own; chowning to a UID we don't own should fail with
680+
// EPERM on Linux and macOS as a non-root user. BestEffortLchown must
681+
// swallow that specific error.
682+
target := filepath.Join(t.TempDir(), "target")
683+
require.NoError(t, os.WriteFile(target, []byte("x"), 0o600))
684+
685+
err := BestEffortLchown(target, 1, 1)
686+
require.NoError(t, err, "permission error must be swallowed, got: %v", err)
687+
}
688+
661689
func TestShellEscape(t *testing.T) {
662690
t.Parallel()
663691

0 commit comments

Comments
 (0)