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