aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/cmd/dist/buildtool.go1
-rw-r--r--src/internal/syscall/unix/at.go17
-rw-r--r--src/internal/syscall/unix/at_sysnum_linux.go3
-rw-r--r--src/internal/syscall/unix/fchmodat_linux.go51
-rw-r--r--src/internal/syscall/unix/fchmodat_other.go29
-rw-r--r--src/internal/syscall/unix/fchmodat_test.go62
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)
+ }
+}