Follow no link below a home as root, and judge more ways to root
An account could replace ~/.claude with a link to /etc and have the node-engine chown, chmod or write through it on the next apply. Below a person's or an agent's home every component is now opened without following a link, and a link refuses the resource in words. The root judge also reads doas and polkit rules, the container runtimes' sockets with their ACLs, ACLs on the secrets, and setuid-root programs no package owns, in the C locale; Judged and NotJudged write down exactly what it covers (hq ADR 0266 review).
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
//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}
|
||||
}
|
||||
|
||||
// openUnder opens the file or directory at path below home, following no link, for its metadata.
|
||||
func openUnder(home, path string) (int, error) {
|
||||
dir, err := openDirUnder(home, filepath.Dir(path))
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
defer unix.Close(dir)
|
||||
fd, err := unix.Openat(dir, filepath.Base(path), unix.O_RDONLY|unix.O_NOFOLLOW|unix.O_NONBLOCK|unix.O_CLOEXEC, 0)
|
||||
if err != nil {
|
||||
return -1, linkOr(path, home, path, "open", err)
|
||||
}
|
||||
return fd, nil
|
||||
}
|
||||
|
||||
func chmodUnder(home, path string, mode os.FileMode) error {
|
||||
fd, err := openUnder(home, path)
|
||||
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 := openUnder(home, path)
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user