package sys import ( "fmt" "os" "runtime" "strconv" "golang.org/x/sys/unix" "github.com/opencontainers/runc/internal/pathrs" ) // FchmodFile is a wrapper around fchmodat2(AT_EMPTY_PATH) with fallbacks for // older kernels. This is distinct from [File.Chmod] and [unix.Fchmod] in that // it works on O_PATH file descriptors. func FchmodFile(f *os.File, mode uint32) error { err := unix.Fchmodat(int(f.Fd()), "", mode, unix.AT_EMPTY_PATH) // If fchmodat2(2) is not available at all, golang.org/x/unix (probably // in order to mirror glibc) returns EOPNOTSUPP rather than EINVAL // (what the kernel actually returns for invalid flags, which is being // emulated) or ENOSYS (which is what glibc actually sees). if err != unix.EINVAL && err != unix.EOPNOTSUPP { //nolint:errorlint // unix errors are bare // err == nil is implicitly handled return os.NewSyscallError("fchmodat2 AT_EMPTY_PATH", err) } // AT_EMPTY_PATH support was added to fchmodat2 in Linux 6.6 // (5daeb41a6fc9d0d81cb2291884b7410e062d8fa1). The alternative for // older kernels is to go through /proc. fdDir, closer, err2 := pathrs.ProcThreadSelfOpen("fd/", unix.O_DIRECTORY) if err2 != nil { return fmt.Errorf("fchmodat2 AT_EMPTY_PATH fallback: %w", err2) } defer closer() defer fdDir.Close() err = unix.Fchmodat(int(fdDir.Fd()), strconv.Itoa(int(f.Fd())), mode, 0) if err != nil { err = fmt.Errorf("fchmodat /proc/self/fd/%d: %w", f.Fd(), err) } runtime.KeepAlive(f) return err } // FchownFile is a wrapper around fchownat(AT_EMPTY_PATH). This is distinct // from [File.Chown] and [unix.Fchown] in that it works on O_PATH file // descriptors. func FchownFile(f *os.File, uid, gid int) error { err := unix.Fchownat(int(f.Fd()), "", uid, gid, unix.AT_EMPTY_PATH) runtime.KeepAlive(f) return os.NewSyscallError("fchownat AT_EMPTY_PATH", err) }