//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 }