| 1 | //go:build linux |
| 2 | |
| 3 | package instancefs |
| 4 | |
| 5 | import ( |
| 6 | "errors" |
| 7 | "os" |
| 8 | "path/filepath" |
| 9 | "syscall" |
| 10 | "testing" |
| 11 | ) |
| 12 | |
| 13 | // A new directory takes its mode from mkdirat and its group and setgid |
| 14 | // from its parent, the way the kernel hands them out; the stat returned is |
| 15 | // what is on disk. |
| 16 | func TestEnsureDirBeneathCreatesAndInheritsSetgid(t *testing.T) { |
| 17 | dir := t.TempDir() |
| 18 | if err := os.Chmod(dir, 0o750|os.ModeSetgid); err != nil { |
| 19 | t.Fatal(err) |
| 20 | } |
| 21 | st, err := EnsureDirBeneath(dir, "run", 0o700) |
| 22 | if err != nil { |
| 23 | t.Fatalf("EnsureDirBeneath: %v", err) |
| 24 | } |
| 25 | fi, err := os.Lstat(filepath.Join(dir, "run")) |
| 26 | if err != nil { |
| 27 | t.Fatal(err) |
| 28 | } |
| 29 | if !fi.IsDir() || fi.Mode().Perm() != 0o700 || fi.Mode()&os.ModeSetgid == 0 { |
| 30 | t.Fatalf("mode = %v, want a directory 0700 with setgid inherited", fi.Mode()) |
| 31 | } |
| 32 | if st.Mode != fi.Mode() || st.UID != os.Geteuid() || st.GID != int(fi.Sys().(*syscall.Stat_t).Gid) { |
| 33 | t.Fatalf("stat = %+v, disk says %v", st, fi.Mode()) |
| 34 | } |
| 35 | } |
| 36 | |
| 37 | // An existing directory is reported as it stands and never changed: a |
| 38 | // chmod by a caller outside the directory's group clears setgid, so the |
| 39 | // caller verifies instead. |
| 40 | func TestEnsureDirBeneathLeavesAnExistingDirectoryAlone(t *testing.T) { |
| 41 | dir := t.TempDir() |
| 42 | d := filepath.Join(dir, "d") |
| 43 | if err := os.Mkdir(d, 0o700); err != nil { |
| 44 | t.Fatal(err) |
| 45 | } |
| 46 | if err := os.Chmod(d, 0o777|os.ModeSetgid); err != nil { |
| 47 | t.Fatal(err) |
| 48 | } |
| 49 | st, err := EnsureDirBeneath(dir, "d", 0o700) |
| 50 | if err != nil { |
| 51 | t.Fatalf("EnsureDirBeneath over an existing directory: %v", err) |
| 52 | } |
| 53 | want := os.ModeDir | os.ModeSetgid | 0o777 |
| 54 | if st.Mode != want { |
| 55 | t.Errorf("stat mode = %v, want %v", st.Mode, want) |
| 56 | } |
| 57 | if fi, _ := os.Lstat(d); fi.Mode() != want { |
| 58 | t.Errorf("mode changed to %v", fi.Mode()) |
| 59 | } |
| 60 | } |
| 61 | |
| 62 | func TestEnsureDirBeneathRefusesASymlinkAndLeavesItsTargetAlone(t *testing.T) { |
| 63 | dir, elsewhere := t.TempDir(), t.TempDir() |
| 64 | if err := os.Chmod(elsewhere, 0o755); err != nil { |
| 65 | t.Fatal(err) |
| 66 | } |
| 67 | if err := os.Symlink(elsewhere, filepath.Join(dir, "d")); err != nil { |
| 68 | t.Fatal(err) |
| 69 | } |
| 70 | if _, err := EnsureDirBeneath(dir, "d", 0o700); !errors.Is(err, ErrSymlink) { |
| 71 | t.Fatalf("err = %v, want ErrSymlink", err) |
| 72 | } |
| 73 | fi, _ := os.Stat(elsewhere) |
| 74 | if fi.Mode().Perm() != 0o755 { |
| 75 | t.Fatalf("the symlink's target was changed to %v", fi.Mode()) |
| 76 | } |
| 77 | } |
| 78 | |
| 79 | func TestEnsureDirBeneathRefusesAFileAndAPathName(t *testing.T) { |
| 80 | dir := t.TempDir() |
| 81 | if err := os.WriteFile(filepath.Join(dir, "f"), nil, 0o600); err != nil { |
| 82 | t.Fatal(err) |
| 83 | } |
| 84 | if _, err := EnsureDirBeneath(dir, "f", 0o700); err == nil { |
| 85 | t.Fatal("a regular file accepted as a directory") |
| 86 | } |
| 87 | if _, err := EnsureDirBeneath(dir, "a/b", 0o700); !errors.Is(err, ErrEscapes) { |
| 88 | t.Fatalf("a multi-component name: err = %v, want ErrEscapes", err) |
| 89 | } |
| 90 | if _, err := EnsureDirBeneath(dir, "..", 0o700); !errors.Is(err, ErrEscapes) { |
| 91 | t.Fatalf("..: err = %v, want ErrEscapes", err) |
| 92 | } |
| 93 | } |
| 94 | |
| 95 | func TestEnsureDirBeneathRefusesASymlinkedParent(t *testing.T) { |
| 96 | base, real := t.TempDir(), t.TempDir() |
| 97 | link := filepath.Join(base, "link") |
| 98 | if err := os.Symlink(real, link); err != nil { |
| 99 | t.Fatal(err) |
| 100 | } |
| 101 | if _, err := EnsureDirBeneath(link, "d", 0o700); !errors.Is(err, ErrSymlink) { |
| 102 | t.Fatalf("err = %v, want ErrSymlink", err) |
| 103 | } |
| 104 | if _, err := os.Lstat(filepath.Join(real, "d")); !errors.Is(err, os.ErrNotExist) { |
| 105 | t.Fatal("a directory was created through the symlinked parent") |
| 106 | } |
| 107 | } |
| 108 | |
| 109 | func TestStatDirReportsModeAndGroupAndRefusesASymlink(t *testing.T) { |
| 110 | base := t.TempDir() |
| 111 | d := filepath.Join(base, "d") |
| 112 | if err := os.Mkdir(d, 0o700); err != nil { |
| 113 | t.Fatal(err) |
| 114 | } |
| 115 | if err := os.Chmod(d, 0o750|os.ModeSetgid); err != nil { |
| 116 | t.Fatal(err) |
| 117 | } |
| 118 | st, err := StatDir(d) |
| 119 | if err != nil { |
| 120 | t.Fatal(err) |
| 121 | } |
| 122 | if st.Mode != os.ModeDir|os.ModeSetgid|0o750 || st.GID != os.Getegid() { |
| 123 | t.Fatalf("stat = %+v", st) |
| 124 | } |
| 125 | if err := os.Symlink(d, filepath.Join(base, "link")); err != nil { |
| 126 | t.Fatal(err) |
| 127 | } |
| 128 | if _, err := StatDir(filepath.Join(base, "link")); !errors.Is(err, ErrSymlink) { |
| 129 | t.Fatalf("err = %v, want ErrSymlink", err) |
| 130 | } |
| 131 | if _, err := StatDir(filepath.Join(base, "absent")); !errors.Is(err, os.ErrNotExist) { |
| 132 | t.Fatalf("err = %v, want ErrNotExist", err) |
| 133 | } |
| 134 | } |
| 135 | |
| 136 | func TestRenameBetweenMovesFilesAndDirectories(t *testing.T) { |
| 137 | src, dst := t.TempDir(), t.TempDir() |
| 138 | if err := os.WriteFile(filepath.Join(src, "h.db"), []byte("db"), 0o600); err != nil { |
| 139 | t.Fatal(err) |
| 140 | } |
| 141 | if err := os.Mkdir(filepath.Join(src, "shadow"), 0o700); err != nil { |
| 142 | t.Fatal(err) |
| 143 | } |
| 144 | if err := RenameBetween(src, "h.db", dst, "h.db"); err != nil { |
| 145 | t.Fatalf("file: %v", err) |
| 146 | } |
| 147 | if err := RenameBetween(src, "shadow", dst, "shadow"); err != nil { |
| 148 | t.Fatalf("directory: %v", err) |
| 149 | } |
| 150 | if b, err := os.ReadFile(filepath.Join(dst, "h.db")); err != nil || string(b) != "db" { |
| 151 | t.Fatalf("moved file: %q %v", b, err) |
| 152 | } |
| 153 | if fi, err := os.Lstat(filepath.Join(dst, "shadow")); err != nil || !fi.IsDir() { |
| 154 | t.Fatalf("moved directory: %v %v", fi, err) |
| 155 | } |
| 156 | } |
| 157 | |
| 158 | // A symlink at the source name is moved as the link it is, never followed: |
| 159 | // rename works on directory entries. |
| 160 | func TestRenameBetweenMovesASymlinkAsALink(t *testing.T) { |
| 161 | src, dst, elsewhere := t.TempDir(), t.TempDir(), t.TempDir() |
| 162 | target := filepath.Join(elsewhere, "secret") |
| 163 | if err := os.WriteFile(target, []byte("s"), 0o600); err != nil { |
| 164 | t.Fatal(err) |
| 165 | } |
| 166 | if err := os.Symlink(target, filepath.Join(src, "h.env")); err != nil { |
| 167 | t.Fatal(err) |
| 168 | } |
| 169 | if err := RenameBetween(src, "h.env", dst, "h.env"); err != nil { |
| 170 | t.Fatal(err) |
| 171 | } |
| 172 | if fi, err := os.Lstat(filepath.Join(dst, "h.env")); err != nil || fi.Mode()&os.ModeSymlink == 0 { |
| 173 | t.Fatalf("destination is not the link: %v %v", fi, err) |
| 174 | } |
| 175 | if _, err := os.Lstat(target); err != nil { |
| 176 | t.Fatalf("the link's target moved: %v", err) |
| 177 | } |
| 178 | } |
| 179 | |
| 180 | func TestRenameBetweenRefusesASymlinkedDirectoryOnEitherSide(t *testing.T) { |
| 181 | base, real := t.TempDir(), t.TempDir() |
| 182 | link := filepath.Join(base, "link") |
| 183 | if err := os.Symlink(real, link); err != nil { |
| 184 | t.Fatal(err) |
| 185 | } |
| 186 | plain := t.TempDir() |
| 187 | if err := os.WriteFile(filepath.Join(plain, "h.db"), []byte("db"), 0o600); err != nil { |
| 188 | t.Fatal(err) |
| 189 | } |
| 190 | if err := RenameBetween(plain, "h.db", link, "h.db"); !errors.Is(err, ErrSymlink) { |
| 191 | t.Fatalf("symlinked destination: err = %v, want ErrSymlink", err) |
| 192 | } |
| 193 | if _, err := os.Lstat(filepath.Join(real, "h.db")); !errors.Is(err, os.ErrNotExist) { |
| 194 | t.Fatal("the file landed through the symlinked destination") |
| 195 | } |
| 196 | if err := os.WriteFile(filepath.Join(real, "x"), nil, 0o600); err != nil { |
| 197 | t.Fatal(err) |
| 198 | } |
| 199 | if err := RenameBetween(link, "x", plain, "x"); !errors.Is(err, ErrSymlink) { |
| 200 | t.Fatalf("symlinked source: err = %v, want ErrSymlink", err) |
| 201 | } |
| 202 | } |
| 203 | |
| 204 | func TestRenameBetweenRefusesPathNames(t *testing.T) { |
| 205 | a, b := t.TempDir(), t.TempDir() |
| 206 | for _, bad := range [][2]string{{"x/y", "y"}, {"y", "../y"}, {"", "y"}, {"y", "."}} { |
| 207 | if err := RenameBetween(a, bad[0], b, bad[1]); !errors.Is(err, ErrEscapes) { |
| 208 | t.Errorf("%q -> %q: err = %v, want ErrEscapes", bad[0], bad[1], err) |
| 209 | } |
| 210 | } |
| 211 | } |
| 212 | |
| 213 | func TestRemoveBeneathRemovesTheEntryNotItsTarget(t *testing.T) { |
| 214 | dir, elsewhere := t.TempDir(), t.TempDir() |
| 215 | target := filepath.Join(elsewhere, "keep") |
| 216 | if err := os.WriteFile(target, []byte("k"), 0o600); err != nil { |
| 217 | t.Fatal(err) |
| 218 | } |
| 219 | if err := os.Symlink(target, filepath.Join(dir, "link")); err != nil { |
| 220 | t.Fatal(err) |
| 221 | } |
| 222 | if err := RemoveBeneath(dir, "link"); err != nil { |
| 223 | t.Fatal(err) |
| 224 | } |
| 225 | if _, err := os.Lstat(target); err != nil { |
| 226 | t.Fatalf("the link's target was removed: %v", err) |
| 227 | } |
| 228 | if err := RemoveBeneath(dir, "link"); !errors.Is(err, os.ErrNotExist) { |
| 229 | t.Fatalf("second remove: err = %v, want ErrNotExist", err) |
| 230 | } |
| 231 | } |
| 232 | |
| 233 | func TestRemoveAllBeneathRemovesATree(t *testing.T) { |
| 234 | dir := t.TempDir() |
| 235 | for _, p := range []string{"run/a", "run/sub/b", "run/sub/deeper/c"} { |
| 236 | full := filepath.Join(dir, p) |
| 237 | if err := os.MkdirAll(filepath.Dir(full), 0o700); err != nil { |
| 238 | t.Fatal(err) |
| 239 | } |
| 240 | if err := os.WriteFile(full, []byte("x"), 0o600); err != nil { |
| 241 | t.Fatal(err) |
| 242 | } |
| 243 | } |
| 244 | if err := RemoveAllBeneath(dir, "run"); err != nil { |
| 245 | t.Fatal(err) |
| 246 | } |
| 247 | if _, err := os.Lstat(filepath.Join(dir, "run")); !os.IsNotExist(err) { |
| 248 | t.Fatalf("run survived: %v", err) |
| 249 | } |
| 250 | if err := RemoveAllBeneath(dir, "run"); err != nil { |
| 251 | t.Fatalf("a missing name must be nil: %v", err) |
| 252 | } |
| 253 | } |
| 254 | |
| 255 | func TestRemoveAllBeneathNeverFollowsASymlink(t *testing.T) { |
| 256 | dir, outside := t.TempDir(), t.TempDir() |
| 257 | keep := filepath.Join(outside, "keep") |
| 258 | if err := os.WriteFile(keep, []byte("x"), 0o600); err != nil { |
| 259 | t.Fatal(err) |
| 260 | } |
| 261 | if err := os.MkdirAll(filepath.Join(dir, "run"), 0o700); err != nil { |
| 262 | t.Fatal(err) |
| 263 | } |
| 264 | if err := os.Symlink(outside, filepath.Join(dir, "run", "link")); err != nil { |
| 265 | t.Fatal(err) |
| 266 | } |
| 267 | if err := os.Symlink(outside, filepath.Join(dir, "top")); err != nil { |
| 268 | t.Fatal(err) |
| 269 | } |
| 270 | if err := RemoveAllBeneath(dir, "run"); err != nil { |
| 271 | t.Fatal(err) |
| 272 | } |
| 273 | if err := RemoveAllBeneath(dir, "top"); err != nil { |
| 274 | t.Fatal(err) |
| 275 | } |
| 276 | if _, err := os.Stat(keep); err != nil { |
| 277 | t.Fatalf("a symlink's target was removed: %v", err) |
| 278 | } |
| 279 | if _, err := os.Lstat(filepath.Join(dir, "top")); !os.IsNotExist(err) { |
| 280 | t.Fatalf("the top-level symlink itself survived: %v", err) |
| 281 | } |
| 282 | } |
| 283 | |
| 284 | func TestRemoveAllBeneathRefusesPathsAndSymlinkedParents(t *testing.T) { |
| 285 | dir, outside := t.TempDir(), t.TempDir() |
| 286 | if err := RemoveAllBeneath(dir, "a/b"); !errors.Is(err, ErrEscapes) { |
| 287 | t.Errorf("a/b: %v, want ErrEscapes", err) |
| 288 | } |
| 289 | link := filepath.Join(dir, "parent") |
| 290 | if err := os.Symlink(outside, link); err != nil { |
| 291 | t.Fatal(err) |
| 292 | } |
| 293 | if err := os.Mkdir(filepath.Join(outside, "run"), 0o700); err != nil { |
| 294 | t.Fatal(err) |
| 295 | } |
| 296 | if err := RemoveAllBeneath(link, "run"); !errors.Is(err, ErrSymlink) { |
| 297 | t.Errorf("symlinked parent: %v, want ErrSymlink", err) |
| 298 | } |
| 299 | if _, err := os.Stat(filepath.Join(outside, "run")); err != nil { |
| 300 | t.Errorf("removed through a symlinked parent: %v", err) |
| 301 | } |
| 302 | } |
| 303 | |