diff options
| -rw-r--r-- | src/cmd/dist/buildtool.go | 1 | ||||
| -rw-r--r-- | src/internal/syscall/unix/at.go | 17 | ||||
| -rw-r--r-- | src/internal/syscall/unix/at_sysnum_linux.go | 3 | ||||
| -rw-r--r-- | src/internal/syscall/unix/fchmodat_linux.go | 51 | ||||
| -rw-r--r-- | src/internal/syscall/unix/fchmodat_other.go | 29 | ||||
| -rw-r--r-- | src/internal/syscall/unix/fchmodat_test.go | 62 |
6 files changed, 145 insertions, 18 deletions
diff --git a/src/cmd/dist/buildtool.go b/src/cmd/dist/buildtool.go index 62cd937692..2da9a17636 100644 --- a/src/cmd/dist/buildtool.go +++ b/src/cmd/dist/buildtool.go @@ -92,6 +92,7 @@ var bootstrapDirs = []string{ "internal/race", "internal/runtime/gc", "internal/saferio", + "internal/strconv", "internal/syscall/unix", "internal/types/errors", "internal/unsafeheader", diff --git a/src/internal/syscall/unix/at.go b/src/internal/syscall/unix/at.go index 96272afc7b..359a2da995 100644 --- a/src/internal/syscall/unix/at.go +++ b/src/internal/syscall/unix/at.go @@ -80,23 +80,6 @@ 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 -} - func Fchownat(dirfd int, path string, uid, gid int, flags int) error { p, err := syscall.BytePtrFromString(path) if err != nil { diff --git a/src/internal/syscall/unix/at_sysnum_linux.go b/src/internal/syscall/unix/at_sysnum_linux.go index bb7f244fe2..d260a239a9 100644 --- a/src/internal/syscall/unix/at_sysnum_linux.go +++ b/src/internal/syscall/unix/at_sysnum_linux.go @@ -11,7 +11,6 @@ const ( openatTrap uintptr = syscall.SYS_OPENAT readlinkatTrap uintptr = syscall.SYS_READLINKAT mkdiratTrap uintptr = syscall.SYS_MKDIRAT - fchmodatTrap uintptr = syscall.SYS_FCHMODAT fchownatTrap uintptr = syscall.SYS_FCHOWNAT linkatTrap uintptr = syscall.SYS_LINKAT symlinkatTrap uintptr = syscall.SYS_SYMLINKAT @@ -24,4 +23,6 @@ const ( AT_SYMLINK_NOFOLLOW = 0x100 UTIME_OMIT = 0x3ffffffe + + O_PATH = 0x200000 ) diff --git a/src/internal/syscall/unix/fchmodat_linux.go b/src/internal/syscall/unix/fchmodat_linux.go new file mode 100644 index 0000000000..786ec29df2 --- /dev/null +++ b/src/internal/syscall/unix/fchmodat_linux.go @@ -0,0 +1,51 @@ +// Copyright 2026 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 linux + +package unix + +import ( + "internal/strconv" + "syscall" +) + +func Fchmodat(dirfd int, path string, mode uint32, flags int) error { + // On Linux, the fchmodat syscall silently ignores the AT_SYMLINK_NOFOLLOW flag. + // We need to use fchmodat2 instead. + // syscall.Fchmodat handles this. + if err := syscall.Fchmodat(dirfd, path, mode, flags); err != syscall.EOPNOTSUPP { + return err + } + + // This kernel doesn't appear to support fchmodat2 (added in Linux 6.6). + // We can't fall back to Fchmod, because it requires write permissions on the file. + // Instead, use the same workaround as GNU libc and musl, which is to open the file + // and then fchmodat the FD in /proc/self/fd. + // See: https://lwn.net/Articles/939217/ + fd, err := Openat(dirfd, path, O_PATH|syscall.O_NOFOLLOW|syscall.O_CLOEXEC, 0) + if err != nil { + return err + } + defer syscall.Close(fd) + procPath := "/proc/self/fd/" + strconv.Itoa(fd) + + // Check to see if this file is a symlink. + // (We passed O_NOFOLLOW above, but O_PATH|O_NOFOLLOW will open a symlink.) + var st syscall.Stat_t + if err := syscall.Stat(procPath, &st); err != nil { + if err == syscall.ENOENT { + // /proc has probably not been mounted. Give up. + return syscall.EOPNOTSUPP + } + return err + } + if st.Mode&syscall.S_IFMT == syscall.S_IFLNK { + // fchmodat on the proc FD for a symlink apparently gives inconsistent + // results, so just refuse to try. + return syscall.EOPNOTSUPP + } + + return syscall.Fchmodat(AT_FDCWD, procPath, mode, flags&^AT_SYMLINK_NOFOLLOW) +} diff --git a/src/internal/syscall/unix/fchmodat_other.go b/src/internal/syscall/unix/fchmodat_other.go new file mode 100644 index 0000000000..76f478c4ae --- /dev/null +++ b/src/internal/syscall/unix/fchmodat_other.go @@ -0,0 +1,29 @@ +// Copyright 2026 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 dragonfly || freebsd || netbsd || (openbsd && mips64) + +package unix + +import ( + "syscall" + "unsafe" +) + +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 +} diff --git a/src/internal/syscall/unix/fchmodat_test.go b/src/internal/syscall/unix/fchmodat_test.go new file mode 100644 index 0000000000..49a098535d --- /dev/null +++ b/src/internal/syscall/unix/fchmodat_test.go @@ -0,0 +1,62 @@ +// Copyright 2026 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 || wasip1 + +package unix_test + +import ( + "internal/syscall/unix" + "os" + "runtime" + "testing" +) + +// TestFchmodAtSymlinkNofollow verifies that Fchmodat honors the AT_SYMLINK_NOFOLLOW flag. +func TestFchmodatSymlinkNofollow(t *testing.T) { + if runtime.GOOS == "wasip1" { + t.Skip("wasip1 doesn't support chmod") + } + + dir := t.TempDir() + filename := dir + "/file" + linkname := dir + "/symlink" + if err := os.WriteFile(filename, nil, 0o100); err != nil { + t.Fatal(err) + } + if err := os.Symlink(filename, linkname); err != nil { + t.Fatal(err) + } + + parent, err := os.Open(dir) + if err != nil { + t.Fatal(err) + } + defer parent.Close() + + lstatMode := func(path string) os.FileMode { + st, err := os.Lstat(path) + if err != nil { + t.Fatal(err) + } + return st.Mode() + } + + // Fchmodat with no flags follows symlinks. + const mode1 = 0o200 + if err := unix.Fchmodat(int(parent.Fd()), "symlink", mode1, 0); err != nil { + t.Fatal(err) + } + if got, want := lstatMode(filename), os.FileMode(mode1); got != want { + t.Errorf("after Fchmodat(parent, symlink, %v, 0); mode = %v, want %v", mode1, got, want) + } + + // Fchmodat with AT_SYMLINK_NOFOLLOW does not follow symlinks. + // The Fchmodat call may fail or chmod the symlink itself, depending on the kernel version. + const mode2 = 0o400 + unix.Fchmodat(int(parent.Fd()), "symlink", mode2, unix.AT_SYMLINK_NOFOLLOW) + if got, want := lstatMode(filename), os.FileMode(mode1); got != want { + t.Errorf("after Fchmodat(parent, symlink, %v, AT_SYMLINK_NOFOLLOW); mode = %v, want %v", mode1, got, want) + } +} |
