diff --git a/src/os/file_anyos.go b/src/os/file_anyos.go index 1483f11c2a..da70d72847 100644 --- a/src/os/file_anyos.go +++ b/src/os/file_anyos.go @@ -147,6 +147,20 @@ func Chmod(name string, mode FileMode) error { return nil } +// Chown changes the numeric uid and gid of the named file. +// If the file is a symbolic link, it changes the uid and gid of the link's target. +// A uid or gid of -1 means to not change that value. +// If there is an error, it will be of type *PathError. +func Chown(name string, uid, gid int) error { + e := ignoringEINTR(func() error { + return syscall.Chown(name, uid, gid) + }) + if e != nil { + return &PathError{Op: "chown", Path: name, Err: e} + } + return nil +} + // ignoringEINTR makes a function call and repeats it if it returns an // EINTR error. This appears to be required even though we install all // signal handlers with SA_RESTART: see #22838, #38033, #38836, #40846. diff --git a/src/os/os_chmod_test.go b/src/os/os_chmod_test.go index 3db6467add..ad151abb03 100644 --- a/src/os/os_chmod_test.go +++ b/src/os/os_chmod_test.go @@ -9,12 +9,15 @@ package os_test import ( + "errors" + "io/fs" . "os" "runtime" "testing" ) func TestChmod(t *testing.T) { + // Chmod f := newFile("TestChmod", t) defer Remove(f.Name()) defer f.Close() @@ -28,4 +31,42 @@ func TestChmod(t *testing.T) { t.Fatalf("chmod %s %#o: %s", f.Name(), fm, err) } checkMode(t, f.Name(), fm) + +} + +// Since testing syscalls requires a static, predictable environment that has to be controlled +// by the CI, we don't test for success but for failures and verify that the error messages are as expected. +// EACCES is returned when the user does not have the required permissions to change the ownership of the file +// ENOENT is returned when the file does not exist +// ENOTDIR is returned when the file is not a directory +func TestChownErr(t *testing.T) { + if runtime.GOOS == "windows" || runtime.GOOS == "plan9" { + t.Log("skipping on " + runtime.GOOS) + return + } + + var ( + TEST_UID_ROOT = 0 + TEST_GID_ROOT = 0 + ) + + f := newFile("TestChown", t) + defer Remove(f.Name()) + defer f.Close() + + // EACCES + if err := Chown(f.Name(), TEST_UID_ROOT, TEST_GID_ROOT); err != nil { + errCmp := fs.PathError{Op: "chown", Path: f.Name(), Err: errors.New("operation not permitted")} + if errors.Is(err, &errCmp) { + t.Fatalf("chown(%s, uid=%v, gid=%v): got '%v', want 'operation not permitted'", f.Name(), TEST_UID_ROOT, TEST_GID_ROOT, err) + } + } + + // ENOENT + if err := Chown("invalid", Geteuid(), Getgid()); err != nil { + errCmp := fs.PathError{Op: "chown", Path: "invalid", Err: errors.New("no such file or directory")} + if errors.Is(err, &errCmp) { + t.Fatalf("chown(%s, uid=%v, gid=%v): got '%v', want 'no such file or directory'", f.Name(), Geteuid(), Getegid(), err) + } + } } diff --git a/src/syscall/syscall_libc.go b/src/syscall/syscall_libc.go index eed18cbd9b..07a8acb0b2 100644 --- a/src/syscall/syscall_libc.go +++ b/src/syscall/syscall_libc.go @@ -163,6 +163,15 @@ func Unlink(path string) (err error) { return } +func Chown(path string, uid, gid int) (err error) { + data := cstring(path) + fail := int(libc_chown(&data[0], uid, gid)) + if fail < 0 { + err = getErrno() + } + return +} + func Faccessat(dirfd int, path string, mode uint32, flags int) (err error) func Kill(pid int, sig Signal) (err error) { @@ -357,6 +366,11 @@ func libc_chdir(pathname *byte) int32 //export chmod func libc_chmod(pathname *byte, mode uint32) int32 +// int chown(const char *pathname, uid_t owner, gid_t group); +// +//export chown +func libc_chown(pathname *byte, owner, group int) int32 + // int mkdir(const char *pathname, mode_t mode); // //export mkdir