github.com/containerd/Containerd@v1.4.13/archive/tar.go (about) 1 /* 2 Copyright The containerd Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package archive 18 19 import ( 20 "archive/tar" 21 "context" 22 "io" 23 "os" 24 "path/filepath" 25 "runtime" 26 "strings" 27 "sync" 28 "syscall" 29 "time" 30 31 "github.com/containerd/containerd/log" 32 "github.com/containerd/continuity/fs" 33 "github.com/pkg/errors" 34 ) 35 36 var bufPool = &sync.Pool{ 37 New: func() interface{} { 38 buffer := make([]byte, 32*1024) 39 return &buffer 40 }, 41 } 42 43 var errInvalidArchive = errors.New("invalid archive") 44 45 // Diff returns a tar stream of the computed filesystem 46 // difference between the provided directories. 47 // 48 // Produces a tar using OCI style file markers for deletions. Deleted 49 // files will be prepended with the prefix ".wh.". This style is 50 // based off AUFS whiteouts. 51 // See https://github.com/opencontainers/image-spec/blob/master/layer.md 52 func Diff(ctx context.Context, a, b string) io.ReadCloser { 53 r, w := io.Pipe() 54 55 go func() { 56 err := WriteDiff(ctx, w, a, b) 57 if err = w.CloseWithError(err); err != nil { 58 log.G(ctx).WithError(err).Debugf("closing tar pipe failed") 59 } 60 }() 61 62 return r 63 } 64 65 // WriteDiff writes a tar stream of the computed difference between the 66 // provided directories. 67 // 68 // Produces a tar using OCI style file markers for deletions. Deleted 69 // files will be prepended with the prefix ".wh.". This style is 70 // based off AUFS whiteouts. 71 // See https://github.com/opencontainers/image-spec/blob/master/layer.md 72 func WriteDiff(ctx context.Context, w io.Writer, a, b string) error { 73 cw := newChangeWriter(w, b) 74 err := fs.Changes(ctx, a, b, cw.HandleChange) 75 if err != nil { 76 return errors.Wrap(err, "failed to create diff tar stream") 77 } 78 return cw.Close() 79 } 80 81 const ( 82 // whiteoutPrefix prefix means file is a whiteout. If this is followed by a 83 // filename this means that file has been removed from the base layer. 84 // See https://github.com/opencontainers/image-spec/blob/master/layer.md#whiteouts 85 whiteoutPrefix = ".wh." 86 87 // whiteoutMetaPrefix prefix means whiteout has a special meaning and is not 88 // for removing an actual file. Normally these files are excluded from exported 89 // archives. 90 whiteoutMetaPrefix = whiteoutPrefix + whiteoutPrefix 91 92 // whiteoutOpaqueDir file means directory has been made opaque - meaning 93 // readdir calls to this directory do not follow to lower layers. 94 whiteoutOpaqueDir = whiteoutMetaPrefix + ".opq" 95 96 paxSchilyXattr = "SCHILY.xattr." 97 ) 98 99 // Apply applies a tar stream of an OCI style diff tar. 100 // See https://github.com/opencontainers/image-spec/blob/master/layer.md#applying-changesets 101 func Apply(ctx context.Context, root string, r io.Reader, opts ...ApplyOpt) (int64, error) { 102 root = filepath.Clean(root) 103 104 var options ApplyOptions 105 for _, opt := range opts { 106 if err := opt(&options); err != nil { 107 return 0, errors.Wrap(err, "failed to apply option") 108 } 109 } 110 if options.Filter == nil { 111 options.Filter = all 112 } 113 if options.applyFunc == nil { 114 options.applyFunc = applyNaive 115 } 116 117 return options.applyFunc(ctx, root, tar.NewReader(r), options) 118 } 119 120 // applyNaive applies a tar stream of an OCI style diff tar to a directory 121 // applying each file as either a whole file or whiteout. 122 // See https://github.com/opencontainers/image-spec/blob/master/layer.md#applying-changesets 123 func applyNaive(ctx context.Context, root string, tr *tar.Reader, options ApplyOptions) (size int64, err error) { 124 var ( 125 dirs []*tar.Header 126 127 // Used for handling opaque directory markers which 128 // may occur out of order 129 unpackedPaths = make(map[string]struct{}) 130 131 convertWhiteout = options.ConvertWhiteout 132 ) 133 134 if convertWhiteout == nil { 135 // handle whiteouts by removing the target files 136 convertWhiteout = func(hdr *tar.Header, path string) (bool, error) { 137 base := filepath.Base(path) 138 dir := filepath.Dir(path) 139 if base == whiteoutOpaqueDir { 140 _, err := os.Lstat(dir) 141 if err != nil { 142 return false, err 143 } 144 err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { 145 if err != nil { 146 if os.IsNotExist(err) { 147 err = nil // parent was deleted 148 } 149 return err 150 } 151 if path == dir { 152 return nil 153 } 154 if _, exists := unpackedPaths[path]; !exists { 155 err := os.RemoveAll(path) 156 return err 157 } 158 return nil 159 }) 160 return false, err 161 } 162 163 if strings.HasPrefix(base, whiteoutPrefix) { 164 originalBase := base[len(whiteoutPrefix):] 165 originalPath := filepath.Join(dir, originalBase) 166 167 return false, os.RemoveAll(originalPath) 168 } 169 170 return true, nil 171 } 172 } 173 174 // Iterate through the files in the archive. 175 for { 176 select { 177 case <-ctx.Done(): 178 return 0, ctx.Err() 179 default: 180 } 181 182 hdr, err := tr.Next() 183 if err == io.EOF { 184 // end of tar archive 185 break 186 } 187 if err != nil { 188 return 0, err 189 } 190 191 size += hdr.Size 192 193 // Normalize name, for safety and for a simple is-root check 194 hdr.Name = filepath.Clean(hdr.Name) 195 196 accept, err := options.Filter(hdr) 197 if err != nil { 198 return 0, err 199 } 200 if !accept { 201 continue 202 } 203 204 if skipFile(hdr) { 205 log.G(ctx).Warnf("file %q ignored: archive may not be supported on system", hdr.Name) 206 continue 207 } 208 209 // Split name and resolve symlinks for root directory. 210 ppath, base := filepath.Split(hdr.Name) 211 ppath, err = fs.RootPath(root, ppath) 212 if err != nil { 213 return 0, errors.Wrap(err, "failed to get root path") 214 } 215 216 // Join to root before joining to parent path to ensure relative links are 217 // already resolved based on the root before adding to parent. 218 path := filepath.Join(ppath, filepath.Join("/", base)) 219 if path == root { 220 log.G(ctx).Debugf("file %q ignored: resolved to root", hdr.Name) 221 continue 222 } 223 224 // If file is not directly under root, ensure parent directory 225 // exists or is created. 226 if ppath != root { 227 parentPath := ppath 228 if base == "" { 229 parentPath = filepath.Dir(path) 230 } 231 if err := mkparent(ctx, parentPath, root, options.Parents); err != nil { 232 return 0, err 233 } 234 } 235 236 // Naive whiteout convert function which handles whiteout files by 237 // removing the target files. 238 if err := validateWhiteout(path); err != nil { 239 return 0, err 240 } 241 writeFile, err := convertWhiteout(hdr, path) 242 if err != nil { 243 return 0, errors.Wrapf(err, "failed to convert whiteout file %q", hdr.Name) 244 } 245 if !writeFile { 246 continue 247 } 248 // If path exits we almost always just want to remove and replace it. 249 // The only exception is when it is a directory *and* the file from 250 // the layer is also a directory. Then we want to merge them (i.e. 251 // just apply the metadata from the layer). 252 if fi, err := os.Lstat(path); err == nil { 253 if !(fi.IsDir() && hdr.Typeflag == tar.TypeDir) { 254 if err := os.RemoveAll(path); err != nil { 255 return 0, err 256 } 257 } 258 } 259 260 srcData := io.Reader(tr) 261 srcHdr := hdr 262 263 if err := createTarFile(ctx, path, root, srcHdr, srcData); err != nil { 264 return 0, err 265 } 266 267 // Directory mtimes must be handled at the end to avoid further 268 // file creation in them to modify the directory mtime 269 if hdr.Typeflag == tar.TypeDir { 270 dirs = append(dirs, hdr) 271 } 272 unpackedPaths[path] = struct{}{} 273 } 274 275 for _, hdr := range dirs { 276 path, err := fs.RootPath(root, hdr.Name) 277 if err != nil { 278 return 0, err 279 } 280 if err := chtimes(path, boundTime(latestTime(hdr.AccessTime, hdr.ModTime)), boundTime(hdr.ModTime)); err != nil { 281 return 0, err 282 } 283 } 284 285 return size, nil 286 } 287 288 func createTarFile(ctx context.Context, path, extractDir string, hdr *tar.Header, reader io.Reader) error { 289 // hdr.Mode is in linux format, which we can use for syscalls, 290 // but for os.Foo() calls we need the mode converted to os.FileMode, 291 // so use hdrInfo.Mode() (they differ for e.g. setuid bits) 292 hdrInfo := hdr.FileInfo() 293 294 switch hdr.Typeflag { 295 case tar.TypeDir: 296 // Create directory unless it exists as a directory already. 297 // In that case we just want to merge the two 298 if fi, err := os.Lstat(path); !(err == nil && fi.IsDir()) { 299 if err := mkdir(path, hdrInfo.Mode()); err != nil { 300 return err 301 } 302 } 303 304 case tar.TypeReg, tar.TypeRegA: 305 file, err := openFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, hdrInfo.Mode()) 306 if err != nil { 307 return err 308 } 309 310 _, err = copyBuffered(ctx, file, reader) 311 if err1 := file.Close(); err == nil { 312 err = err1 313 } 314 if err != nil { 315 return err 316 } 317 318 case tar.TypeBlock, tar.TypeChar: 319 // Handle this is an OS-specific way 320 if err := handleTarTypeBlockCharFifo(hdr, path); err != nil { 321 return err 322 } 323 324 case tar.TypeFifo: 325 // Handle this is an OS-specific way 326 if err := handleTarTypeBlockCharFifo(hdr, path); err != nil { 327 return err 328 } 329 330 case tar.TypeLink: 331 targetPath, err := hardlinkRootPath(extractDir, hdr.Linkname) 332 if err != nil { 333 return err 334 } 335 336 if err := os.Link(targetPath, path); err != nil { 337 return err 338 } 339 340 case tar.TypeSymlink: 341 if err := os.Symlink(hdr.Linkname, path); err != nil { 342 return err 343 } 344 345 case tar.TypeXGlobalHeader: 346 log.G(ctx).Debug("PAX Global Extended Headers found and ignored") 347 return nil 348 349 default: 350 return errors.Errorf("unhandled tar header type %d\n", hdr.Typeflag) 351 } 352 353 // Lchown is not supported on Windows. 354 if runtime.GOOS != "windows" { 355 if err := os.Lchown(path, hdr.Uid, hdr.Gid); err != nil { 356 return err 357 } 358 } 359 360 for key, value := range hdr.PAXRecords { 361 if strings.HasPrefix(key, paxSchilyXattr) { 362 key = key[len(paxSchilyXattr):] 363 if err := setxattr(path, key, value); err != nil { 364 if errors.Is(err, syscall.ENOTSUP) { 365 log.G(ctx).WithError(err).Warnf("ignored xattr %s in archive", key) 366 continue 367 } 368 return err 369 } 370 } 371 } 372 373 // There is no LChmod, so ignore mode for symlink. Also, this 374 // must happen after chown, as that can modify the file mode 375 if err := handleLChmod(hdr, path, hdrInfo); err != nil { 376 return err 377 } 378 379 return chtimes(path, boundTime(latestTime(hdr.AccessTime, hdr.ModTime)), boundTime(hdr.ModTime)) 380 } 381 382 func mkparent(ctx context.Context, path, root string, parents []string) error { 383 if dir, err := os.Lstat(path); err == nil { 384 if dir.IsDir() { 385 return nil 386 } 387 return &os.PathError{ 388 Op: "mkparent", 389 Path: path, 390 Err: syscall.ENOTDIR, 391 } 392 } else if !os.IsNotExist(err) { 393 return err 394 } 395 396 i := len(path) 397 for i > len(root) && !os.IsPathSeparator(path[i-1]) { 398 i-- 399 } 400 401 if i > len(root)+1 { 402 if err := mkparent(ctx, path[:i-1], root, parents); err != nil { 403 return err 404 } 405 } 406 407 if err := mkdir(path, 0755); err != nil { 408 // Check that still doesn't exist 409 dir, err1 := os.Lstat(path) 410 if err1 == nil && dir.IsDir() { 411 return nil 412 } 413 return err 414 } 415 416 for _, p := range parents { 417 ppath, err := fs.RootPath(p, path[len(root):]) 418 if err != nil { 419 return err 420 } 421 422 dir, err := os.Lstat(ppath) 423 if err == nil { 424 if !dir.IsDir() { 425 // Replaced, do not copy attributes 426 break 427 } 428 if err := copyDirInfo(dir, path); err != nil { 429 return err 430 } 431 return copyUpXAttrs(path, ppath) 432 } else if !os.IsNotExist(err) { 433 return err 434 } 435 } 436 437 log.G(ctx).Debugf("parent directory %q not found: default permissions(0755) used", path) 438 439 return nil 440 } 441 442 type changeWriter struct { 443 tw *tar.Writer 444 source string 445 whiteoutT time.Time 446 inodeSrc map[uint64]string 447 inodeRefs map[uint64][]string 448 addedDirs map[string]struct{} 449 } 450 451 func newChangeWriter(w io.Writer, source string) *changeWriter { 452 return &changeWriter{ 453 tw: tar.NewWriter(w), 454 source: source, 455 whiteoutT: time.Now(), 456 inodeSrc: map[uint64]string{}, 457 inodeRefs: map[uint64][]string{}, 458 addedDirs: map[string]struct{}{}, 459 } 460 } 461 462 func (cw *changeWriter) HandleChange(k fs.ChangeKind, p string, f os.FileInfo, err error) error { 463 if err != nil { 464 return err 465 } 466 if k == fs.ChangeKindDelete { 467 whiteOutDir := filepath.Dir(p) 468 whiteOutBase := filepath.Base(p) 469 whiteOut := filepath.Join(whiteOutDir, whiteoutPrefix+whiteOutBase) 470 hdr := &tar.Header{ 471 Typeflag: tar.TypeReg, 472 Name: whiteOut[1:], 473 Size: 0, 474 ModTime: cw.whiteoutT, 475 AccessTime: cw.whiteoutT, 476 ChangeTime: cw.whiteoutT, 477 } 478 if err := cw.includeParents(hdr); err != nil { 479 return err 480 } 481 if err := cw.tw.WriteHeader(hdr); err != nil { 482 return errors.Wrap(err, "failed to write whiteout header") 483 } 484 } else { 485 var ( 486 link string 487 err error 488 source = filepath.Join(cw.source, p) 489 ) 490 491 switch { 492 case f.Mode()&os.ModeSocket != 0: 493 return nil // ignore sockets 494 case f.Mode()&os.ModeSymlink != 0: 495 if link, err = os.Readlink(source); err != nil { 496 return err 497 } 498 } 499 500 hdr, err := tar.FileInfoHeader(f, link) 501 if err != nil { 502 return err 503 } 504 505 hdr.Mode = int64(chmodTarEntry(os.FileMode(hdr.Mode))) 506 507 // truncate timestamp for compatibility. without PAX stdlib rounds timestamps instead 508 hdr.Format = tar.FormatPAX 509 hdr.ModTime = hdr.ModTime.Truncate(time.Second) 510 hdr.AccessTime = time.Time{} 511 hdr.ChangeTime = time.Time{} 512 513 name := p 514 if strings.HasPrefix(name, string(filepath.Separator)) { 515 name, err = filepath.Rel(string(filepath.Separator), name) 516 if err != nil { 517 return errors.Wrap(err, "failed to make path relative") 518 } 519 } 520 name, err = tarName(name) 521 if err != nil { 522 return errors.Wrap(err, "cannot canonicalize path") 523 } 524 // suffix with '/' for directories 525 if f.IsDir() && !strings.HasSuffix(name, "/") { 526 name += "/" 527 } 528 hdr.Name = name 529 530 if err := setHeaderForSpecialDevice(hdr, name, f); err != nil { 531 return errors.Wrap(err, "failed to set device headers") 532 } 533 534 // additionalLinks stores file names which must be linked to 535 // this file when this file is added 536 var additionalLinks []string 537 inode, isHardlink := fs.GetLinkInfo(f) 538 if isHardlink { 539 // If the inode has a source, always link to it 540 if source, ok := cw.inodeSrc[inode]; ok { 541 hdr.Typeflag = tar.TypeLink 542 hdr.Linkname = source 543 hdr.Size = 0 544 } else { 545 if k == fs.ChangeKindUnmodified { 546 cw.inodeRefs[inode] = append(cw.inodeRefs[inode], name) 547 return nil 548 } 549 cw.inodeSrc[inode] = name 550 additionalLinks = cw.inodeRefs[inode] 551 delete(cw.inodeRefs, inode) 552 } 553 } else if k == fs.ChangeKindUnmodified { 554 // Nothing to write to diff 555 return nil 556 } 557 558 if capability, err := getxattr(source, "security.capability"); err != nil { 559 return errors.Wrap(err, "failed to get capabilities xattr") 560 } else if capability != nil { 561 if hdr.PAXRecords == nil { 562 hdr.PAXRecords = map[string]string{} 563 } 564 hdr.PAXRecords[paxSchilyXattr+"security.capability"] = string(capability) 565 } 566 567 if err := cw.includeParents(hdr); err != nil { 568 return err 569 } 570 if err := cw.tw.WriteHeader(hdr); err != nil { 571 return errors.Wrap(err, "failed to write file header") 572 } 573 574 if hdr.Typeflag == tar.TypeReg && hdr.Size > 0 { 575 file, err := open(source) 576 if err != nil { 577 return errors.Wrapf(err, "failed to open path: %v", source) 578 } 579 defer file.Close() 580 581 n, err := copyBuffered(context.TODO(), cw.tw, file) 582 if err != nil { 583 return errors.Wrap(err, "failed to copy") 584 } 585 if n != hdr.Size { 586 return errors.New("short write copying file") 587 } 588 } 589 590 if additionalLinks != nil { 591 source = hdr.Name 592 for _, extra := range additionalLinks { 593 hdr.Name = extra 594 hdr.Typeflag = tar.TypeLink 595 hdr.Linkname = source 596 hdr.Size = 0 597 598 if err := cw.includeParents(hdr); err != nil { 599 return err 600 } 601 if err := cw.tw.WriteHeader(hdr); err != nil { 602 return errors.Wrap(err, "failed to write file header") 603 } 604 } 605 } 606 } 607 return nil 608 } 609 610 func (cw *changeWriter) Close() error { 611 if err := cw.tw.Close(); err != nil { 612 return errors.Wrap(err, "failed to close tar writer") 613 } 614 return nil 615 } 616 617 func (cw *changeWriter) includeParents(hdr *tar.Header) error { 618 if cw.addedDirs == nil { 619 return nil 620 } 621 name := strings.TrimRight(hdr.Name, "/") 622 fname := filepath.Join(cw.source, name) 623 parent := filepath.Dir(name) 624 pname := filepath.Join(cw.source, parent) 625 626 // Do not include root directory as parent 627 if fname != cw.source && pname != cw.source { 628 _, ok := cw.addedDirs[parent] 629 if !ok { 630 cw.addedDirs[parent] = struct{}{} 631 fi, err := os.Stat(pname) 632 if err != nil { 633 return err 634 } 635 if err := cw.HandleChange(fs.ChangeKindModify, parent, fi, nil); err != nil { 636 return err 637 } 638 } 639 } 640 if hdr.Typeflag == tar.TypeDir { 641 cw.addedDirs[name] = struct{}{} 642 } 643 return nil 644 } 645 646 func copyBuffered(ctx context.Context, dst io.Writer, src io.Reader) (written int64, err error) { 647 buf := bufPool.Get().(*[]byte) 648 defer bufPool.Put(buf) 649 650 for { 651 select { 652 case <-ctx.Done(): 653 err = ctx.Err() 654 return 655 default: 656 } 657 658 nr, er := src.Read(*buf) 659 if nr > 0 { 660 nw, ew := dst.Write((*buf)[0:nr]) 661 if nw > 0 { 662 written += int64(nw) 663 } 664 if ew != nil { 665 err = ew 666 break 667 } 668 if nr != nw { 669 err = io.ErrShortWrite 670 break 671 } 672 } 673 if er != nil { 674 if er != io.EOF { 675 err = er 676 } 677 break 678 } 679 } 680 return written, err 681 682 } 683 684 // hardlinkRootPath returns target linkname, evaluating and bounding any 685 // symlink to the parent directory. 686 // 687 // NOTE: Allow hardlink to the softlink, not the real one. For example, 688 // 689 // touch /tmp/zzz 690 // ln -s /tmp/zzz /tmp/xxx 691 // ln /tmp/xxx /tmp/yyy 692 // 693 // /tmp/yyy should be softlink which be same of /tmp/xxx, not /tmp/zzz. 694 func hardlinkRootPath(root, linkname string) (string, error) { 695 ppath, base := filepath.Split(linkname) 696 ppath, err := fs.RootPath(root, ppath) 697 if err != nil { 698 return "", err 699 } 700 701 targetPath := filepath.Join(ppath, base) 702 if !strings.HasPrefix(targetPath, root) { 703 targetPath = root 704 } 705 return targetPath, nil 706 } 707 708 func validateWhiteout(path string) error { 709 base := filepath.Base(path) 710 dir := filepath.Dir(path) 711 712 if base == whiteoutOpaqueDir { 713 return nil 714 } 715 716 if strings.HasPrefix(base, whiteoutPrefix) { 717 originalBase := base[len(whiteoutPrefix):] 718 originalPath := filepath.Join(dir, originalBase) 719 720 // Ensure originalPath is under dir 721 if dir[len(dir)-1] != filepath.Separator { 722 dir += string(filepath.Separator) 723 } 724 if !strings.HasPrefix(originalPath, dir) { 725 return errors.Wrapf(errInvalidArchive, "invalid whiteout name: %v", base) 726 } 727 } 728 return nil 729 }