]> Cypherpunks repositories - gostls13.git/commitdiff
os: add Root.Chmod
authorDamien Neil <dneil@google.com>
Thu, 30 Jan 2025 23:53:06 +0000 (15:53 -0800)
committerGopher Robot <gobot@golang.org>
Mon, 10 Feb 2025 23:33:35 +0000 (15:33 -0800)
For #67002

Change-Id: Id6c3a2096bd10f5f5f6921a0441dc6d9e6cdeb3b
Reviewed-on: https://go-review.googlesource.com/c/go/+/645718
Commit-Queue: Damien Neil <dneil@google.com>
Reviewed-by: Ian Lance Taylor <iant@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Auto-Submit: Damien Neil <dneil@google.com>

22 files changed:
api/next/67002.txt [new file with mode: 0644]
doc/next/6-stdlib/99-minor/os/67002.md [new file with mode: 0644]
src/internal/syscall/unix/asm_darwin.s
src/internal/syscall/unix/asm_openbsd.s
src/internal/syscall/unix/at.go
src/internal/syscall/unix/at_darwin.go
src/internal/syscall/unix/at_libc.go
src/internal/syscall/unix/at_openbsd.go
src/internal/syscall/unix/at_sysnum_dragonfly.go
src/internal/syscall/unix/at_sysnum_freebsd.go
src/internal/syscall/unix/at_sysnum_linux.go
src/internal/syscall/unix/at_sysnum_netbsd.go
src/internal/syscall/unix/at_wasip1.go
src/internal/syscall/windows/at_windows.go
src/internal/syscall/windows/at_windows_test.go
src/os/root.go
src/os/root_noopenat.go
src/os/root_openat.go
src/os/root_test.go
src/os/root_unix.go
src/os/root_windows.go
src/syscall/types_windows.go

diff --git a/api/next/67002.txt b/api/next/67002.txt
new file mode 100644 (file)
index 0000000..06119c0
--- /dev/null
@@ -0,0 +1 @@
+pkg os, method (*Root) Chmod(string, fs.FileMode) error #67002
diff --git a/doc/next/6-stdlib/99-minor/os/67002.md b/doc/next/6-stdlib/99-minor/os/67002.md
new file mode 100644 (file)
index 0000000..a0751c3
--- /dev/null
@@ -0,0 +1,3 @@
+The [os.Root] type supports the following additional methods:
+
+  * [os.Root.Chmod]
index b96eb1e80752fa6dff1c23cc16f7089f4f0791de..de6e01ee4a4fe3c70140fc392903e4e86cfea69b 100644 (file)
@@ -25,3 +25,4 @@ TEXT ·libc_sysconf_trampoline(SB),NOSPLIT,$0-0; JMP libc_sysconf(SB)
 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)
index 90f6831e4e53dccb60f04912d6e5c70037605c6f..306ef4664d57d84b131b47c7fc8f62156ad4b7dd 100644 (file)
@@ -14,3 +14,5 @@ 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)
index 27a798e0461d70d69441fd8a066eb3c7d87207ac..2a29dd6a5a3ac777d622998f48f3310d7c374606 100644 (file)
@@ -79,3 +79,20 @@ func Mkdirat(dirfd int, path string, mode uint32) error {
        }
        return nil
 }
+
+func Fchmodat(dirfd int, path string, mode uint32, flags int) error {
+       p, err := syscall.BytePtrFromString(path)
+       if err != nil {
+               return err
+       }
+       _, _, errno := syscall.Syscall6(fchmodatTrap,
+               uintptr(dirfd),
+               uintptr(unsafe.Pointer(p)),
+               uintptr(mode),
+               uintptr(flags),
+               0, 0)
+       if errno != 0 {
+               return errno
+       }
+       return nil
+}
index dbcae5a7889a0af4d653a72458b476a8730df382..759b0943f558c38451ffdb4451473d7dc4991264 100644 (file)
@@ -58,3 +58,25 @@ func Mkdirat(dirfd int, path string, mode uint32) error {
        }
        return nil
 }
