diff --git a/api/next/67002.txt b/api/next/67002.txt index 06119c0e754589..216d3a3afeb606 100644 --- a/api/next/67002.txt +++ b/api/next/67002.txt @@ -1 +1,2 @@ pkg os, method (*Root) Chmod(string, fs.FileMode) error #67002 +pkg os, method (*Root) Chown(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 a0751c30e2fec3..04ff6d5de0fec0 100644 --- a/doc/next/6-stdlib/99-minor/os/67002.md +++ b/doc/next/6-stdlib/99-minor/os/67002.md @@ -1,3 +1,4 @@ The [os.Root] type supports the following additional methods: * [os.Root.Chmod] + * [os.Root.Chown] diff --git a/src/internal/syscall/unix/asm_darwin.s b/src/internal/syscall/unix/asm_darwin.s index de6e01ee4a4fe3..0f28cd1e393bc7 100644 --- a/src/internal/syscall/unix/asm_darwin.s +++ b/src/internal/syscall/unix/asm_darwin.s @@ -26,3 +26,4 @@ TEXT ·libc_faccessat_trampoline(SB),NOSPLIT,$0-0; JMP libc_faccessat(SB) TEXT ·libc_readlinkat_trampoline(SB),NOSPLIT,$0-0; JMP libc_readlinkat(SB) TEXT ·libc_mkdirat_trampoline(SB),NOSPLIT,$0-0; JMP libc_mkdirat(SB) TEXT ·libc_fchmodat_trampoline(SB),NOSPLIT,$0-0; JMP libc_fchmodat(SB) +TEXT ·libc_fchownat_trampoline(SB),NOSPLIT,$0-0; JMP libc_fchownat(SB) diff --git a/src/internal/syscall/unix/asm_openbsd.s b/src/internal/syscall/unix/asm_openbsd.s index 306ef4664d57d8..b804a52714c3b4 100644 --- a/src/internal/syscall/unix/asm_openbsd.s +++ b/src/internal/syscall/unix/asm_openbsd.s @@ -16,3 +16,5 @@ TEXT ·libc_mkdirat_trampoline(SB),NOSPLIT,$0-0 JMP libc_mkdirat(SB) TEXT ·libc_fchmodat_trampoline(SB),NOSPLIT,$0-0 JMP libc_fchmodat(SB) +TEXT ·libc_fchownat_trampoline(SB),NOSPLIT,$0-0 + JMP libc_fchownat(SB) diff --git a/src/internal/syscall/unix/at.go b/src/internal/syscall/unix/at.go index 2a29dd6a5a3ac7..794f8ace14f20d 100644 --- a/src/internal/syscall/unix/at.go +++ b/src/internal/syscall/unix/at.go @@ -96,3 +96,21 @@ func Fchmodat(dirfd int, path string, mode uint32, flags int) error { } return nil } + +func Fchownat(dirfd int, path string, uid, gid int, flags int) error { + p, err := syscall.BytePtrFromString(path) + if err != nil { + return err + } + _, _, errno := syscall.Syscall6(fchownatTrap, + uintptr(dirfd), + uintptr(unsafe.Pointer(p)), + uintptr(uid), + uintptr(gid), + uintptr(flags), + 0) + if errno != 0 { + return errno + } + return nil +} diff --git a/src/internal/syscall/unix/at_aix.go b/src/internal/syscall/unix/at_aix.go index e679efc34459d8..aa188cdb76f6bf 100644 --- a/src/internal/syscall/unix/at_aix.go +++ b/src/internal/syscall/unix/at_aix.go @@ -5,6 +5,7 @@ package unix //go:cgo_import_dynamic libc_fchmodat fchmodat "libc.a/shr_64.o" +//go:cgo_import_dynamic libc_fchownat fchownat "libc.a/shr_64.o" //go:cgo_import_dynamic libc_fstatat fstatat "libc.a/shr_64.o" //go:cgo_import_dynamic libc_openat openat "libc.a/shr_64.o" //go:cgo_import_dynamic libc_unlinkat unlinkat "libc.a/shr_64.o" diff --git a/src/internal/syscall/unix/at_darwin.go b/src/internal/syscall/unix/at_darwin.go index 759b0943f558c3..75d7b455691859 100644 --- a/src/internal/syscall/unix/at_darwin.go +++ b/src/internal/syscall/unix/at_darwin.go @@ -80,3 +80,25 @@ func Fchmodat(dirfd int, path string, mode uint32, flags int) error { } return nil } + +func libc_fchownat_trampoline() + +//go:cgo_import_dynamic libc_fchownat fchownat "/usr/lib/libSystem.B.dylib" + +func Fchownat(dirfd int, path string, uid, gid int, flags int) error { + p, err := syscall.BytePtrFromString(path) + if err != nil { + return err + } + _, _, errno := syscall_syscall6(abi.FuncPCABI0(libc_fchownat_trampoline), + uintptr(dirfd), + uintptr(unsafe.Pointer(p)), + uintptr(uid), + uintptr(gid), + uintptr(flags), + 0) + if errno != 0 { + return errno + } + return nil +} diff --git a/src/internal/syscall/unix/at_libc.go b/src/internal/syscall/unix/at_libc.go index f88e09d31db417..137e0e093651ed 100644 --- a/src/internal/syscall/unix/at_libc.go +++ b/src/internal/syscall/unix/at_libc.go @@ -17,6 +17,7 @@ import ( //go:linkname procReadlinkat libc_readlinkat //go:linkname procMkdirat libc_mkdirat //go:linkname procFchmodat libc_fchmodat +//go:linkname procFchownat libc_chownat var ( procFstatat, @@ -24,7 +25,8 @@ var ( procUnlinkat, procReadlinkat, procMkdirat, - procFchmodat uintptr + procFchmodat, + procFchownat uintptr ) func Unlinkat(dirfd int, path string, flags int) error { @@ -126,3 +128,21 @@ func Fchmodat(dirfd int, path string, mode uint32, flags int) error { } return nil } + +func Fchownat(dirfd int, path string, uid, gid int, flags int) error { + p, err := syscall.BytePtrFromString(path) + if err != nil { + return err + } + _, _, errno := syscall6(uintptr(unsafe.Pointer(&procFchownat)), 4, + uintptr(dirfd), + uintptr(unsafe.Pointer(p)), + uintptr(uid), + uintptr(gid), + uintptr(flags), + 0) + if errno != 0 { + return errno + } + return nil +} diff --git a/src/internal/syscall/unix/at_openbsd.go b/src/internal/syscall/unix/at_openbsd.go index 26ca70322b7f15..771cb063e0cf40 100644 --- a/src/internal/syscall/unix/at_openbsd.go +++ b/src/internal/syscall/unix/at_openbsd.go @@ -71,3 +71,25 @@ func Fchmodat(dirfd int, path string, mode uint32, flags int) error { } return nil } + +//go:cgo_import_dynamic libc_fchownat fchownat "libc.so" + +func libc_fchownat_trampoline() + +func Fchownat(dirfd int, path string, uid, gid int, flags int) error { + p, err := syscall.BytePtrFromString(path) + if err != nil { + return err + } + _, _, errno := syscall_syscall6(abi.FuncPCABI0(libc_fchmodat_trampoline), + uintptr(dirfd), + uintptr(unsafe.Pointer(p)), + uintptr(uid), + uintptr(gid), + uintptr(flags), + 0) + if errno != 0 { + return errno + } + return nil +} diff --git a/src/internal/syscall/unix/at_solaris.go b/src/internal/syscall/unix/at_solaris.go index a4910f1003aaae..f84e8e35daa11c 100644 --- a/src/internal/syscall/unix/at_solaris.go +++ b/src/internal/syscall/unix/at_solaris.go @@ -14,6 +14,7 @@ func rawSyscall6(trap, nargs, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, e //go:cgo_import_dynamic libc_faccessat faccessat "libc.so" //go:cgo_import_dynamic libc_fchmodat fchmodat "libc.so" +//go:cgo_import_dynamic libc_fchownat fchownat "libc.so" //go:cgo_import_dynamic libc_fstatat fstatat "libc.so" //go:cgo_import_dynamic libc_openat openat "libc.so" //go:cgo_import_dynamic libc_unlinkat unlinkat "libc.so" diff --git a/src/internal/syscall/unix/at_sysnum_dragonfly.go b/src/internal/syscall/unix/at_sysnum_dragonfly.go index 84c60c47b8ac5c..1e89e97f388077 100644 --- a/src/internal/syscall/unix/at_sysnum_dragonfly.go +++ b/src/internal/syscall/unix/at_sysnum_dragonfly.go @@ -13,6 +13,7 @@ const ( readlinkatTrap uintptr = syscall.SYS_READLINKAT mkdiratTrap uintptr = syscall.SYS_MKDIRAT fchmodatTrap uintptr = syscall.SYS_FCHMODAT + fchownatTrap uintptr = syscall.SYS_FCHOWNAT AT_EACCESS = 0x4 AT_FDCWD = 0xfffafdcd diff --git a/src/internal/syscall/unix/at_sysnum_freebsd.go b/src/internal/syscall/unix/at_sysnum_freebsd.go index 22ff4e7e895c58..59a8c2ce5a21c1 100644 --- a/src/internal/syscall/unix/at_sysnum_freebsd.go +++ b/src/internal/syscall/unix/at_sysnum_freebsd.go @@ -20,4 +20,5 @@ const ( readlinkatTrap uintptr = syscall.SYS_READLINKAT mkdiratTrap uintptr = syscall.SYS_MKDIRAT fchmodatTrap uintptr = syscall.SYS_FCHMODAT + fchownatTrap uintptr = syscall.SYS_FCHOWNAT ) diff --git a/src/internal/syscall/unix/at_sysnum_linux.go b/src/internal/syscall/unix/at_sysnum_linux.go index 8fba319cab7dbd..35cc4307e92f3d 100644 --- a/src/internal/syscall/unix/at_sysnum_linux.go +++ b/src/internal/syscall/unix/at_sysnum_linux.go @@ -12,6 +12,7 @@ const ( readlinkatTrap uintptr = syscall.SYS_READLINKAT mkdiratTrap uintptr = syscall.SYS_MKDIRAT fchmodatTrap uintptr = syscall.SYS_FCHMODAT + fchownatTrap uintptr = syscall.SYS_FCHOWNAT ) const ( diff --git a/src/internal/syscall/unix/at_sysnum_netbsd.go b/src/internal/syscall/unix/at_sysnum_netbsd.go index f2b7a4f9ebfe8a..bb946b6581d253 100644 --- a/src/internal/syscall/unix/at_sysnum_netbsd.go +++ b/src/internal/syscall/unix/at_sysnum_netbsd.go @@ -13,6 +13,7 @@ const ( readlinkatTrap uintptr = syscall.SYS_READLINKAT mkdiratTrap uintptr = syscall.SYS_MKDIRAT fchmodatTrap uintptr = syscall.SYS_FCHMODAT + fchownatTrap uintptr = syscall.SYS_FCHOWNAT ) const ( diff --git a/src/internal/syscall/unix/at_wasip1.go b/src/internal/syscall/unix/at_wasip1.go index 7289317110f855..3fdc95436c2dc1 100644 --- a/src/internal/syscall/unix/at_wasip1.go +++ b/src/internal/syscall/unix/at_wasip1.go @@ -106,6 +106,11 @@ func Fchmodat(dirfd int, path string, mode uint32, flags int) error { return syscall.ENOSYS } +func Fchownat(dirfd int, path string, uid, gid int, flags int) error { + // WASI preview 1 doesn't support changing file ownership. + return syscall.ENOSYS +} + //go:wasmimport wasi_snapshot_preview1 path_create_directory //go:noescape func path_create_directory(fd int32, path *byte, pathLen size) syscall.Errno diff --git a/src/os/root.go b/src/os/root.go index cd26144ab7410f..fd3b603ed83805 100644 --- a/src/os/root.go +++ b/src/os/root.go @@ -151,6 +151,12 @@ func (r *Root) Mkdir(name string, perm FileMode) error { return rootMkdir(r, name, perm) } +// Chown changes the numeric uid and gid of the named file in the root. +// See [Chown] for more details. +func (r *Root) Chown(name string, uid, gid int) error { + return rootChown(r, name, uid, gid) +} + // Remove removes the named file or (empty) directory in the root. // See [Remove] for more details. func (r *Root) Remove(name string) error { diff --git a/src/os/root_noopenat.go b/src/os/root_noopenat.go index 819486f289f397..919e78c777825d 100644 --- a/src/os/root_noopenat.go +++ b/src/os/root_noopenat.go @@ -105,6 +105,16 @@ func rootChmod(r *Root, name string, mode FileMode) error { return nil } +func rootChown(r *Root, name string, uid, gid int) error { + if err := checkPathEscapes(r, name); err != nil { + return &PathError{Op: "chownat", Path: name, Err: err} + } + if err := Chown(joinPath(r.root.name, name), uid, gid); err != nil { + return &PathError{Op: "chownat", Path: name, Err: underlyingError(err)} + } + return nil +} + func rootMkdir(r *Root, name string, perm FileMode) error { if err := checkPathEscapes(r, name); err != nil { return &PathError{Op: "mkdirat", Path: name, Err: err} diff --git a/src/os/root_openat.go b/src/os/root_openat.go index d98d2e36752f4b..65d3eacf4df98b 100644 --- a/src/os/root_openat.go +++ b/src/os/root_openat.go @@ -77,6 +77,16 @@ func rootChmod(r *Root, name string, mode FileMode) error { return nil } +func rootChown(r *Root, name string, uid, gid int) error { + _, err := doInRoot(r, name, func(parent sysfdType, name string) (struct{}, error) { + return struct{}{}, chownat(parent, name, uid, gid) + }) + if err != nil { + return &PathError{Op: "chownat", Path: name, Err: err} + } + return err +} + func rootMkdir(r *Root, name string, perm FileMode) error { _, err := doInRoot(r, name, func(parent sysfdType, name string) (struct{}, error) { return struct{}{}, mkdirat(parent, name, perm) diff --git a/src/os/root_unix.go b/src/os/root_unix.go index 06da8da15ec0d2..76d6b74eb7364b 100644 --- a/src/os/root_unix.go +++ b/src/os/root_unix.go @@ -157,6 +157,14 @@ func chmodat(parent int, name string, mode FileMode) error { }) } +func chownat(parent int, name string, uid, gid int) error { + return afterResolvingSymlink(parent, name, func() error { + return ignoringEINTR(func() error { + return unix.Fchownat(parent, name, uid, gid, unix.AT_SYMLINK_NOFOLLOW) + }) + }) +} + func mkdirat(fd int, name string, perm FileMode) error { return ignoringEINTR(func() error { return unix.Mkdirat(fd, name, syscallMode(perm)) diff --git a/src/os/root_unix_test.go b/src/os/root_unix_test.go new file mode 100644 index 00000000000000..280efc68751ed3 --- /dev/null +++ b/src/os/root_unix_test.go @@ -0,0 +1,87 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build unix || (js && wasm) || wasip1 + +package os_test + +import ( + "fmt" + "os" + "runtime" + "syscall" + "testing" +) + +func TestRootChown(t *testing.T) { + if runtime.GOOS == "wasip1" { + t.Skip("Chown 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) { + if target != "" { + if err := os.WriteFile(target, nil, 0o666); err != nil { + t.Fatal(err) + } + } + for _, gid := range groups { + err := root.Chown(test.open, -1, gid) + if errEndsTest(t, err, test.wantError, "root.Chown(%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) + } + 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) { + chown := os.Chown + lstat := os.Lstat + if r != nil { + chown = r.Chown + lstat = r.Lstat + } + err := chown(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 9b57d5648e6956..4f391cb2a7d9fd 100644 --- a/src/os/root_windows.go +++ b/src/os/root_windows.go @@ -276,6 +276,10 @@ func chmodat(parent syscall.Handle, name string, mode FileMode) error { return windows.SetFileInformationByHandle(h, windows.FileBasicInfo, unsafe.Pointer(&fbi), uint32(unsafe.Sizeof(fbi))) } +func chownat(parent syscall.Handle, name string, uid, gid int) error { + return syscall.EWINDOWS // matches syscall.Chown +} + func mkdirat(dirfd syscall.Handle, name string, perm FileMode) error { return windows.Mkdirat(dirfd, name, syscallMode(perm)) }