A hard link swapped in for ~/.claude would have had root chown another account's file; fstat on the descriptor now refuses a second link, a fifo or an unexpected kind before anything is changed (hq ADR 0266, the re-review). The judge also finds polkit rules for every account, a runtime's API on TCP, setgid-to-root programs whoever owns them, setuid programs on every suid filesystem, and unprotected links; the rest is listed as not judged.
242 lines
7.8 KiB
Go
242 lines
7.8 KiB
Go
//go:build linux
|
|
|
|
package apply
|
|
|
|
// The descriptor half of home_links.go: below a home, every component is opened from the one above it with
|
|
// O_NOFOLLOW, so a link is refused by the kernel at the moment of use, not only at the check before it.
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
// openDirUnder opens the directory rel names below home, following no link below the home.
|
|
func openDirUnder(home, dir string) (int, error) {
|
|
rel, err := filepath.Rel(home, filepath.Clean(dir))
|
|
if err != nil || strings.HasPrefix(rel, "..") {
|
|
return -1, fmt.Errorf("%s is not below %s", dir, home)
|
|
}
|
|
fd, err := unix.Open(home, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
|
|
if err != nil {
|
|
return -1, &os.PathError{Op: "open", Path: home, Err: err}
|
|
}
|
|
if rel == "." {
|
|
return fd, nil
|
|
}
|
|
at := home
|
|
for _, part := range strings.Split(rel, string(os.PathSeparator)) {
|
|
at = filepath.Join(at, part)
|
|
next, err := unix.Openat(fd, part, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
|
|
unix.Close(fd)
|
|
if err != nil {
|
|
return -1, linkOr(at, home, dir, "open", err)
|
|
}
|
|
fd = next
|
|
}
|
|
return fd, nil
|
|
}
|
|
|
|
// linkOr says a refused link in the mesh's words, and any other failure as the system's.
|
|
func linkOr(at, home, path, op string, err error) error {
|
|
if errors.Is(err, unix.ELOOP) || errors.Is(err, unix.ENOTDIR) {
|
|
if info, lerr := os.Lstat(at); lerr == nil && info.Mode()&os.ModeSymlink != 0 {
|
|
return &LinkUnderHomeError{Path: path, Link: at, Home: home}
|
|
}
|
|
}
|
|
return &os.PathError{Op: op, Path: at, Err: err}
|
|
}
|
|
|
|
// What a caller expects to find at a path below a home: either, a directory, or a regular file.
|
|
const (
|
|
kindAny = iota
|
|
kindDir
|
|
kindFile
|
|
)
|
|
|
|
// openUnder opens the file or directory at path below home, following no link, for its metadata.
|
|
func openUnder(home, path string) (int, error) { return openUnderAs(home, path, kindAny) }
|
|
|
|
// openUnderAs opens path below home as what the caller expects, and refuses what root must not act on
|
|
// (novox/hq ADR 0266): a directory is opened with O_DIRECTORY|O_NOFOLLOW; whatever was opened is judged by
|
|
// fstat on the descriptor itself, before any fchown, fchmod or read — so a name swapped after a check is
|
|
// judged as what it now is. Refused: anything but a directory or a regular file (a device, a fifo, a
|
|
// socket), a kind other than the one expected, and a regular file with more than one link. The account that
|
|
// owns the home can hard-link a file it does not own (root's, where fs.protected_hardlinks is off) into its
|
|
// home; owned or chmodded by name, root would hand that file over.
|
|
func openUnderAs(home, path string, want int) (int, error) {
|
|
dir, err := openDirUnder(home, filepath.Dir(path))
|
|
if err != nil {
|
|
return -1, err
|
|
}
|
|
defer unix.Close(dir)
|
|
base := filepath.Base(path)
|
|
flags := unix.O_RDONLY | unix.O_NOFOLLOW | unix.O_NONBLOCK | unix.O_CLOEXEC
|
|
fd := -1
|
|
if want != kindFile {
|
|
fd, err = unix.Openat(dir, base, flags|unix.O_DIRECTORY, 0)
|
|
if errors.Is(err, unix.ENOTDIR) && want == kindAny {
|
|
fd, err = unix.Openat(dir, base, flags, 0)
|
|
}
|
|
} else {
|
|
fd, err = unix.Openat(dir, base, flags, 0)
|
|
}
|
|
if err != nil {
|
|
err = linkOr(path, home, path, "open", err)
|
|
var link *LinkUnderHomeError
|
|
if want == kindDir && !errors.As(err, &link) && errors.Is(err, unix.ENOTDIR) {
|
|
return -1, fmt.Errorf("%s is not a directory where a directory was expected: below a home it is left alone", path)
|
|
}
|
|
return -1, err
|
|
}
|
|
if err := judgeOpened(fd, path, want); err != nil {
|
|
unix.Close(fd)
|
|
return -1, err
|
|
}
|
|
return fd, nil
|
|
}
|
|
|
|
// judgeOpened is openUnderAs's verdict on what the descriptor holds.
|
|
func judgeOpened(fd int, path string, want int) error {
|
|
var st unix.Stat_t
|
|
if err := unix.Fstat(fd, &st); err != nil {
|
|
return &os.PathError{Op: "fstat", Path: path, Err: err}
|
|
}
|
|
switch st.Mode & unix.S_IFMT {
|
|
case unix.S_IFDIR:
|
|
if want == kindFile {
|
|
return fmt.Errorf("%s is a directory where a file was expected: below a home it is left alone", path)
|
|
}
|
|
case unix.S_IFREG:
|
|
if want == kindDir {
|
|
return fmt.Errorf("%s is a file where a directory was expected: below a home it is left alone", path)
|
|
}
|
|
if st.Nlink > 1 {
|
|
return &HardLinkUnderHomeError{Path: path, Links: uint64(st.Nlink)}
|
|
}
|
|
default:
|
|
return fmt.Errorf("%s is neither a file nor a directory: below a home it is left alone", path)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func chmodUnder(home, path string, mode os.FileMode) error {
|
|
return chmodUnderAs(home, path, mode, kindAny)
|
|
}
|
|
|
|
func chmodUnderAs(home, path string, mode os.FileMode, want int) error {
|
|
fd, err := openUnderAs(home, path, want)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer unix.Close(fd)
|
|
if err := unix.Fchmod(fd, uint32(mode.Perm())); err != nil {
|
|
return &os.PathError{Op: "chmod", Path: path, Err: err}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func chownUnder(home, path string, uid, gid int) error {
|
|
fd, err := openUnder(home, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer unix.Close(fd)
|
|
if err := unix.Fchown(fd, uid, gid); err != nil {
|
|
return &os.PathError{Op: "chown", Path: path, Err: err}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func readUnder(home, path string) ([]byte, error) {
|
|
fd, err := openUnderAs(home, path, kindFile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f := os.NewFile(uintptr(fd), path)
|
|
defer f.Close()
|
|
var st unix.Stat_t
|
|
if err := unix.Fstat(fd, &st); err == nil && st.Mode&unix.S_IFMT != unix.S_IFREG {
|
|
return nil, fmt.Errorf("%s is not a regular file", path)
|
|
}
|
|
return io.ReadAll(f)
|
|
}
|
|
|
|
// mkdirAllUnder makes the directories missing below home down to dir, each made in its parent's descriptor
|
|
// and opened without following a link; answers those it made, deepest first, as makeDirsSaying does.
|
|
func mkdirAllUnder(home, dir string, mode os.FileMode) ([]string, error) {
|
|
rel, err := filepath.Rel(home, filepath.Clean(dir))
|
|
if err != nil || strings.HasPrefix(rel, "..") {
|
|
return nil, fmt.Errorf("%s is not below %s", dir, home)
|
|
}
|
|
fd, err := unix.Open(home, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
|
|
if err != nil {
|
|
return nil, &os.PathError{Op: "open", Path: home, Err: err}
|
|
}
|
|
defer func() { unix.Close(fd) }()
|
|
var made []string
|
|
if rel == "." {
|
|
return nil, nil
|
|
}
|
|
at := home
|
|
for _, part := range strings.Split(rel, string(os.PathSeparator)) {
|
|
at = filepath.Join(at, part)
|
|
if err := unix.Mkdirat(fd, part, uint32(mode.Perm())); err == nil {
|
|
made = append([]string{at}, made...)
|
|
} else if !errors.Is(err, unix.EEXIST) {
|
|
return made, &os.PathError{Op: "mkdir", Path: at, Err: err}
|
|
}
|
|
next, err := unix.Openat(fd, part, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
|
|
if err != nil {
|
|
return made, linkOr(at, home, dir, "open", err)
|
|
}
|
|
unix.Close(fd)
|
|
fd = next
|
|
}
|
|
return made, nil
|
|
}
|
|
|
|
// writeUnder writes a file below home atomically, through its directory's descriptor: made O_EXCL|O_NOFOLLOW
|
|
// under a name of its own, given its mode, and renamed over the file within that directory.
|
|
func writeUnder(home, path string, content []byte, mode os.FileMode) error {
|
|
dir, err := openDirUnder(home, filepath.Dir(path))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer unix.Close(dir)
|
|
name := ".mesh-host-" + strconv.FormatInt(time.Now().UnixNano(), 36) + "-" + strconv.Itoa(os.Getpid())
|
|
fd, err := unix.Openat(dir, name, unix.O_WRONLY|unix.O_CREAT|unix.O_EXCL|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0o600)
|
|
if err != nil {
|
|
return &os.PathError{Op: "create", Path: filepath.Join(filepath.Dir(path), name), Err: err}
|
|
}
|
|
f := os.NewFile(uintptr(fd), name)
|
|
_, werr := f.Write(content)
|
|
if werr == nil {
|
|
werr = f.Sync()
|
|
}
|
|
if werr == nil {
|
|
if err := unix.Fchmod(fd, uint32(mode.Perm())); err != nil {
|
|
werr = &os.PathError{Op: "chmod", Path: path, Err: err}
|
|
}
|
|
}
|
|
if cerr := f.Close(); werr == nil {
|
|
werr = cerr
|
|
}
|
|
if werr == nil {
|
|
if err := unix.Renameat(dir, name, dir, filepath.Base(path)); err != nil {
|
|
werr = &os.PathError{Op: "rename", Path: path, Err: err}
|
|
}
|
|
}
|
|
if werr != nil {
|
|
_ = unix.Unlinkat(dir, name, 0)
|
|
}
|
|
return werr
|
|
}
|