| 1 | //go:build linux |
| 2 | |
| 3 | package instancefs |
| 4 | |
| 5 | import ( |
| 6 | "errors" |
| 7 | "fmt" |
| 8 | "os" |
| 9 | "path/filepath" |
| 10 | |
| 11 | "golang.org/x/sys/unix" |
| 12 | ) |
| 13 | |
| 14 | // These are the directory operations the layout pass performs on the raw |
| 15 | // directories behind the instance bind mounts (isolated-exec design §3.2). |
| 16 | // A flat tenant shares the agent's uid during the transition and can plant |
| 17 | // a symlink anywhere the agent can write, so every one of them works from a |
| 18 | // directory fd that openDir opened without following any symlink, and names |
| 19 | // exactly one entry beneath it. The *at calls do not follow a symlink at |
| 20 | // that last component either: mkdirat answers EEXIST, renameat and unlinkat |
| 21 | // act on the link itself, and fstatat is told not to. |
| 22 | |
| 23 | // EnsureDirBeneath makes sure name, directly under dir, is a real |
| 24 | // directory and reports what is there. It creates it with mkdirat when it |
| 25 | // is absent, so its group and setgid come from dir the way the kernel |
| 26 | // hands them out, and leaves an existing directory exactly as it is. It |
| 27 | // never changes a mode or an owner: a chmod by a caller outside the |
| 28 | // directory's group silently clears setgid, and a chown into a group the |
| 29 | // caller is not in fails, so the caller checks the returned stat and |
| 30 | // refuses what it cannot vouch for. Anything else at name, a symlink or a |
| 31 | // file, is refused and left untouched. |
| 32 | func EnsureDirBeneath(dir, name string, perm os.FileMode) (DirStat, error) { |
| 33 | if err := bareName(name); err != nil { |
| 34 | return DirStat{}, err |
| 35 | } |
| 36 | dfd, err := openDir(dir) |
| 37 | if err != nil { |
| 38 | return DirStat{}, err |
| 39 | } |
| 40 | defer unix.Close(dfd) |
| 41 | path := filepath.Join(dir, name) |
| 42 | if err := unix.Mkdirat(dfd, name, uint32(perm.Perm())); err != nil && !errors.Is(err, unix.EEXIST) { |
| 43 | return DirStat{}, &os.PathError{Op: "mkdirat", Path: path, Err: err} |
| 44 | } |
| 45 | var st unix.Stat_t |
| 46 | if err := unix.Fstatat(dfd, name, &st, unix.AT_SYMLINK_NOFOLLOW); err != nil { |
| 47 | return DirStat{}, &os.PathError{Op: "fstatat", Path: path, Err: err} |
| 48 | } |
| 49 | switch st.Mode & unix.S_IFMT { |
| 50 | case unix.S_IFDIR: |
| 51 | return dirStatOf(&st), nil |
| 52 | case unix.S_IFLNK: |
| 53 | return DirStat{}, fmt.Errorf("%w: %s", ErrSymlink, path) |
| 54 | } |
| 55 | return DirStat{}, fmt.Errorf("instancefs: %s is not a directory", path) |
| 56 | } |
| 57 | |
| 58 | // StatDir reports dir itself, opened without following a symlink at any |
| 59 | // component. |
| 60 | func StatDir(dir string) (DirStat, error) { |
| 61 | fd, err := openDir(dir) |
| 62 | if err != nil { |
| 63 | return DirStat{}, err |
| 64 | } |
| 65 | defer unix.Close(fd) |
| 66 | var st unix.Stat_t |
| 67 | if err := unix.Fstat(fd, &st); err != nil { |
| 68 | return DirStat{}, &os.PathError{Op: "fstat", Path: dir, Err: err} |
| 69 | } |
| 70 | return dirStatOf(&st), nil |
| 71 | } |
| 72 | |
| 73 | func dirStatOf(st *unix.Stat_t) DirStat { |
| 74 | mode := os.ModeDir | os.FileMode(st.Mode&0o777) |
| 75 | if st.Mode&unix.S_ISUID != 0 { |
| 76 | mode |= os.ModeSetuid |
| 77 | } |
| 78 | if st.Mode&unix.S_ISGID != 0 { |
| 79 | mode |= os.ModeSetgid |
| 80 | } |
| 81 | if st.Mode&unix.S_ISVTX != 0 { |
| 82 | mode |= os.ModeSticky |
| 83 | } |
| 84 | return DirStat{Mode: mode, UID: int(st.Uid), GID: int(st.Gid)} |
| 85 | } |
| 86 | |
| 87 | // RenameBetween renames srcName in srcDir to dstName in dstDir. Both |
| 88 | // directories are opened without following a symlink at any component, so |
| 89 | // neither side of the rename can be redirected; the entry itself moves as |
| 90 | // whatever it is, a link included. An existing entry at the destination is |
| 91 | // replaced, as rename(2) always does: callers that must not replace |
| 92 | // something check for it first. |
| 93 | func RenameBetween(srcDir, srcName, dstDir, dstName string) error { |
| 94 | if err := bareName(srcName); err != nil { |
| 95 | return err |
| 96 | } |
| 97 | if err := bareName(dstName); err != nil { |
| 98 | return err |
| 99 | } |
| 100 | sfd, err := openDir(srcDir) |
| 101 | if err != nil { |
| 102 | return err |
| 103 | } |
| 104 | defer unix.Close(sfd) |
| 105 | dfd, err := openDir(dstDir) |
| 106 | if err != nil { |
| 107 | return err |
| 108 | } |
| 109 | defer unix.Close(dfd) |
| 110 | if err := unix.Renameat(sfd, srcName, dfd, dstName); err != nil { |
| 111 | return &os.LinkError{Op: "renameat", Old: filepath.Join(srcDir, srcName), New: filepath.Join(dstDir, dstName), Err: err} |
| 112 | } |
| 113 | return nil |
| 114 | } |
| 115 | |
| 116 | // RemoveBeneath unlinks name, a non-directory entry directly under dir. A |
| 117 | // symlink there is removed, never its target. |
| 118 | func RemoveBeneath(dir, name string) error { |
| 119 | if err := bareName(name); err != nil { |
| 120 | return err |
| 121 | } |
| 122 | dfd, err := openDir(dir) |
| 123 | if err != nil { |
| 124 | return err |
| 125 | } |
| 126 | defer unix.Close(dfd) |
| 127 | if err := unix.Unlinkat(dfd, name, 0); err != nil { |
| 128 | return &os.PathError{Op: "unlinkat", Path: filepath.Join(dir, name), Err: err} |
| 129 | } |
| 130 | return nil |
| 131 | } |
| 132 | |
| 133 | // RenameBetweenNoReplace is RenameBetween that refuses to replace anything |
| 134 | // at the destination (RENAME_NOREPLACE): EEXIST comes back instead. It is |
| 135 | // for a caller that must not lose what is already at a name, however the |
| 136 | // name came to be taken. |
| 137 | func RenameBetweenNoReplace(srcDir, srcName, dstDir, dstName string) error { |
| 138 | if err := bareName(srcName); err != nil { |
| 139 | return err |
| 140 | } |
| 141 | if err := bareName(dstName); err != nil { |
| 142 | return err |
| 143 | } |
| 144 | sfd, err := openDir(srcDir) |
| 145 | if err != nil { |
| 146 | return err |
| 147 | } |
| 148 | defer unix.Close(sfd) |
| 149 | dfd, err := openDir(dstDir) |
| 150 | if err != nil { |
| 151 | return err |
| 152 | } |
| 153 | defer unix.Close(dfd) |
| 154 | if err := unix.Renameat2(sfd, srcName, dfd, dstName, unix.RENAME_NOREPLACE); err != nil { |
| 155 | return &os.LinkError{Op: "renameat2", Old: filepath.Join(srcDir, srcName), New: filepath.Join(dstDir, dstName), Err: err} |
| 156 | } |
| 157 | return nil |
| 158 | } |
| 159 | |
| 160 | // RemoveAllBeneath removes name, directly under dir, and everything below |
| 161 | // it. No symlink is followed at any depth: dir itself is opened without |
| 162 | // following one (ErrSymlink), every entry is examined with fstatat |
| 163 | // AT_SYMLINK_NOFOLLOW, and a symlink is unlinked, never its target. A |
| 164 | // tenant sharing the agent's uid can plant links and directories in a run |
| 165 | // directory (run spec §5.2); this is the only way the agent deletes one. |
| 166 | // A missing name is not an error. |
| 167 | func RemoveAllBeneath(dir, name string) error { |
| 168 | if err := bareName(name); err != nil { |
| 169 | return err |
| 170 | } |
| 171 | dfd, err := openDir(dir) |
| 172 | if err != nil { |
| 173 | return err |
| 174 | } |
| 175 | defer unix.Close(dfd) |
| 176 | return removeAt(dfd, name, filepath.Join(dir, name), 0) |
| 177 | } |
| 178 | |
| 179 | // removeAt removes name under the directory fd parent; path is for errors. |
| 180 | func removeAt(parent int, name, path string, depth int) error { |
| 181 | if depth > 64 { |
| 182 | return fmt.Errorf("instancefs: %s is nested more than 64 deep", path) |
| 183 | } |
| 184 | var st unix.Stat_t |
| 185 | if err := unix.Fstatat(parent, name, &st, unix.AT_SYMLINK_NOFOLLOW); err != nil { |
| 186 | if errors.Is(err, unix.ENOENT) { |
| 187 | return nil |
| 188 | } |
| 189 | return &os.PathError{Op: "fstatat", Path: path, Err: err} |
| 190 | } |
| 191 | if st.Mode&unix.S_IFMT != unix.S_IFDIR { |
| 192 | if err := unix.Unlinkat(parent, name, 0); err != nil && !errors.Is(err, unix.ENOENT) { |
| 193 | return &os.PathError{Op: "unlinkat", Path: path, Err: err} |
| 194 | } |
| 195 | return nil |
| 196 | } |
| 197 | fd, err := unix.Openat(parent, name, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) |
| 198 | if err != nil { |
| 199 | return &os.PathError{Op: "openat", Path: path, Err: err} |
| 200 | } |
| 201 | f := os.NewFile(uintptr(fd), path) |
| 202 | names, err := f.Readdirnames(-1) |
| 203 | if err != nil { |
| 204 | f.Close() |
| 205 | return err |
| 206 | } |
| 207 | for _, n := range names { |
| 208 | if err := removeAt(fd, n, filepath.Join(path, n), depth+1); err != nil { |
| 209 | f.Close() |
| 210 | return err |
| 211 | } |
| 212 | } |
| 213 | f.Close() |
| 214 | if err := unix.Unlinkat(parent, name, unix.AT_REMOVEDIR); err != nil && !errors.Is(err, unix.ENOENT) { |
| 215 | return &os.PathError{Op: "unlinkat", Path: path, Err: err} |
| 216 | } |
| 217 | return nil |
| 218 | } |
| 219 | |