go/src/os/root_unix.go
Damien Neil 371e83cd7b os: add Root.Chmod
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>
2025-02-10 15:33:35 -08:00

225 lines
6.1 KiB
Go

// Copyright 2024 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 aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || wasip1
package os
import (
"errors"
"internal/syscall/unix"
"runtime"
"syscall"
)
type sysfdType = int
// openRootNolog is OpenRoot.
func openRootNolog(name string) (*Root, error) {
var fd int
err := ignoringEINTR(func() error {
var err error
fd, _, err = open(name, syscall.O_CLOEXEC, 0)
return err
})
if err != nil {
return nil, &PathError{Op: "open", Path: name, Err: err}
}
return newRoot(fd, name)
}
// newRoot returns a new Root.
// If fd is not a directory, it closes it and returns an error.
func newRoot(fd int, name string) (*Root, error) {
var fs fileStat
err := ignoringEINTR(func() error {
return syscall.Fstat(fd, &fs.sys)
})
fillFileStatFromSys(&fs, name)
if err == nil && !fs.IsDir() {
syscall.Close(fd)
return nil, &PathError{Op: "open", Path: name, Err: errors.New("not a directory")}
}
// There's a race here with fork/exec, which we are
// content to live with. See ../syscall/exec_unix.go.
if !supportsCloseOnExec {
syscall.CloseOnExec(fd)
}
r := &Root{&root{
fd: fd,
name: name,
}}
runtime.SetFinalizer(r.root, (*root).Close)
return r, nil
}
// openRootInRoot is Root.OpenRoot.
func openRootInRoot(r *Root, name string) (*Root, error) {
fd, err := doInRoot(r, name, func(parent int, name string) (fd int, err error) {
ignoringEINTR(func() error {
fd, err = unix.Openat(parent, name, syscall.O_NOFOLLOW|syscall.O_CLOEXEC, 0)
if isNoFollowErr(err) {
err = checkSymlink(parent, name, err)
}
return err
})
return fd, err
})
if err != nil {
return nil, &PathError{Op: "openat", Path: name, Err: err}
}
return newRoot(fd, name)
}
// rootOpenFileNolog is Root.OpenFile.
func rootOpenFileNolog(root *Root, name string, flag int, perm FileMode) (*File, error) {
fd, err := doInRoot(root, name, func(parent int, name string) (fd int, err error) {
ignoringEINTR(func() error {
fd, err = unix.Openat(parent, name, syscall.O_NOFOLLOW|syscall.O_CLOEXEC|flag, uint32(perm))
if isNoFollowErr(err) || err == syscall.ENOTDIR {
err = checkSymlink(parent, name, err)
}
return err
})
return fd, err
})
if err != nil {
return nil, &PathError{Op: "openat", Path: name, Err: err}
}
f := newFile(fd, joinPath(root.Name(), name), kindOpenFile, unix.HasNonblockFlag(flag))
return f, nil
}
func rootOpenDir(parent int, name string) (int, error) {
var (
fd int
err error
)
ignoringEINTR(func() error {
fd, err = unix.Openat(parent, name, syscall.O_NOFOLLOW|syscall.O_CLOEXEC|syscall.O_DIRECTORY, 0)
if isNoFollowErr(err) || err == syscall.ENOTDIR {
err = checkSymlink(parent, name, err)
} else if err == syscall.ENOTSUP || err == syscall.EOPNOTSUPP {
// ENOTSUP and EOPNOTSUPP are often, but not always, the same errno.
// Translate both to ENOTDIR, since this indicates a non-terminal
// path component was not a directory.
err = syscall.ENOTDIR
}
return err
})
return fd, err
}
func rootStat(r *Root, name string, lstat bool) (FileInfo, error) {
fi, err := doInRoot(r, name, func(parent sysfdType, n string) (FileInfo, error) {
var fs fileStat
if err := unix.Fstatat(parent, n, &fs.sys, unix.AT_SYMLINK_NOFOLLOW); err != nil {
return nil, err
}
fillFileStatFromSys(&fs, name)
if !lstat && fs.Mode()&ModeSymlink != 0 {
return nil, checkSymlink(parent, n, syscall.ELOOP)
}
return &fs, nil
})
if err != nil {
return nil, &PathError{Op: "statat", Path: name, Err: err}
}
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))
})
}
func removeat(fd int, name string) error {
// The system call interface forces us to know whether
// we are removing a file or directory. Try both.
e := ignoringEINTR(func() error {
return unix.Unlinkat(fd, name, 0)
})
if e == nil {
return nil
}
e1 := ignoringEINTR(func() error {
return unix.Unlinkat(fd, name, unix.AT_REMOVEDIR)
})
if e1 == nil {
return nil
}
// Both failed. See comment in Remove for how we decide which error to return.
if e1 != syscall.ENOTDIR {
return e1
}
return e
}
// checkSymlink resolves the symlink name in parent,
// and returns errSymlink with the link contents.
//
// If name is not a symlink, return origError.
func checkSymlink(parent int, name string, origError error) error {
link, err := readlinkat(parent, name)
if err != nil {
return origError
}
return errSymlink(link)
}
func readlinkat(fd int, name string) (string, error) {
for len := 128; ; len *= 2 {
b := make([]byte, len)
var (
n int
e error
)
ignoringEINTR(func() error {
n, e = unix.Readlinkat(fd, name, b)
return e
})
if e == syscall.ERANGE {
continue
}
if e != nil {
return "", e
}
if n < 0 {
n = 0
}
if n < len {
return string(b[0:n]), nil
}
}
}