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