diff --git a/api/next/67002.txt b/api/next/67002.txt index 2e6b6fe662..f7c3530f59 100644 --- a/api/next/67002.txt +++ b/api/next/67002.txt @@ -1,3 +1,4 @@ pkg os, method (*Root) Chmod(string, fs.FileMode) error #67002 pkg os, method (*Root) Chown(string, int, int) error #67002 pkg os, method (*Root) Chtimes(string, time.Time, time.Time) error #67002 +pkg os, method (*Root) Lchown(string, int, int) error #67002 diff --git a/doc/next/6-stdlib/99-minor/os/67002.md b/doc/next/6-stdlib/99-minor/os/67002.md index 0f82fb31e6..629ea55ac0 100644 --- a/doc/next/6-stdlib/99-minor/os/67002.md +++ b/doc/next/6-stdlib/99-minor/os/67002.md @@ -3,3 +3,4 @@ The [os.Root] type supports the following additional methods: * [os.Root.Chmod] * [os.Root.Chown] * [os.Root.Chtimes] + * [os.Root.Lchown] diff --git a/src/os/root.go b/src/os/root.go index bcabb496bc..27bd871c17 100644 --- a/src/os/root.go +++ b/src/os/root.go @@ -159,6 +159,12 @@ func (r *Root) Chown(name string, uid, gid int) error { return rootChown(r, name, uid, gid) } +// Lchown changes the numeric uid and gid of the named file in the root. +// See [Lchown] for more details. +func (r *Root) Lchown(name string, uid, gid int) error { + return rootLchown(r, name, uid, gid) +} + // Chtimes changes the access and modification times of the named file in the root. // See [Chtimes] for more details. func (r *Root) Chtimes(name string, atime time.Time, mtime time.Time) error { diff --git a/src/os/root_noopenat.go b/src/os/root_noopenat.go index 186a382df3..46dd794739 100644 --- a/src/os/root_noopenat.go +++ b/src/os/root_noopenat.go @@ -116,6 +116,16 @@ func rootChown(r *Root, name string, uid, gid int) error { return nil } +func rootLchown(r *Root, name string, uid, gid int) error { + if err := checkPathEscapesLstat(r, name); err != nil { + return &PathError{Op: "lchownat", Path: name, Err: err} + } + if err := Lchown(joinPath(r.root.name, name), uid, gid); err != nil { + return &PathError{Op: "lchownat", Path: name, Err: underlyingError(err)} + } + return nil +} + func rootChtimes(r *Root, name string, atime time.Time, mtime time.Time) error { if err := checkPathEscapes(r, name); err != nil { return &PathError{Op: "chtimesat", Path: name, Err: err} diff --git a/src/os/root_openat.go b/src/os/root_openat.go index e28b192f4c..e47004e8ea 100644 --- a/src/os/root_openat.go +++ b/src/os/root_openat.go @@ -88,6 +88,16 @@ func rootChown(r *Root, name string, uid, gid int) error { return nil } +func rootLchown(r *Root, name string, uid, gid int) error { + _, err := doInRoot(r, name, func(parent sysfdType, name string) (struct{}, error) { + return struct{}{}, lchownat(parent, name, uid, gid) + }) + if err != nil { + return &PathError{Op: "lchownat", Path: name, Err: err} + } + return err +} + func rootChtimes(r *Root, name string, atime time.Time, mtime time.Time) error { _, err := doInRoot(r, name, func(parent sysfdType, name string) (struct{}, error) { return struct{}{}, chtimesat(parent, name, atime, mtime) diff --git a/src/os/root_unix.go b/src/os/root_unix.go index 884c1a38d9..a5ca10b0cd 100644 --- a/src/os/root_unix.go +++ b/src/os/root_unix.go @@ -166,6 +166,12 @@ func chownat(parent int, name string, uid, gid int) error { }) } +func lchownat(parent int, name string, uid, gid int) error { + return ignoringEINTR(func() error { + return unix.Fchownat(parent, name, uid, gid, unix.AT_SYMLINK_NOFOLLOW) + }) +} + func chtimesat(parent int, name string, atime time.Time, mtime time.Time) error { return afterResolvingSymlink(parent, name, func() error { return ignoringEINTR(func() error { diff --git a/src/os/root_unix_test.go b/src/os/root_unix_test.go index 280efc6875..0562af1f5e 100644 --- a/src/os/root_unix_test.go +++ b/src/os/root_unix_test.go @@ -9,6 +9,7 @@ package os_test import ( "fmt" "os" + "path/filepath" "runtime" "syscall" "testing" @@ -50,6 +51,46 @@ func TestRootChown(t *testing.T) { } } +func TestRootLchown(t *testing.T) { + if runtime.GOOS == "wasip1" { + t.Skip("Lchown not supported on " + runtime.GOOS) + } + + // Look up the current default uid/gid. + f := newFile(t) + dir, err := f.Stat() + if err != nil { + t.Fatal(err) + } + sys := dir.Sys().(*syscall.Stat_t) + + groups, err := os.Getgroups() + if err != nil { + t.Fatalf("getgroups: %v", err) + } + groups = append(groups, os.Getgid()) + for _, test := range rootTestCases { + test.run(t, func(t *testing.T, target string, root *os.Root) { + wantError := test.wantError + if test.ltarget != "" { + wantError = false + target = filepath.Join(root.Name(), test.ltarget) + } else if target != "" { + if err := os.WriteFile(target, nil, 0o666); err != nil { + t.Fatal(err) + } + } + for _, gid := range groups { + err := root.Lchown(test.open, -1, gid) + if errEndsTest(t, err, wantError, "root.Lchown(%q, -1, %v)", test.open, gid) { + return + } + checkUidGid(t, target, int(sys.Uid), gid) + } + }) + } +} + func TestRootConsistencyChown(t *testing.T) { if runtime.GOOS == "wasip1" { t.Skip("Chown not supported on " + runtime.GOOS) @@ -85,3 +126,39 @@ func TestRootConsistencyChown(t *testing.T) { }) } } + +func TestRootConsistencyLchown(t *testing.T) { + if runtime.GOOS == "wasip1" { + t.Skip("Lchown not supported on " + runtime.GOOS) + } + groups, err := os.Getgroups() + if err != nil { + t.Fatalf("getgroups: %v", err) + } + var gid int + if len(groups) == 0 { + gid = os.Getgid() + } else { + gid = groups[0] + } + for _, test := range rootConsistencyTestCases { + test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) { + lchown := os.Lchown + lstat := os.Lstat + if r != nil { + lchown = r.Lchown + lstat = r.Lstat + } + err := lchown(path, -1, gid) + if err != nil { + return "", err + } + fi, err := lstat(path) + if err != nil { + return "", err + } + sys := fi.Sys().(*syscall.Stat_t) + return fmt.Sprintf("%v %v", sys.Uid, sys.Gid), nil + }) + } +} diff --git a/src/os/root_windows.go b/src/os/root_windows.go index eed81ea51b..c56946d0d5 100644 --- a/src/os/root_windows.go +++ b/src/os/root_windows.go @@ -281,6 +281,10 @@ func chownat(parent syscall.Handle, name string, uid, gid int) error { return syscall.EWINDOWS // matches syscall.Chown } +func lchownat(parent syscall.Handle, name string, uid, gid int) error { + return syscall.EWINDOWS // matches syscall.Lchown +} + func mkdirat(dirfd syscall.Handle, name string, perm FileMode) error { return windows.Mkdirat(dirfd, name, syscallMode(perm)) }