+
+func libc_fchmodat_trampoline()
+
+//go:cgo_import_dynamic libc_fchmodat fchmodat "/usr/lib/libSystem.B.dylib"
+
+func Fchmodat(dirfd int, path string, mode uint32, 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(mode),
+               uintptr(flags),
+               0,
+               0)
+       if errno != 0 {
+               return errno
+       }
+       return nil
+}
index faf38be602af659cec8ab5f46d434ede97ca0298..f88e09d31db417e49df6575ce02d69a8e23c7781 100644 (file)
@@ -16,13 +16,15 @@ import (
 //go:linkname procUnlinkat libc_unlinkat
 //go:linkname procReadlinkat libc_readlinkat
 //go:linkname procMkdirat libc_mkdirat
+//go:linkname procFchmodat libc_fchmodat
 
 var (
        procFstatat,
        procOpenat,
        procUnlinkat,
        procReadlinkat,
-       procMkdirat uintptr
+       procMkdirat,
+       procFchmodat uintptr
 )
 
 func Unlinkat(dirfd int, path string, flags int) error {
@@ -107,3 +109,20 @@ func Mkdirat(dirfd int, path string, mode uint32) error {
        }
        return nil
 }
+
+func Fchmodat(dirfd int, path string, mode uint32, flags int) error {
+       p, err := syscall.BytePtrFromString(path)
+       if err != nil {
+               return err
+       }
+       _, _, errno := syscall6(uintptr(unsafe.Pointer(&procFchmodat)), 4,
+               uintptr(dirfd),
+               uintptr(unsafe.Pointer(p)),
+               uintptr(mode),
+               uintptr(flags),
+               0, 0)
+       if errno != 0 {
+               return errno
+       }
+       return nil
+}
index 69463e00b94be9dc387ada416469284611d61948..26ca70322b7f150db44c363a890197181eefb2ae 100644 (file)
@@ -49,3 +49,25 @@ func Mkdirat(dirfd int, path string, mode uint32) error {
        }
        return nil
 }
