github.com/opencontainers/umoci@v0.4.8-0.20240508124516-656e4836fb0d/oci/layer/tar_extract_test.go (about) 1 /* 2 * umoci: Umoci Modifies Open Containers' Images 3 * Copyright (C) 2016-2020 SUSE LLC 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18 package layer 19 20 import ( 21 "archive/tar" 22 "bytes" 23 "crypto/rand" 24 "io" 25 "io/ioutil" 26 "os" 27 "path/filepath" 28 "runtime" 29 "strings" 30 "testing" 31 "time" 32 33 rspec "github.com/opencontainers/runtime-spec/specs-go" 34 "github.com/opencontainers/umoci/pkg/testutils" 35 "github.com/pkg/errors" 36 "golang.org/x/sys/unix" 37 ) 38 39 // TODO: Test the parent directory metadata is kept the same when unpacking. 40 // TODO: Add tests for metadata and consistency. 41 42 // testUnpackEntrySanitiseHelper is a basic helper to check that a tar header 43 // with the given prefix will resolve to the same path without it during 44 // unpacking. The "unsafe" version should resolve to the parent directory 45 // (which will be checked). The rootfs is assumed to be <dir>/rootfs. 46 func testUnpackEntrySanitiseHelper(t *testing.T, dir, file, prefix string) { 47 hostValue := []byte("host content") 48 ctrValue := []byte("container content") 49 50 rootfs := filepath.Join(dir, "rootfs") 51 52 // Create a host file that we want to make sure doesn't get overwrittern. 53 if err := ioutil.WriteFile(filepath.Join(dir, "file"), hostValue, 0644); err != nil { 54 t.Fatal(err) 55 } 56 57 // Create our header. We raw prepend the prefix because we are generating 58 // invalid tar headers. 59 hdr := &tar.Header{ 60 Name: prefix + "/" + filepath.Base(file), 61 Uid: os.Getuid(), 62 Gid: os.Getgid(), 63 Mode: 0644, 64 Size: int64(len(ctrValue)), 65 Typeflag: tar.TypeReg, 66 ModTime: time.Now(), 67 AccessTime: time.Now(), 68 ChangeTime: time.Now(), 69 } 70 71 te := NewTarExtractor(UnpackOptions{}) 72 if err := te.UnpackEntry(rootfs, hdr, bytes.NewBuffer(ctrValue)); err != nil { 73 t.Fatalf("unexpected UnpackEntry error: %s", err) 74 } 75 76 hostValueGot, err := ioutil.ReadFile(filepath.Join(dir, "file")) 77 if err != nil { 78 t.Fatalf("unexpected readfile error on host: %s", err) 79 } 80 81 ctrValueGot, err := ioutil.ReadFile(filepath.Join(rootfs, "file")) 82 if err != nil { 83 t.Fatalf("unexpected readfile error in ctr: %s", err) 84 } 85 86 if !bytes.Equal(ctrValue, ctrValueGot) { 87 t.Errorf("ctr path was not updated: expected='%s' got='%s'", string(ctrValue), string(ctrValueGot)) 88 } 89 if !bytes.Equal(hostValue, hostValueGot) { 90 t.Errorf("HOST PATH WAS CHANGED! THIS IS A PATH ESCAPE! expected='%s' got='%s'", string(hostValue), string(hostValueGot)) 91 } 92 } 93 94 // TestUnpackEntrySanitiseScoping makes sure that path sanitisation is done 95 // safely with regards to /../../ prefixes in invalid tar archives. 96 func TestUnpackEntrySanitiseScoping(t *testing.T) { 97 for _, test := range []struct { 98 name string 99 prefix string 100 }{ 101 {"GarbagePrefix", "/.."}, 102 {"DotDotPrefix", ".."}, 103 } { 104 t.Run(test.name, func(t *testing.T) { 105 dir, err := ioutil.TempDir("", "umoci-TestUnpackEntrySanitiseScoping") 106 if err != nil { 107 t.Fatal(err) 108 } 109 defer os.RemoveAll(dir) 110 111 rootfs := filepath.Join(dir, "rootfs") 112 if err := os.Mkdir(rootfs, 0755); err != nil { 113 t.Fatal(err) 114 } 115 116 testUnpackEntrySanitiseHelper(t, dir, filepath.Join("/", test.prefix, "file"), test.prefix) 117 }) 118 } 119 } 120 121 // TestUnpackEntrySymlinkScoping makes sure that path sanitisation is done 122 // safely with regards to symlinks path components set to /.. and similar 123 // prefixes in invalid tar archives (a regular tar archive won't contain stuff 124 // like that). 125 func TestUnpackEntrySymlinkScoping(t *testing.T) { 126 for _, test := range []struct { 127 name string 128 prefix string 129 }{ 130 {"RootPrefix", "/"}, 131 {"GarbagePrefix1", "/../"}, 132 {"GarbagePrefix2", "/../../../../../../../../../../../../../../../"}, 133 {"GarbagePrefix3", "/./.././.././.././.././.././.././.././.././../"}, 134 {"DotDotPrefix", ".."}, 135 } { 136 t.Run(test.name, func(t *testing.T) { 137 dir, err := ioutil.TempDir("", "umoci-TestUnpackEntrySymlinkScoping") 138 if err != nil { 139 t.Fatal(err) 140 } 141 defer os.RemoveAll(dir) 142 143 rootfs := filepath.Join(dir, "rootfs") 144 if err := os.Mkdir(rootfs, 0755); err != nil { 145 t.Fatal(err) 146 } 147 148 // Create the symlink. 149 if err := os.Symlink(test.prefix, filepath.Join(rootfs, "link")); err != nil { 150 t.Fatal(err) 151 } 152 153 testUnpackEntrySanitiseHelper(t, dir, filepath.Join("/", test.prefix, "file"), "link") 154 }) 155 } 156 } 157 158 // TestUnpackEntryParentDir ensures that when UnpackEntry hits a path that 159 // doesn't have its leading directories, we create all of the parent 160 // directories. 161 func TestUnpackEntryParentDir(t *testing.T) { 162 dir, err := ioutil.TempDir("", "umoci-TestUnpackEntryParentDir") 163 if err != nil { 164 t.Fatal(err) 165 } 166 defer os.RemoveAll(dir) 167 168 rootfs := filepath.Join(dir, "rootfs") 169 if err := os.Mkdir(rootfs, 0755); err != nil { 170 t.Fatal(err) 171 } 172 173 ctrValue := []byte("creating parentdirs") 174 175 // Create our header. We raw prepend the prefix because we are generating 176 // invalid tar headers. 177 hdr := &tar.Header{ 178 Name: "a/b/c/file", 179 Uid: os.Getuid(), 180 Gid: os.Getgid(), 181 Mode: 0644, 182 Size: int64(len(ctrValue)), 183 Typeflag: tar.TypeReg, 184 ModTime: time.Now(), 185 AccessTime: time.Now(), 186 ChangeTime: time.Now(), 187 } 188 189 te := NewTarExtractor(UnpackOptions{}) 190 if err := te.UnpackEntry(rootfs, hdr, bytes.NewBuffer(ctrValue)); err != nil { 191 t.Fatalf("unexpected UnpackEntry error: %s", err) 192 } 193 194 ctrValueGot, err := ioutil.ReadFile(filepath.Join(rootfs, "a/b/c/file")) 195 if err != nil { 196 t.Fatalf("unexpected readfile error: %s", err) 197 } 198 199 if !bytes.Equal(ctrValue, ctrValueGot) { 200 t.Errorf("ctr path was not updated: expected='%s' got='%s'", string(ctrValue), string(ctrValueGot)) 201 } 202 } 203 204 // TestUnpackEntryWhiteout checks whether whiteout handling is done correctly, 205 // as well as ensuring that the metadata of the parent is maintained. 206 func TestUnpackEntryWhiteout(t *testing.T) { 207 for _, test := range []struct { 208 name string 209 path string 210 dir bool // TODO: Switch to Typeflag 211 }{ 212 {"FileInRoot", "rootpath", false}, 213 {"HiddenFileInRoot", ".hiddenroot", false}, 214 {"FileInSubdir", "some/path/file", false}, 215 {"HiddenFileInSubdir", "another/path/.hiddenfile", false}, 216 {"DirInRoot", "rootpath", true}, 217 {"HiddenDirInRoot", ".hiddenroot", true}, 218 {"DirInSubdir", "some/path/dir", true}, 219 {"HiddenDirInSubdir", "another/path/.hiddendir", true}, 220 } { 221 t.Run(test.name, func(t *testing.T) { 222 testMtime := testutils.Unix(123, 456) 223 testAtime := testutils.Unix(789, 111) 224 225 dir, err := ioutil.TempDir("", "umoci-TestUnpackEntryWhiteout") 226 if err != nil { 227 t.Fatal(err) 228 } 229 defer os.RemoveAll(dir) 230 231 rawDir, rawFile := filepath.Split(test.path) 232 wh := filepath.Join(rawDir, whPrefix+rawFile) 233 234 // Create the parent directory. 235 if err := os.MkdirAll(filepath.Join(dir, rawDir), 0755); err != nil { 236 t.Fatal(err) 237 } 238 239 // Create the path itself. 240 if test.dir { 241 if err := os.Mkdir(filepath.Join(dir, test.path), 0755); err != nil { 242 t.Fatal(err) 243 } 244 // Make some subfiles and directories. 245 if err := ioutil.WriteFile(filepath.Join(dir, test.path, "file1"), []byte("some value"), 0644); err != nil { 246 t.Fatal(err) 247 } 248 if err := ioutil.WriteFile(filepath.Join(dir, test.path, "file2"), []byte("some value"), 0644); err != nil { 249 t.Fatal(err) 250 } 251 if err := os.Mkdir(filepath.Join(dir, test.path, ".subdir"), 0755); err != nil { 252 t.Fatal(err) 253 } 254 if err := ioutil.WriteFile(filepath.Join(dir, test.path, ".subdir", "file3"), []byte("some value"), 0644); err != nil { 255 t.Fatal(err) 256 } 257 } else { 258 if err := ioutil.WriteFile(filepath.Join(dir, test.path), []byte("some value"), 0644); err != nil { 259 t.Fatal(err) 260 } 261 } 262 263 // Set the modified time of the directory itself. 264 if err := os.Chtimes(filepath.Join(dir, rawDir), testAtime, testMtime); err != nil { 265 t.Fatal(err) 266 } 267 268 // Whiteout the path. 269 hdr := &tar.Header{ 270 Name: wh, 271 Typeflag: tar.TypeReg, 272 } 273 274 te := NewTarExtractor(UnpackOptions{}) 275 if err := te.UnpackEntry(dir, hdr, nil); err != nil { 276 t.Fatalf("unexpected error in UnpackEntry: %s", err) 277 } 278 279 // Make sure that the path is gone. 280 if _, err := os.Lstat(filepath.Join(dir, test.path)); !os.IsNotExist(err) { 281 if err != nil { 282 t.Fatalf("unexpected error checking whiteout out path: %s", err) 283 } 284 t.Errorf("path was not removed by whiteout: %s", test.path) 285 } 286 287 // Make sure the parent directory wasn't modified. 288 if fi, err := os.Lstat(filepath.Join(dir, rawDir)); err != nil { 289 t.Fatalf("error checking parent directory of whiteout: %s", err) 290 } else { 291 hdr, err := tar.FileInfoHeader(fi, "") 292 if err != nil { 293 t.Fatalf("error generating header from fileinfo of parent directory of whiteout: %s", err) 294 } 295 296 if !hdr.ModTime.Equal(testMtime) { 297 t.Errorf("mtime of parent directory changed after whiteout: got='%s' expected='%s'", hdr.ModTime, testMtime) 298 } 299 if !hdr.AccessTime.Equal(testAtime) { 300 t.Errorf("atime of parent directory changed after whiteout: got='%s' expected='%s'", hdr.ModTime, testAtime) 301 } 302 } 303 }) 304 } 305 } 306 307 type pseudoHdr struct { 308 path string 309 linkname string 310 typeflag byte 311 upper bool 312 } 313 314 func fromPseudoHdr(ph pseudoHdr) (*tar.Header, io.Reader) { 315 var r io.Reader 316 var size int64 317 if ph.typeflag == tar.TypeReg || ph.typeflag == tar.TypeRegA { 318 size = 256 * 1024 319 r = &io.LimitedReader{ 320 R: rand.Reader, 321 N: size, 322 } 323 } 324 325 mode := os.FileMode(0777) 326 if ph.typeflag == tar.TypeDir { 327 mode |= os.ModeDir 328 } 329 330 return &tar.Header{ 331 Name: ph.path, 332 Linkname: ph.linkname, 333 Typeflag: ph.typeflag, 334 Mode: int64(mode), 335 Size: size, 336 ModTime: testutils.Unix(1210393, 4528036), 337 AccessTime: testutils.Unix(7892829, 2341211), 338 ChangeTime: testutils.Unix(8731293, 8218947), 339 }, r 340 } 341 342 // TestUnpackOpaqueWhiteout checks whether *opaque* whiteout handling is done 343 // correctly, as well as ensuring that the metadata of the parent is 344 // maintained -- and that upperdir entries are handled. 345 func TestUnpackOpaqueWhiteout(t *testing.T) { 346 for _, test := range []struct { 347 name string 348 ignoreExist bool // ignore if extra upper files exist 349 pseudoHeaders []pseudoHdr 350 }{ 351 {"EmptyDir", false, nil}, 352 {"OneLevel", false, []pseudoHdr{ 353 {"file", "", tar.TypeReg, false}, 354 {"link", "..", tar.TypeSymlink, true}, 355 {"badlink", "./nothing", tar.TypeSymlink, true}, 356 {"fifo", "", tar.TypeFifo, false}, 357 }}, 358 {"OneLevelNoUpper", false, []pseudoHdr{ 359 {"file", "", tar.TypeReg, false}, 360 {"link", "..", tar.TypeSymlink, false}, 361 {"badlink", "./nothing", tar.TypeSymlink, false}, 362 {"fifo", "", tar.TypeFifo, false}, 363 }}, 364 {"TwoLevel", false, []pseudoHdr{ 365 {"file", "", tar.TypeReg, true}, 366 {"link", "..", tar.TypeSymlink, false}, 367 {"badlink", "./nothing", tar.TypeSymlink, false}, 368 {"dir", "", tar.TypeDir, true}, 369 {"dir/file", "", tar.TypeRegA, true}, 370 {"dir/link", "../badlink", tar.TypeSymlink, false}, 371 {"dir/verybadlink", "../../../../../../../../../../../../etc/shadow", tar.TypeSymlink, true}, 372 {"dir/verybadlink2", "/../../../../../../../../../../../../etc/shadow", tar.TypeSymlink, false}, 373 }}, 374 {"TwoLevelNoUpper", false, []pseudoHdr{ 375 {"file", "", tar.TypeReg, false}, 376 {"link", "..", tar.TypeSymlink, false}, 377 {"badlink", "./nothing", tar.TypeSymlink, false}, 378 {"dir", "", tar.TypeDir, false}, 379 {"dir/file", "", tar.TypeRegA, false}, 380 {"dir/link", "../badlink", tar.TypeSymlink, false}, 381 {"dir/verybadlink", "../../../../../../../../../../../../etc/shadow", tar.TypeSymlink, false}, 382 {"dir/verybadlink2", "/../../../../../../../../../../../../etc/shadow", tar.TypeSymlink, false}, 383 }}, 384 {"MultiLevel", false, []pseudoHdr{ 385 {"level1_file", "", tar.TypeReg, true}, 386 {"level1_link", "..", tar.TypeSymlink, false}, 387 {"level1a", "", tar.TypeDir, true}, 388 {"level1a/level2_file", "", tar.TypeRegA, false}, 389 {"level1a/level2_link", "../../../", tar.TypeSymlink, true}, 390 {"level1a/level2a", "", tar.TypeDir, false}, 391 {"level1a/level2a/level3_fileA", "", tar.TypeReg, false}, 392 {"level1a/level2a/level3_fileB", "", tar.TypeReg, false}, 393 {"level1a/level2b", "", tar.TypeDir, true}, 394 {"level1a/level2b/level3_fileA", "", tar.TypeReg, true}, 395 {"level1a/level2b/level3_fileB", "", tar.TypeReg, false}, 396 {"level1a/level2b/level3", "", tar.TypeDir, false}, 397 {"level1a/level2b/level3/level4", "", tar.TypeDir, false}, 398 {"level1a/level2b/level3/level4", "", tar.TypeDir, false}, 399 {"level1a/level2b/level3_fileA", "", tar.TypeReg, true}, 400 {"level1b", "", tar.TypeDir, false}, 401 {"level1b/level2_fileA", "", tar.TypeReg, false}, 402 {"level1b/level2_fileB", "", tar.TypeReg, false}, 403 {"level1b/level2", "", tar.TypeDir, false}, 404 {"level1b/level2/level3_file", "", tar.TypeReg, false}, 405 }}, 406 {"MultiLevelNoUpper", false, []pseudoHdr{ 407 {"level1_file", "", tar.TypeReg, false}, 408 {"level1_link", "..", tar.TypeSymlink, false}, 409 {"level1a", "", tar.TypeDir, false}, 410 {"level1a/level2_file", "", tar.TypeRegA, false}, 411 {"level1a/level2_link", "../../../", tar.TypeSymlink, false}, 412 {"level1a/level2a", "", tar.TypeDir, false}, 413 {"level1a/level2a/level3_fileA", "", tar.TypeReg, false}, 414 {"level1a/level2a/level3_fileB", "", tar.TypeReg, false}, 415 {"level1a/level2b", "", tar.TypeDir, false}, 416 {"level1a/level2b/level3_fileA", "", tar.TypeReg, false}, 417 {"level1a/level2b/level3_fileB", "", tar.TypeReg, false}, 418 {"level1a/level2b/level3", "", tar.TypeDir, false}, 419 {"level1a/level2b/level3/level4", "", tar.TypeDir, false}, 420 {"level1a/level2b/level3/level4", "", tar.TypeDir, false}, 421 {"level1a/level2b/level3_fileA", "", tar.TypeReg, false}, 422 {"level1b", "", tar.TypeDir, false}, 423 {"level1b/level2_fileA", "", tar.TypeReg, false}, 424 {"level1b/level2_fileB", "", tar.TypeReg, false}, 425 {"level1b/level2", "", tar.TypeDir, false}, 426 {"level1b/level2/level3_file", "", tar.TypeReg, false}, 427 }}, 428 {"MissingUpperAncestor", true, []pseudoHdr{ 429 {"some", "", tar.TypeDir, false}, 430 {"some/dir", "", tar.TypeDir, false}, 431 {"some/dir/somewhere", "", tar.TypeReg, true}, 432 {"another", "", tar.TypeDir, false}, 433 {"another/dir", "", tar.TypeDir, false}, 434 {"another/dir/somewhere", "", tar.TypeReg, false}, 435 }}, 436 {"UpperWhiteout", false, []pseudoHdr{ 437 {whPrefix + "fileB", "", tar.TypeReg, true}, 438 {"fileA", "", tar.TypeReg, true}, 439 {"fileB", "", tar.TypeReg, true}, 440 {"fileC", "", tar.TypeReg, false}, 441 {whPrefix + "fileA", "", tar.TypeReg, true}, 442 {whPrefix + "fileC", "", tar.TypeReg, true}, 443 }}, 444 // XXX: What umoci should do here is not really defined by the 445 // spec. In particular, whether you need a whiteout for every 446 // sub-path or just the path itself is not well-defined. This 447 // code assumes that you *do not*. 448 {"UpperDirWhiteout", false, []pseudoHdr{ 449 {whPrefix + "dir2", "", tar.TypeReg, true}, 450 {"file", "", tar.TypeReg, false}, 451 {"dir1", "", tar.TypeDir, true}, 452 {"dir1/file", "", tar.TypeRegA, true}, 453 {"dir1/link", "../badlink", tar.TypeSymlink, false}, 454 {"dir1/verybadlink", "../../../../../../../../../../../../etc/shadow", tar.TypeSymlink, true}, 455 {"dir1/verybadlink2", "/../../../../../../../../../../../../etc/shadow", tar.TypeSymlink, false}, 456 {whPrefix + "dir1", "", tar.TypeReg, true}, 457 {"dir2", "", tar.TypeDir, true}, 458 {"dir2/file", "", tar.TypeRegA, true}, 459 {"dir2/link", "../badlink", tar.TypeSymlink, false}, 460 }}, 461 } { 462 t.Run(test.name, func(t *testing.T) { 463 unpackOptions := UnpackOptions{ 464 MapOptions: MapOptions{ 465 Rootless: os.Geteuid() != 0, 466 }, 467 } 468 469 dir, err := ioutil.TempDir("", "umoci-TestUnpackOpaqueWhiteout") 470 if err != nil { 471 t.Fatal(err) 472 } 473 defer os.RemoveAll(dir) 474 475 // We do all whiteouts in a subdirectory. 476 whiteoutDir := "test-subdir" 477 whiteoutRoot := filepath.Join(dir, whiteoutDir) 478 if err := os.MkdirAll(whiteoutRoot, 0755); err != nil { 479 t.Fatal(err) 480 } 481 482 // Track if we have upper entries. 483 numUpper := 0 484 485 // First we apply the non-upper files in a new TarExtractor. 486 te := NewTarExtractor(unpackOptions) 487 for _, ph := range test.pseudoHeaders { 488 // Skip upper. 489 if ph.upper { 490 numUpper++ 491 continue 492 } 493 hdr, rdr := fromPseudoHdr(ph) 494 hdr.Name = filepath.Join(whiteoutDir, hdr.Name) 495 if err := te.UnpackEntry(dir, hdr, rdr); err != nil { 496 t.Errorf("UnpackEntry %s failed: %v", hdr.Name, err) 497 } 498 } 499 500 // Now we apply the upper files in another TarExtractor. 501 te = NewTarExtractor(unpackOptions) 502 for _, ph := range test.pseudoHeaders { 503 // Skip non-upper. 504 if !ph.upper { 505 continue 506 } 507 hdr, rdr := fromPseudoHdr(ph) 508 hdr.Name = filepath.Join(whiteoutDir, hdr.Name) 509 if err := te.UnpackEntry(dir, hdr, rdr); err != nil { 510 t.Errorf("UnpackEntry %s failed: %v", hdr.Name, err) 511 } 512 } 513 514 // And now apply a whiteout for the whiteoutRoot. 515 whHdr := &tar.Header{ 516 Name: filepath.Join(whiteoutDir, whOpaque), 517 Typeflag: tar.TypeReg, 518 } 519 if err := te.UnpackEntry(dir, whHdr, nil); err != nil { 520 t.Fatalf("unpack whiteout %s failed: %v", whiteoutRoot, err) 521 } 522 523 // Now we double-check it worked. If the file was in "upper" it 524 // should have survived. If it was in lower it shouldn't. We don't 525 // bother checking the contents here. 526 for _, ph := range test.pseudoHeaders { 527 // If there's an explicit whiteout in the headers we ignore it 528 // here, since it won't be on the filesystem. 529 if strings.HasPrefix(filepath.Base(ph.path), whPrefix) { 530 t.Logf("ignoring whiteout entry %q during post-check", ph.path) 531 continue 532 } 533 534 fullPath := filepath.Join(whiteoutRoot, ph.path) 535 _, err := te.fsEval.Lstat(fullPath) 536 if err != nil && !os.IsNotExist(errors.Cause(err)) { 537 t.Errorf("unexpected lstat error of %s: %v", ph.path, err) 538 } else if ph.upper && err != nil { 539 t.Errorf("expected upper %s to exist: got %v", ph.path, err) 540 } else if !ph.upper && err == nil { 541 if !test.ignoreExist { 542 t.Errorf("expected lower %s to not exist", ph.path) 543 } 544 } 545 } 546 547 // Make sure the whiteoutRoot still exists. 548 if fi, err := te.fsEval.Lstat(whiteoutRoot); err != nil { 549 if os.IsNotExist(errors.Cause(err)) { 550 t.Errorf("expected whiteout root to still exist: %v", err) 551 } else { 552 t.Errorf("unexpected error in lstat of whiteout root: %v", err) 553 } 554 } else if !fi.IsDir() { 555 t.Errorf("expected whiteout root to still be a directory") 556 } 557 558 // Check that the directory is empty if there's no uppers. 559 if numUpper == 0 { 560 if fd, err := os.Open(whiteoutRoot); err != nil { 561 t.Errorf("unexpected error opening whiteoutRoot: %v", err) 562 } else if names, err := fd.Readdirnames(-1); err != nil { 563 t.Errorf("unexpected error reading dirnames: %v", err) 564 } else if len(names) != 0 { 565 t.Errorf("expected empty opaque'd dir: got %v", names) 566 } 567 } 568 }) 569 } 570 } 571 572 // TestUnpackHardlink makes sure that hardlinks are correctly unpacked in all 573 // cases. In particular when it comes to hardlinks to symlinks. 574 func TestUnpackHardlink(t *testing.T) { 575 // Create the files we're going to play with. 576 dir, err := ioutil.TempDir("", "umoci-TestUnpackHardlink") 577 if err != nil { 578 t.Fatal(err) 579 } 580 defer os.RemoveAll(dir) 581 582 var ( 583 hdr *tar.Header 584 585 // On MacOS, this might not work. 586 hardlinkToSymlinkSupported = true 587 588 ctrValue = []byte("some content we won't check") 589 regFile = "regular" 590 symFile = "link" 591 hardFileA = "hard link" 592 hardFileB = "hard link to symlink" 593 ) 594 595 te := NewTarExtractor(UnpackOptions{}) 596 597 // Regular file. 598 hdr = &tar.Header{ 599 Name: regFile, 600 Uid: os.Getuid(), 601 Gid: os.Getgid(), 602 Mode: 0644, 603 Size: int64(len(ctrValue)), 604 Typeflag: tar.TypeReg, 605 ModTime: time.Now(), 606 AccessTime: time.Now(), 607 ChangeTime: time.Now(), 608 } 609 if err := te.UnpackEntry(dir, hdr, bytes.NewBuffer(ctrValue)); err != nil { 610 t.Fatalf("regular: unexpected UnpackEntry error: %s", err) 611 } 612 613 // Hardlink to regFile. 614 hdr = &tar.Header{ 615 Name: hardFileA, 616 Typeflag: tar.TypeLink, 617 Linkname: filepath.Join("/", regFile), 618 // These should **not** be applied. 619 Uid: os.Getuid() + 1337, 620 Gid: os.Getgid() + 2020, 621 } 622 if err := te.UnpackEntry(dir, hdr, nil); err != nil { 623 t.Fatalf("hardlinkA: unexpected UnpackEntry error: %s", err) 624 } 625 626 // Symlink to regFile. 627 hdr = &tar.Header{ 628 Name: symFile, 629 Uid: os.Getuid(), 630 Gid: os.Getgid(), 631 Typeflag: tar.TypeSymlink, 632 Linkname: filepath.Join("../../../", regFile), 633 } 634 if err := te.UnpackEntry(dir, hdr, nil); err != nil { 635 t.Fatalf("symlink: unexpected UnpackEntry error: %s", err) 636 } 637 638 // Hardlink to symlink. 639 hdr = &tar.Header{ 640 Name: hardFileB, 641 Typeflag: tar.TypeLink, 642 Linkname: filepath.Join("../../../", symFile), 643 // These should **really not** be applied. 644 Uid: os.Getuid() + 1337, 645 Gid: os.Getgid() + 2020, 646 } 647 if err := te.UnpackEntry(dir, hdr, nil); err != nil { 648 // On Travis' setup, hardlinks to symlinks are not permitted under 649 // MacOS. That's fine. 650 if runtime.GOOS == "darwin" && errors.Is(err, unix.ENOTSUP) { 651 hardlinkToSymlinkSupported = false 652 t.Logf("hardlinks to symlinks unsupported -- skipping that part of the test") 653 } else { 654 t.Fatalf("hardlinkB: unexpected UnpackEntry error: %s", err) 655 } 656 } 657 658 // Make sure that the contents are as expected. 659 ctrValueGot, err := ioutil.ReadFile(filepath.Join(dir, regFile)) 660 if err != nil { 661 t.Fatalf("regular file was not created: %s", err) 662 } 663 if !bytes.Equal(ctrValueGot, ctrValue) { 664 t.Fatalf("regular file did not have expected values: expected=%s got=%s", ctrValue, ctrValueGot) 665 } 666 667 // Now we have to check the inode numbers. 668 var regFi, symFi, hardAFi unix.Stat_t 669 670 if err := unix.Lstat(filepath.Join(dir, regFile), ®Fi); err != nil { 671 t.Fatalf("could not stat regular file: %s", err) 672 } 673 if err := unix.Lstat(filepath.Join(dir, symFile), &symFi); err != nil { 674 t.Fatalf("could not stat symlink: %s", err) 675 } 676 if err := unix.Lstat(filepath.Join(dir, hardFileA), &hardAFi); err != nil { 677 t.Fatalf("could not stat hardlinkA: %s", err) 678 } 679 680 if regFi.Ino == symFi.Ino { 681 t.Errorf("regular and symlink have the same inode! ino=%d", regFi.Ino) 682 } 683 if hardAFi.Ino != regFi.Ino { 684 t.Errorf("hardlink to regular has a different inode: reg=%d hard=%d", regFi.Ino, hardAFi.Ino) 685 } 686 687 if hardlinkToSymlinkSupported { 688 var hardBFi unix.Stat_t 689 690 if err := unix.Lstat(filepath.Join(dir, hardFileB), &hardBFi); err != nil { 691 t.Fatalf("could not stat hardlinkB: %s", err) 692 } 693 694 // Check inode numbers of hardlink-to-symlink. 695 if hardAFi.Ino == hardBFi.Ino { 696 t.Errorf("both hardlinks have the same inode! ino=%d", hardAFi.Ino) 697 } 698 if hardBFi.Ino != symFi.Ino { 699 t.Errorf("hardlink to symlink has a different inode: sym=%d hard=%d", symFi.Ino, hardBFi.Ino) 700 } 701 702 // Double-check readlink. 703 linknameA, err := os.Readlink(filepath.Join(dir, symFile)) 704 if err != nil { 705 t.Errorf("unexpected error reading symlink: %s", err) 706 } 707 linknameB, err := os.Readlink(filepath.Join(dir, hardFileB)) 708 if err != nil { 709 t.Errorf("unexpected error reading hardlink to symlink: %s", err) 710 } 711 if linknameA != linknameB { 712 t.Errorf("hardlink to symlink doesn't match linkname: link=%s hard=%s", linknameA, linknameB) 713 } 714 } 715 716 // Make sure that uid and gid don't apply to hardlinks. 717 if int(regFi.Uid) != os.Getuid() { 718 t.Errorf("regular file: uid was changed by hardlink unpack: expected=%d got=%d", os.Getuid(), regFi.Uid) 719 } 720 if int(regFi.Gid) != os.Getgid() { 721 t.Errorf("regular file: gid was changed by hardlink unpack: expected=%d got=%d", os.Getgid(), regFi.Gid) 722 } 723 if int(symFi.Uid) != os.Getuid() { 724 t.Errorf("symlink: uid was changed by hardlink unpack: expected=%d got=%d", os.Getuid(), symFi.Uid) 725 } 726 if int(symFi.Gid) != os.Getgid() { 727 t.Errorf("symlink: gid was changed by hardlink unpack: expected=%d got=%d", os.Getgid(), symFi.Gid) 728 } 729 } 730 731 // TestUnpackEntryMap checks that the mapOptions handling works. 732 func TestUnpackEntryMap(t *testing.T) { 733 if os.Geteuid() != 0 { 734 t.Skip("mapOptions tests only work with root privileges") 735 } 736 737 for _, test := range []struct { 738 name string 739 uidMap rspec.LinuxIDMapping 740 gidMap rspec.LinuxIDMapping 741 }{ 742 {"IdentityRoot", 743 rspec.LinuxIDMapping{HostID: 0, ContainerID: 0, Size: 100}, 744 rspec.LinuxIDMapping{HostID: 0, ContainerID: 0, Size: 100}}, 745 {"MapSelfToRoot", 746 rspec.LinuxIDMapping{HostID: uint32(os.Getuid()), ContainerID: 0, Size: 100}, 747 rspec.LinuxIDMapping{HostID: uint32(os.Getgid()), ContainerID: 0, Size: 100}}, 748 {"MapOtherToRoot", 749 rspec.LinuxIDMapping{HostID: uint32(os.Getuid() + 100), ContainerID: 0, Size: 100}, 750 rspec.LinuxIDMapping{HostID: uint32(os.Getgid() + 200), ContainerID: 0, Size: 100}}, 751 } { 752 t.Run(test.name, func(t *testing.T) { 753 // Create the files we're going to play with. 754 dir, err := ioutil.TempDir("", "umoci-TestUnpackEntryMap") 755 if err != nil { 756 t.Fatal(err) 757 } 758 defer os.RemoveAll(dir) 759 760 var ( 761 hdrUID, hdrGID, uUID, uGID int 762 hdr *tar.Header 763 fi unix.Stat_t 764 765 ctrValue = []byte("some content we won't check") 766 regFile = "regular" 767 symFile = "link" 768 regDir = " a directory" 769 symDir = "link-dir" 770 ) 771 772 te := NewTarExtractor(UnpackOptions{MapOptions: MapOptions{ 773 UIDMappings: []rspec.LinuxIDMapping{test.uidMap}, 774 GIDMappings: []rspec.LinuxIDMapping{test.gidMap}, 775 }}) 776 777 // Regular file. 778 hdrUID, hdrGID = 0, 0 779 hdr = &tar.Header{ 780 Name: regFile, 781 Uid: hdrUID, 782 Gid: hdrGID, 783 Mode: 0644, 784 Size: int64(len(ctrValue)), 785 Typeflag: tar.TypeReg, 786 ModTime: time.Now(), 787 AccessTime: time.Now(), 788 ChangeTime: time.Now(), 789 } 790 if err := te.UnpackEntry(dir, hdr, bytes.NewBuffer(ctrValue)); err != nil { 791 t.Fatalf("regfile: unexpected UnpackEntry error: %s", err) 792 } 793 794 if err := unix.Lstat(filepath.Join(dir, hdr.Name), &fi); err != nil { 795 t.Errorf("failed to lstat %s: %s", hdr.Name, err) 796 } else { 797 uUID = int(fi.Uid) 798 uGID = int(fi.Gid) 799 if uUID != int(test.uidMap.HostID)+hdrUID { 800 t.Errorf("file %s has the wrong uid mapping: got=%d expected=%d", hdr.Name, uUID, int(test.uidMap.HostID)+hdrUID) 801 } 802 if uGID != int(test.gidMap.HostID)+hdrGID { 803 t.Errorf("file %s has the wrong gid mapping: got=%d expected=%d", hdr.Name, uGID, int(test.gidMap.HostID)+hdrGID) 804 } 805 } 806 807 // Regular directory. 808 hdrUID, hdrGID = 13, 42 809 hdr = &tar.Header{ 810 Name: regDir, 811 Uid: hdrUID, 812 Gid: hdrGID, 813 Mode: 0755, 814 Typeflag: tar.TypeDir, 815 ModTime: time.Now(), 816 AccessTime: time.Now(), 817 ChangeTime: time.Now(), 818 } 819 if err := te.UnpackEntry(dir, hdr, bytes.NewBuffer(ctrValue)); err != nil { 820 t.Fatalf("regdir: unexpected UnpackEntry error: %s", err) 821 } 822 823 if err := unix.Lstat(filepath.Join(dir, hdr.Name), &fi); err != nil { 824 t.Errorf("failed to lstat %s: %s", hdr.Name, err) 825 } else { 826 uUID = int(fi.Uid) 827 uGID = int(fi.Gid) 828 if uUID != int(test.uidMap.HostID)+hdrUID { 829 t.Errorf("file %s has the wrong uid mapping: got=%d expected=%d", hdr.Name, uUID, int(test.uidMap.HostID)+hdrUID) 830 } 831 if uGID != int(test.gidMap.HostID)+hdrGID { 832 t.Errorf("file %s has the wrong gid mapping: got=%d expected=%d", hdr.Name, uGID, int(test.gidMap.HostID)+hdrGID) 833 } 834 } 835 836 // Symlink to file. 837 hdrUID, hdrGID = 23, 22 838 hdr = &tar.Header{ 839 Name: symFile, 840 Uid: hdrUID, 841 Gid: hdrGID, 842 Typeflag: tar.TypeSymlink, 843 Linkname: regFile, 844 ModTime: time.Now(), 845 AccessTime: time.Now(), 846 ChangeTime: time.Now(), 847 } 848 if err := te.UnpackEntry(dir, hdr, bytes.NewBuffer(ctrValue)); err != nil { 849 t.Fatalf("regdir: unexpected UnpackEntry error: %s", err) 850 } 851 852 if err := unix.Lstat(filepath.Join(dir, hdr.Name), &fi); err != nil { 853 t.Errorf("failed to lstat %s: %s", hdr.Name, err) 854 } else { 855 uUID = int(fi.Uid) 856 uGID = int(fi.Gid) 857 if uUID != int(test.uidMap.HostID)+hdrUID { 858 t.Errorf("file %s has the wrong uid mapping: got=%d expected=%d", hdr.Name, uUID, int(test.uidMap.HostID)+hdrUID) 859 } 860 if uGID != int(test.gidMap.HostID)+hdrGID { 861 t.Errorf("file %s has the wrong gid mapping: got=%d expected=%d", hdr.Name, uGID, int(test.gidMap.HostID)+hdrGID) 862 } 863 } 864 865 // Symlink to director. 866 hdrUID, hdrGID = 99, 88 867 hdr = &tar.Header{ 868 Name: symDir, 869 Uid: hdrUID, 870 Gid: hdrGID, 871 Typeflag: tar.TypeSymlink, 872 Linkname: regDir, 873 ModTime: time.Now(), 874 AccessTime: time.Now(), 875 ChangeTime: time.Now(), 876 } 877 if err := te.UnpackEntry(dir, hdr, bytes.NewBuffer(ctrValue)); err != nil { 878 t.Fatalf("regdir: unexpected UnpackEntry error: %s", err) 879 } 880 881 if err := unix.Lstat(filepath.Join(dir, hdr.Name), &fi); err != nil { 882 t.Errorf("failed to lstat %s: %s", hdr.Name, err) 883 } else { 884 uUID = int(fi.Uid) 885 uGID = int(fi.Gid) 886 if uUID != int(test.uidMap.HostID)+hdrUID { 887 t.Errorf("file %s has the wrong uid mapping: got=%d expected=%d", hdr.Name, uUID, int(test.uidMap.HostID)+hdrUID) 888 } 889 if uGID != int(test.gidMap.HostID)+hdrGID { 890 t.Errorf("file %s has the wrong gid mapping: got=%d expected=%d", hdr.Name, uGID, int(test.gidMap.HostID)+hdrGID) 891 } 892 } 893 }) 894 } 895 } 896 897 func TestIsDirlink(t *testing.T) { 898 dir, err := ioutil.TempDir("", "umoci-TestDirLink") 899 if err != nil { 900 t.Fatal(err) 901 } 902 defer os.RemoveAll(dir) 903 904 if err := os.Mkdir(filepath.Join(dir, "test_dir"), 0755); err != nil { 905 t.Fatal(err) 906 } 907 if file, err := os.Create(filepath.Join(dir, "test_file")); err != nil { 908 t.Fatal(err) 909 } else { 910 file.Close() 911 } 912 if err := os.Symlink("test_dir", filepath.Join(dir, "link")); err != nil { 913 t.Fatal(err) 914 } 915 916 te := NewTarExtractor(UnpackOptions{}) 917 // Basic symlink usage. 918 if dirlink, err := te.isDirlink(dir, filepath.Join(dir, "link")); err != nil { 919 t.Errorf("symlink failed: %v", err) 920 } else if !dirlink { 921 t.Errorf("dirlink test failed") 922 } 923 924 // "Read" a non-existent link. 925 if _, err := te.isDirlink(dir, filepath.Join(dir, "doesnt-exist")); err == nil { 926 t.Errorf("read non-existent dirlink") 927 } 928 // "Read" a directory. 929 if _, err := te.isDirlink(dir, filepath.Join(dir, "test_dir")); err == nil { 930 t.Errorf("read non-link dirlink") 931 } 932 // "Read" a file. 933 if _, err := te.isDirlink(dir, filepath.Join(dir, "test_file")); err == nil { 934 t.Errorf("read non-link dirlink") 935 } 936 937 // Break the symlink. 938 if err := os.Remove(filepath.Join(dir, "test_dir")); err != nil { 939 t.Fatal(err) 940 } 941 if dirlink, err := te.isDirlink(dir, filepath.Join(dir, "link")); err != nil { 942 t.Errorf("broken symlink failed: %v", err) 943 } else if dirlink { 944 t.Errorf("broken dirlink test failed") 945 } 946 947 // Point the symlink to a file. 948 if err := os.Remove(filepath.Join(dir, "link")); err != nil { 949 t.Fatal(err) 950 } 951 if err := os.Symlink("test_file", filepath.Join(dir, "link")); err != nil { 952 t.Fatal(err) 953 } 954 if dirlink, err := te.isDirlink(dir, filepath.Join(dir, "link")); err != nil { 955 t.Errorf("file symlink failed: %v", err) 956 } else if dirlink { 957 t.Errorf("file dirlink test failed") 958 } 959 }