//go:build linux package apply // The descriptor half of mesh_dirs.go (novox/hq issue 358). import ( "errors" "fmt" "os" "path/filepath" "strings" "golang.org/x/sys/unix" ) // maxLinksFollowed bounds the root-only links one walk follows, as the kernel bounds a lookup's (MAXSYMLINKS). const maxLinksFollowed = 40 // errNotThereYet is a read-only walk meeting a component that does not exist: an apply would make it there. var errNotThereYet = errors.New("not there yet") // meshWalk is one walk of a mesh directory's path from /. type meshWalk struct { // want is the directory as declared. want string // create makes a missing component, with mode, in its parent's descriptor; without it the walk stops there // with errNotThereYet and makes nothing. create bool mode os.FileMode // made is called with each directory the walk made and its descriptor. made func(dir string, fd int) error // Made is every directory the walk made, outermost first, and At the path the directory was reached at: // want, or where the root-only links in its path led. Made []string At string } // judgeMeshDir is openMeshDir's judgement and nothing else (novox/hq issue 358): the same walk, making nothing // and changing nothing; nil when an apply would give the directory its mode and owner, also when a part of its // path is not there yet (an apply makes it, as root, in a directory it reached). func judgeMeshDir(path string) error { w := &meshWalk{want: filepath.Clean(path)} fd, err := w.open() if errors.Is(err, errNotThereYet) { return nil } if err != nil { return err } unix.Close(fd) return nil } func (w *meshWalk) open() (int, error) { if !filepath.IsAbs(w.want) { return -1, fmt.Errorf("%s is not an absolute path", w.want) } p := w.want for followed := 0; ; followed++ { if followed > maxLinksFollowed { return -1, &os.PathError{Op: "open", Path: w.want, Err: unix.ELOOP} } fd, next, err := w.fromRoot(p) if err != nil { return -1, err } if next == "" { w.At = p return fd, nil } p = next } } // fromRoot is one walk down p: the directory's descriptor, or the path to walk next when a root-only link was // met (with -1), or the refusal. func (w *meshWalk) fromRoot(p string) (int, string, error) { fd, err := unix.Open("/", unix.O_RDONLY|unix.O_DIRECTORY|unix.O_CLOEXEC, 0) if err != nil { return -1, "", &os.PathError{Op: "open", Path: "/", Err: err} } // Who could replace an entry of the directories walked so far: once one could, every link below it is // theirs to make, whoever owns the link. who, err := replacerOf(fd, "/") if err != nil { unix.Close(fd) return -1, "", err } at := "/" var parts []string if p != "/" { parts = strings.Split(strings.TrimPrefix(p, "/"), "/") } const dirFlags = unix.O_RDONLY | unix.O_DIRECTORY | unix.O_NOFOLLOW | unix.O_CLOEXEC for i, part := range parts { here := filepath.Join(at, part) last := i == len(parts)-1 next, err := unix.Openat(fd, part, dirFlags, 0) if errors.Is(err, unix.ENOENT) { if !w.create { unix.Close(fd) return -1, "", errNotThereYet } mkErr := unix.Mkdirat(fd, part, uint32(w.mode.Perm())) if mkErr != nil && !errors.Is(mkErr, unix.EEXIST) { unix.Close(fd) return -1, "", &os.PathError{Op: "mkdir", Path: here, Err: mkErr} } next, err = unix.Openat(fd, part, dirFlags, 0) if mkErr == nil && err == nil { w.Made = append(w.Made, here) if w.made != nil { if gErr := w.made(here, next); gErr != nil { unix.Close(next) unix.Close(fd) return -1, "", gErr } } } } if errors.Is(err, unix.ELOOP) || errors.Is(err, unix.ENOTDIR) { var st unix.Stat_t if lerr := unix.Fstatat(fd, part, &st, unix.AT_SYMLINK_NOFOLLOW); lerr == nil && st.Mode&unix.S_IFMT == unix.S_IFLNK { to, rerr := w.followable(fd, part, here, st, who, last) unix.Close(fd) if rerr != nil { return -1, "", rerr } if !filepath.IsAbs(to) { to = filepath.Join(at, to) } return -1, filepath.Join("/", to, filepath.Join(parts[i+1:]...)), nil } } unix.Close(fd) if err != nil { switch { case errors.Is(err, unix.ENOTDIR) && last: return -1, "", fmt.Errorf("%s exists and is not a directory", here) case errors.Is(err, unix.ENOTDIR): return -1, "", fmt.Errorf("%s is not a directory, and %s lies below it", here, w.want) } return -1, "", &os.PathError{Op: "open", Path: here, Err: err} } fd = next at = here if who == "" { if who, err = replacerOf(fd, here); err != nil { unix.Close(fd) return -1, "", err } } } return fd, "", nil } // replacerOf is couldReplace for the directory open on fd. func replacerOf(fd int, path string) (string, error) { var st unix.Stat_t if err := unix.Fstat(fd, &st); err != nil { return "", &os.PathError{Op: "fstat", Path: path, Err: err} } return couldReplace(path, st.Uid, st.Gid, st.Mode&0o7777), nil } // followable judges the link at here, held by the directory open on fd: its target when root alone could have // made it, else the refusal. who is who could replace an entry of a directory above it ("" when nobody). func (w *meshWalk) followable(fd int, name, here string, st unix.Stat_t, who string, last bool) (string, error) { if last { return "", &PlacementRefusedError{Path: w.want, Why: "it is a symbolic link, and the node-engine follows no " + "link to a directory it owns"} } if who == "" { who = linkReplacer(here, st.Uid) } if who != "" { return "", &LinkInPathError{Path: w.want, Link: here, Who: who} } buf := make([]byte, unix.PathMax) n, err := unix.Readlinkat(fd, name, buf) if err != nil { return "", &os.PathError{Op: "readlink", Path: here, Err: err} } return string(buf[:n]), nil }