+
+//go:cgo_import_dynamic libc_fchmodat fchmodat "libc.so"
+
+func libc_fchmodat_trampoline()
+
+func Fchmodat(dirfd int, path string, mode uint32, 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(mode),
+               uintptr(flags),
+               0,
+               0)
+       if errno != 0 {
+               return errno
+       }
+       return nil
+}
index d0ba12a78af0555e6fef14d6cc117ce2e3dd1117..84c60c47b8ac5c81cf76b4a0ca4b65e3d42b5bf6 100644 (file)
@@ -12,6 +12,7 @@ const (
        fstatatTrap    uintptr = syscall.SYS_FSTATAT
        readlinkatTrap uintptr = syscall.SYS_READLINKAT
        mkdiratTrap    uintptr = syscall.SYS_MKDIRAT
+       fchmodatTrap   uintptr = syscall.SYS_FCHMODAT
 
        AT_EACCESS          = 0x4
        AT_FDCWD            = 0xfffafdcd
index 0f3472243226530f9522c37b7218f752ba22e387..22ff4e7e895c5834a2c8436519ced9f6dae36fba 100644 (file)
@@ -19,4 +19,5 @@ const (
        posixFallocateTrap uintptr = syscall.SYS_POSIX_FALLOCATE
        readlinkatTrap     uintptr = syscall.SYS_READLINKAT
        mkdiratTrap        uintptr = syscall.SYS_MKDIRAT
+       fchmodatTrap       uintptr = syscall.SYS_FCHMODAT
 )
index 2885c7c68106e9563926d57dbd05e582d4d68fac..8fba319cab7dbdbe1366ac0e4f65d5f2b7c2e4ea 100644 (file)
@@ -11,6 +11,7 @@ const (
        openatTrap     uintptr = syscall.SYS_OPENAT
        readlinkatTrap uintptr = syscall.SYS_READLINKAT
        mkdiratTrap    uintptr = syscall.SYS_MKDIRAT
+       fchmodatTrap   uintptr = syscall.SYS_FCHMODAT
 )
 
 const (
index 820b977436377fcf13a2a8ea1e8fd5c87ac3b06d..f2b7a4f9ebfe8a387e523d6a221ce5f99664c4e4 100644 (file)
@@ -12,6 +12,7 @@ const (
        fstatatTrap    uintptr = syscall.SYS_FSTATAT
        readlinkatTrap uintptr = syscall.SYS_READLINKAT
        mkdiratTrap    uintptr = syscall.SYS_MKDIRAT
+       fchmodatTrap   uintptr = syscall.SYS_FCHMODAT
 )
 
 const (
index cd0cb4b7e473501f80f9062afb48ddc0b4773b20..7289317110f8552698f5ac07c82651c0dccbccc7 100644 (file)
@@ -101,6 +101,11 @@ func Mkdirat(dirfd int, path string, mode uint32) error {
        ))
 }
 
+func Fchmodat(dirfd int, path string, mode uint32, flags int) error {
+       // WASI preview 1 doesn't support changing file modes.
+       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
index 18429773c060e78e1ee3bdc3dff0c4202cea6c44..19bcc0dbac10ce73dd6b556225ad56dead235a7f 100644 (file)
@@ -20,9 +20,10 @@ const (
        O_DIRECTORY    = 0x100000   // target must be a directory
        O_NOFOLLOW_ANY = 0x20000000 // disallow symlinks anywhere in the path
        O_OPEN_REPARSE = 0x40000000 // FILE_OPEN_REPARSE_POINT, used by Lstat
+       O_WRITE_ATTRS  = 0x80000000 // FILE_WRITE_ATTRIBUTES, used by Chmod
 )
 
-func Openat(dirfd syscall.Handle, name string, flag int, perm uint32) (_ syscall.Handle, e1 error) {
+func Openat(dirfd syscall.Handle, name string, flag uint64, perm uint32) (_ syscall.Handle, e1 error) {
        if len(name) == 0 {
                return syscall.InvalidHandle, syscall.ERROR_FILE_NOT_FOUND
        }
@@ -61,6 +62,9 @@ func Openat(dirfd syscall.Handle, name string, flag int, perm uint32) (_ syscall
        if flag&syscall.O_SYNC != 0 {
                options |= FILE_WRITE_THROUGH
        }
+       if flag&O_WRITE_ATTRS != 0 {
+               access |= FILE_WRITE_ATTRIBUTES
+       }
        // Allow File.Stat.
        access |= STANDARD_RIGHTS_READ | FILE_READ_ATTRIBUTES | FILE_READ_EA
 
@@ -129,7 +133,7 @@ func Openat(dirfd syscall.Handle, name string, flag int, perm uint32) (_ syscall
 }
 
 // ntCreateFileError maps error returns from NTCreateFile to user-visible errors.
-func ntCreateFileError(err error, flag int) error {
+func ntCreateFileError(err error, flag uint64) error {
        s, ok := err.(NTStatus)
        if !ok {
                // Shouldn't really be possible, NtCreateFile always returns NTStatus.
index 7da9ecf07a2f7399b8c9f820051eae1d9484baff..daeb4fcde37d70487ed36901f94682829f72a439 100644 (file)
@@ -46,7 +46,7 @@ func TestOpen(t *testing.T) {
                        continue
                }
                base := filepath.Base(tt.path)
-               h, err := windows.Openat(dirfd, base, tt.flag, 0o660)
+               h, err := windows.Openat(dirfd, base, uint64(tt.flag), 0o660)
                syscall.CloseHandle(dirfd)
                if err == nil {
                        syscall.CloseHandle(h)
index f91c0f75f30e2aab0b2c31c0a0f206e07d922cd8..cd26144ab7410f2556e07601f8f96a1cd21d5a9a 100644 (file)
@@ -54,11 +54,16 @@ func OpenInRoot(dir, name string) (*File, error) {
 //
 //   - When GOOS=windows, file names may not reference Windows reserved device names
 //     such as NUL and COM1.
+//   - On Unix, [Root.Chmod] and [Root.Chown] are vulnerable to a race condition.
+//     If the target of the operation is changed from a regular file to a symlink
+//     while the operation is in progress, the operation may be peformed on the link
+//     rather than the link target.
 //   - When GOOS=js, Root is vulnerable to TOCTOU (time-of-check-time-of-use)
 //     attacks in symlink validation, and cannot ensure that operations will not
 //     escape the root.
 //   - When GOOS=plan9 or GOOS=js, Root does not track directories across renames.
 //     On these platforms, a Root references a directory name, not a file descriptor.
+//   - WASI preview 1 (GOOS=wasip1) does not support [Root.Chmod].
 type Root struct {
        root *root
 }
@@ -127,6 +132,12 @@ func (r *Root) OpenRoot(name string) (*Root, error) {
        return openRootInRoot(r, name)
 }
 
+// Chmod changes the mode of the named file in the root to mode.
+// See [Chmod] for more details.
+func (r *Root) Chmod(name string, mode FileMode) error {
+       return rootChmod(r, name, mode)
+}
+
 // Mkdir creates a new directory in the root
 // with the specified name and permission bits (before umask).
 // See [Mkdir] for more details.
index 8be55a029fa2b64afd45ad6e84d2bd30400c5e21..819486f289f397440c26f7528841b5ffc97d99fe 100644 (file)
@@ -95,6 +95,16 @@ func rootStat(r *Root, name string, lstat bool) (FileInfo, error) {
        return fi, nil
 }
 
+func rootChmod(r *Root, name string, mode FileMode) error {
+       if err := checkPathEscapes(r, name); err != nil {
+               return &PathError{Op: "chmodat", Path: name, Err: err}
+       }
+       if err := Chmod(joinPath(r.root.name, name), mode); err != nil {
+               return &PathError{Op: "chmodat", 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}
index a03208b4c170e9b511e6b24f4593bfef0ccab6b2..97e389db8d2352fda116dd1fb7bc9855b5b8c3b1 100644 (file)
@@ -64,6 +64,16 @@ func (r *root) Name() string {
        return r.name
 }
 
+func rootChmod(r *Root, name string, mode FileMode) error {
+       _, err := doInRoot(r, name, func(parent sysfdType, name string) (struct{}, error) {
+               return struct{}{}, chmodat(parent, name, mode)
+       })
+       if err != nil {
+               return &PathError{Op: "chmodat", 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)
index cbb985b2ceeb484484c04ea3d7f47baac553bc51..3591214ffd7043a2922f2f45af8064660f57797c 100644 (file)
@@ -389,6 +389,43 @@ func TestRootCreate(t *testing.T) {
        }
 }
 
+func TestRootChmod(t *testing.T) {
+       if runtime.GOOS == "wasip1" {
+               t.Skip("Chmod not supported on " + runtime.GOOS)
+       }
+       for _, test := range rootTestCases {
+               test.run(t, func(t *testing.T, target string, root *os.Root) {
+                       if target != "" {
+                               // Create a file with no read/write permissions,
+                               // to ensure we can use Chmod on an inaccessible file.
+                               if err := os.WriteFile(target, nil, 0o000); err != nil {
+                                       t.Fatal(err)
+                               }
+                       }
+                       if runtime.GOOS == "windows" {
+                               // On Windows, Chmod("symlink") affects the link, not its target.
+                               // See issue 71492.
+                               fi, err := root.Lstat(test.open)
+                               if err == nil && !fi.Mode().IsRegular() {
+                                       t.Skip("https://go.dev/issue/71492")
+                               }
+                       }
+                       want := os.FileMode(0o666)
+                       err := root.Chmod(test.open, want)
+                       if errEndsTest(t, err, test.wantError, "root.Chmod(%q)", test.open) {
+                               return
+                       }
+                       st, err := os.Stat(target)
+                       if err != nil {
+                               t.Fatalf("os.Stat(%q) = %v", target, err)
+                       }
+                       if got := st.Mode(); got != want {
+                               t.Errorf("after root.Chmod(%q, %v): file mode = %v, want %v", test.open, want, got, want)
+                       }
+               })
+       }
+}
+
 func TestRootMkdir(t *testing.T) {
        for _, test := range rootTestCases {
                test.run(t, func(t *testing.T, target string, root *os.Root) {
@@ -877,6 +914,35 @@ func TestRootConsistencyCreate(t *testing.T) {
        }
 }
 
+func TestRootConsistencyChmod(t *testing.T) {
+       if runtime.GOOS == "wasip1" {
+               t.Skip("Chmod not supported on " + runtime.GOOS)
+       }
+       for _, test := range rootConsistencyTestCases {
+               test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
+                       chmod := os.Chmod
+                       lstat := os.Lstat
+                       if r != nil {
+                               chmod = r.Chmod
+                               lstat = r.Lstat
+                       }
+
+                       var m1, m2 os.FileMode
+                       err := chmod(path, 0o555)
+                       fi, err := lstat(path)
+                       if err == nil {
+                               m1 = fi.Mode()
+                       }
+                       err = chmod(path, 0o777)
+                       fi, err = lstat(path)
+                       if err == nil {
+                               m2 = fi.Mode()
+                       }
+                       return fmt.Sprintf("%v %v", m1, m2), err
+               })
+       }
+}
+
 func TestRootConsistencyMkdir(t *testing.T) {
        for _, test := range rootConsistencyTestCases {
                test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
index 02d3b4bdad007e0de69ebb2ffbd206bf2147cea0..31773ef6817b921d6a23b9733b6a872e255a1e69 100644 (file)
@@ -131,6 +131,32 @@ func rootStat(r *Root, name string, lstat bool) (FileInfo, error) {
        return fi, nil
 }
 
+// On systems which use fchmodat, fchownat, etc., we have a race condition:
+// When "name" is a symlink, Root.Chmod("name") should act on the target of that link.
+// However, fchmodat doesn't allow us to chmod a file only if it is not a symlink;
+// the AT_SYMLINK_NOFOLLOW parameter causes the operation to act on the symlink itself.
+//
+// We do the best we can by first checking to see if the target of the operation is a symlink,
+// and only attempting the fchmodat if it is not. If the target is replaced between the check
+// and the fchmodat, we will chmod the symlink rather than following it.
+//
+// This race condition is unfortunate, but does not permit escaping a root:
+// We may act on the wrong file, but that file will be contained within the root.
+func afterResolvingSymlink(parent int, name string, f func() error) error {
+       if err := checkSymlink(parent, name, nil); err != nil {
+               return err
+       }
+       return f()
+}
+
+func chmodat(parent int, name string, mode FileMode) error {
+       return afterResolvingSymlink(parent, name, func() error {
+               return ignoringEINTR(func() error {
+                       return unix.Fchmodat(parent, name, syscallMode(mode), unix.AT_SYMLINK_NOFOLLOW)
+               })
+       })
+}
+
 func mkdirat(fd int, name string, perm FileMode) error {
        return ignoringEINTR(func() error {
                return unix.Mkdirat(fd, name, syscallMode(perm))
index 32dfa070b740fd2577b295dbf3457335c7f9ad0e..ba809bd6e0ba944269f391356710d8e252896a62 100644 (file)
@@ -134,7 +134,7 @@ func rootOpenFileNolog(root *Root, name string, flag int, perm FileMode) (*File,
 }
 
 func openat(dirfd syscall.Handle, name string, flag int, perm FileMode) (syscall.Handle, error) {
-       h, err := windows.Openat(dirfd, name, flag|syscall.O_CLOEXEC|windows.O_NOFOLLOW_ANY, syscallMode(perm))
+       h, err := windows.Openat(dirfd, name, uint64(flag)|syscall.O_CLOEXEC|windows.O_NOFOLLOW_ANY, syscallMode(perm))
        if err == syscall.ELOOP || err == syscall.ENOTDIR {
                if link, err := readReparseLinkAt(dirfd, name); err == nil {
                        return syscall.InvalidHandle, errSymlink(link)
@@ -232,6 +232,50 @@ func rootStat(r *Root, name string, lstat bool) (FileInfo, error) {
        return fi, nil
 }
 
+func chmodat(parent syscall.Handle, name string, mode FileMode) error {
+       // Currently, on Windows os.Chmod("symlink") will act on "symlink",
+       // not on any file it points to.
+       //
+       // This may or may not be the desired behavior: https://go.dev/issue/71492
+       //
+       // For now, be consistent with os.Symlink.
+       // Passing O_OPEN_REPARSE causes us to open the named file itself,
+       // not any file that it links to.
+       //
+       // If we want to change this in the future, pass O_NOFOLLOW_ANY instead
+       // and return errSymlink when encountering a symlink:
+       //
+       //     if err == syscall.ELOOP || err == syscall.ENOTDIR {
+       //         if link, err := readReparseLinkAt(parent, name); err == nil {
+       //                 return errSymlink(link)
+       //         }
+       //     }
+       h, err := windows.Openat(parent, name, syscall.O_CLOEXEC|windows.O_OPEN_REPARSE|windows.O_WRITE_ATTRS, 0)
+       if err != nil {
+               return err
+       }
+       defer syscall.CloseHandle(h)
+
+       var d syscall.ByHandleFileInformation
+       if err := syscall.GetFileInformationByHandle(h, &d); err != nil {
+               return err
+       }
+       attrs := d.FileAttributes
+
+       if mode&syscall.S_IWRITE != 0 {
+               attrs &^= syscall.FILE_ATTRIBUTE_READONLY
+       } else {
+               attrs |= syscall.FILE_ATTRIBUTE_READONLY
+       }
+       if attrs == d.FileAttributes {
+               return nil
+       }
+
+       var fbi windows.FILE_BASIC_INFO
+       fbi.FileAttributes = attrs
+       return windows.SetFileInformationByHandle(h, windows.FileBasicInfo, unsafe.Pointer(&fbi), uint32(unsafe.Sizeof(fbi)))
+}
+
 func mkdirat(dirfd syscall.Handle, name string, perm FileMode) error {
        return windows.Mkdirat(dirfd, name, syscallMode(perm))
 }
index fa340531782bd5947d66739121838bbe18dbaa6e..b61889cc438640f99ba0e9abdb194bd0e57db458 100644 (file)
@@ -49,6 +49,7 @@ const (
        o_DIRECTORY    = 0x100000   // used by internal/syscall/windows
        o_NOFOLLOW_ANY = 0x20000000 // used by internal/syscall/windows
        o_OPEN_REPARSE = 0x40000000 // used by internal/syscall/windows
+       o_WRITE_ATTRS  = 0x80000000 // used by internal/syscall/windows
 )
 
 const (