//go:build linux package instancefs import ( "errors" "fmt" "os" "path/filepath" "golang.org/x/sys/unix" ) // These are the directory operations the layout pass performs on the raw // directories behind the instance bind mounts (isolated-exec design §3.2). // A flat tenant shares the agent's uid during the transition and can plant // a symlink anywhere the agent can write, so every one of them works from a // directory fd that openDir opened without following any symlink, and names // exactly one entry beneath it. The *at calls do not follow a symlink at // that last component either: mkdirat answers EEXIST, renameat and unlinkat // act on the link itself, and fstatat is told not to. // EnsureDirBeneath makes sure name, directly under dir, is a real // directory and reports what is there. It creates it with mkdirat when it // is absent, so its group and setgid come from dir the way the kernel // hands them out, and leaves an existing directory exactly as it is. It // never changes a mode or an owner: a chmod by a caller outside the // directory's group silently clears setgid, and a chown into a group the // caller is not in fails, so the caller checks the returned stat and // refuses what it cannot vouch for. Anything else at name, a symlink or a // file, is refused and left untouched. func EnsureDirBeneath(dir, name string, perm os.FileMode) (DirStat, error) { if err := bareName(name); err != nil { return DirStat{}, err } dfd, err := openDir(dir) if err != nil { return DirStat{}, err } defer unix.Close(dfd) path := filepath.Join(dir, name) if err := unix.Mkdirat(dfd, name, uint32(perm.Perm())); err != nil && !errors.Is(err, unix.EEXIST) { return DirStat{}, &os.PathError{Op: "mkdirat", Path: path, Err: err} } var st unix.Stat_t if err := unix.Fstatat(dfd, name, &st, unix.AT_SYMLINK_NOFOLLOW); err != nil { return DirStat{}, &os.PathError{Op: "fstatat", Path: path, Err: err} } switch st.Mode & unix.S_IFMT { case unix.S_IFDIR: return dirStatOf(&st), nil case unix.S_IFLNK: return DirStat{}, fmt.Errorf("%w: %s", ErrSymlink, path) } return DirStat{}, fmt.Errorf("instancefs: %s is not a directory", path) } // StatDir reports dir itself, opened without following a symlink at any // component. func StatDir(dir string) (DirStat, error) { fd, err := openDir(dir) if err != nil { return DirStat{}, err } defer unix.Close(fd) var st unix.Stat_t if err := unix.Fstat(fd, &st); err != nil { return DirStat{}, &os.PathError{Op: "fstat", Path: dir, Err: err} } return dirStatOf(&st), nil } func dirStatOf(st *unix.Stat_t) DirStat { mode := os.ModeDir | os.FileMode(st.Mode&0o777) if st.Mode&unix.S_ISUID != 0 { mode |= os.ModeSetuid } if st.Mode&unix.S_ISGID != 0 { mode |= os.ModeSetgid } if st.Mode&unix.S_ISVTX != 0 { mode |= os.ModeSticky } return DirStat{Mode: mode, UID: int(st.Uid), GID: int(st.Gid)} } // RenameBetween renames srcName in srcDir to dstName in dstDir. Both // directories are opened without following a symlink at any component, so // neither side of the rename can be redirected; the entry itself moves as // whatever it is, a link included. An existing entry at the destination is // replaced, as rename(2) always does: callers that must not replace // something check for it first. func RenameBetween(srcDir, srcName, dstDir, dstName string) error { if err := bareName(srcName); err != nil { return err } if err := bareName(dstName); err != nil { return err } sfd, err := openDir(srcDir) if err != nil { return err } defer unix.Close(sfd) dfd, err := openDir(dstDir) if err != nil { return err } defer unix.Close(dfd) if err := unix.Renameat(sfd, srcName, dfd, dstName); err != nil { return &os.LinkError{Op: "renameat", Old: filepath.Join(srcDir, srcName), New: filepath.Join(dstDir, dstName), Err: err} } return nil } // RemoveBeneath unlinks name, a non-directory entry directly under dir. A // symlink there is removed, never its target. func RemoveBeneath(dir, name string) error { if err := bareName(name); err != nil { return err } dfd, err := openDir(dir) if err != nil { return err } defer unix.Close(dfd) if err := unix.Unlinkat(dfd, name, 0); err != nil { return &os.PathError{Op: "unlinkat", Path: filepath.Join(dir, name), Err: err} } return nil } // RenameBetweenNoReplace is RenameBetween that refuses to replace anything // at the destination (RENAME_NOREPLACE): EEXIST comes back instead. It is // for a caller that must not lose what is already at a name, however the // name came to be taken. func RenameBetweenNoReplace(srcDir, srcName, dstDir, dstName string) error { if err := bareName(srcName); err != nil { return err } if err := bareName(dstName); err != nil { return err } sfd, err := openDir(srcDir) if err != nil { return err } defer unix.Close(sfd) dfd, err := openDir(dstDir) if err != nil { return err } defer unix.Close(dfd) if err := unix.Renameat2(sfd, srcName, dfd, dstName, unix.RENAME_NOREPLACE); err != nil { return &os.LinkError{Op: "renameat2", Old: filepath.Join(srcDir, srcName), New: filepath.Join(dstDir, dstName), Err: err} } return nil } // RemoveAllBeneath removes name, directly under dir, and everything below // it. No symlink is followed at any depth: dir itself is opened without // following one (ErrSymlink), every entry is examined with fstatat // AT_SYMLINK_NOFOLLOW, and a symlink is unlinked, never its target. A // tenant sharing the agent's uid can plant links and directories in a run // directory (run spec §5.2); this is the only way the agent deletes one. // A missing name is not an error. func RemoveAllBeneath(dir, name string) error { if err := bareName(name); err != nil { return err } dfd, err := openDir(dir) if err != nil { return err } defer unix.Close(dfd) return removeAt(dfd, name, filepath.Join(dir, name), 0) } // removeAt removes name under the directory fd parent; path is for errors. func removeAt(parent int, name, path string, depth int) error { if depth > 64 { return fmt.Errorf("instancefs: %s is nested more than 64 deep", path) } var st unix.Stat_t if err := unix.Fstatat(parent, name, &st, unix.AT_SYMLINK_NOFOLLOW); err != nil { if errors.Is(err, unix.ENOENT) { return nil } return &os.PathError{Op: "fstatat", Path: path, Err: err} } if st.Mode&unix.S_IFMT != unix.S_IFDIR { if err := unix.Unlinkat(parent, name, 0); err != nil && !errors.Is(err, unix.ENOENT) { return &os.PathError{Op: "unlinkat", Path: path, Err: err} } return nil } fd, err := unix.Openat(parent, name, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) if err != nil { return &os.PathError{Op: "openat", Path: path, Err: err} } f := os.NewFile(uintptr(fd), path) names, err := f.Readdirnames(-1) if err != nil { f.Close() return err } for _, n := range names { if err := removeAt(fd, n, filepath.Join(path, n), depth+1); err != nil { f.Close() return err } } f.Close() if err := unix.Unlinkat(parent, name, unix.AT_REMOVEDIR); err != nil && !errors.Is(err, unix.ENOENT) { return &os.PathError{Op: "unlinkat", Path: path, Err: err} } return nil }