From c195ebb3e2f316225905b94ff833eec78f9c836b Mon Sep 17 00:00:00 2001 From: Darren Stahl Date: Fri, 8 Dec 2017 15:16:24 -0800 Subject: [PATCH] Implement archive.Apply on Windows Signed-off-by: Darren Stahl --- archive/strconv.go | 52 ++++++ archive/tar.go | 203 +++++++++++----------- archive/tar_opts.go | 4 + archive/tar_opts_unix.go | 7 + archive/tar_opts_windows.go | 28 ++++ archive/tar_unix.go | 7 + archive/tar_windows.go | 323 ++++++++++++++++++++++++++++++++++++ 7 files changed, 528 insertions(+), 96 deletions(-) create mode 100644 archive/strconv.go create mode 100644 archive/tar_opts.go create mode 100644 archive/tar_opts_unix.go create mode 100644 archive/tar_opts_windows.go diff --git a/archive/strconv.go b/archive/strconv.go new file mode 100644 index 000000000..185ee5dd2 --- /dev/null +++ b/archive/strconv.go @@ -0,0 +1,52 @@ +// +build windows + +package archive + +import ( + "strconv" + "strings" + "time" + + "github.com/dmcgowan/go-tar" +) + +// Forked from https://github.com/golang/go/blob/master/src/archive/tar/strconv.go +// as archive/tar doesn't support CreationTime, but does handle PAX time parsing, +// and there's no need to re-invent the wheel. + +// parsePAXTime takes a string of the form %d.%d as described in the PAX +// specification. Note that this implementation allows for negative timestamps, +// which is allowed for by the PAX specification, but not always portable. +func parsePAXTime(s string) (time.Time, error) { + const maxNanoSecondDigits = 9 + + // Split string into seconds and sub-seconds parts. + ss, sn := s, "" + if pos := strings.IndexByte(s, '.'); pos >= 0 { + ss, sn = s[:pos], s[pos+1:] + } + + // Parse the seconds. + secs, err := strconv.ParseInt(ss, 10, 64) + if err != nil { + return time.Time{}, tar.ErrHeader + } + if len(sn) == 0 { + return time.Unix(secs, 0), nil // No sub-second values + } + + // Parse the nanoseconds. + if strings.Trim(sn, "0123456789") != "" { + return time.Time{}, tar.ErrHeader + } + if len(sn) < maxNanoSecondDigits { + sn += strings.Repeat("0", maxNanoSecondDigits-len(sn)) // Right pad + } else { + sn = sn[:maxNanoSecondDigits] // Right truncate + } + nsecs, _ := strconv.ParseInt(sn, 10, 64) // Must succeed + if len(ss) > 0 && ss[0] == '-' { + return time.Unix(secs, -1*int64(nsecs)), nil // Negative correction + } + return time.Unix(secs, int64(nsecs)), nil +} diff --git a/archive/tar.go b/archive/tar.go index c12d4f280..d7fb7b698 100644 --- a/archive/tar.go +++ b/archive/tar.go @@ -87,12 +87,23 @@ const ( // Apply applies a tar stream of an OCI style diff tar. // See https://github.com/opencontainers/image-spec/blob/master/layer.md#applying-changesets -func Apply(ctx context.Context, root string, r io.Reader) (int64, error) { +func Apply(ctx context.Context, root string, r io.Reader, opts ...ApplyOpt) (int64, error) { root = filepath.Clean(root) + var options ApplyOptions + for _, opt := range opts { + if err := opt(&options); err != nil { + return 0, errors.Wrap(err, "failed to apply option") + } + } + + return apply(ctx, root, tar.NewReader(r), options) +} + +// applyNaive applies a tar stream of an OCI style diff tar. +// See https://github.com/opencontainers/image-spec/blob/master/layer.md#applying-changesets +func applyNaive(ctx context.Context, root string, tr *tar.Reader, options ApplyOptions) (size int64, err error) { var ( - tr = tar.NewReader(r) - size int64 dirs []*tar.Header // Used for handling opaque directory markers which @@ -285,6 +296,99 @@ func Apply(ctx context.Context, root string, r io.Reader) (int64, error) { return size, nil } +func createTarFile(ctx context.Context, path, extractDir string, hdr *tar.Header, reader io.Reader) error { + // hdr.Mode is in linux format, which we can use for syscalls, + // but for os.Foo() calls we need the mode converted to os.FileMode, + // so use hdrInfo.Mode() (they differ for e.g. setuid bits) + hdrInfo := hdr.FileInfo() + + switch hdr.Typeflag { + case tar.TypeDir: + // Create directory unless it exists as a directory already. + // In that case we just want to merge the two + if fi, err := os.Lstat(path); !(err == nil && fi.IsDir()) { + if err := mkdir(path, hdrInfo.Mode()); err != nil { + return err + } + } + + case tar.TypeReg, tar.TypeRegA: + file, err := openFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, hdrInfo.Mode()) + if err != nil { + return err + } + + _, err = copyBuffered(ctx, file, reader) + if err1 := file.Close(); err == nil { + err = err1 + } + if err != nil { + return err + } + + case tar.TypeBlock, tar.TypeChar: + // Handle this is an OS-specific way + if err := handleTarTypeBlockCharFifo(hdr, path); err != nil { + return err + } + + case tar.TypeFifo: + // Handle this is an OS-specific way + if err := handleTarTypeBlockCharFifo(hdr, path); err != nil { + return err + } + + case tar.TypeLink: + targetPath, err := fs.RootPath(extractDir, hdr.Linkname) + if err != nil { + return err + } + if err := os.Link(targetPath, path); err != nil { + return err + } + + case tar.TypeSymlink: + if err := os.Symlink(hdr.Linkname, path); err != nil { + return err + } + + case tar.TypeXGlobalHeader: + log.G(ctx).Debug("PAX Global Extended Headers found and ignored") + return nil + + default: + return errors.Errorf("unhandled tar header type %d\n", hdr.Typeflag) + } + + // Lchown is not supported on Windows. + if runtime.GOOS != "windows" { + if err := os.Lchown(path, hdr.Uid, hdr.Gid); err != nil { + return err + } + } + + for key, value := range hdr.PAXRecords { + if strings.HasPrefix(key, paxSchilyXattr) { + key = key[len(paxSchilyXattr):] + if err := setxattr(path, key, value); err != nil { + if errors.Cause(err) == syscall.ENOTSUP { + log.G(ctx).WithError(err).Warnf("ignored xattr %s in archive", key) + continue + } + return err + } + } + } + + // There is no LChmod, so ignore mode for symlink. Also, this + // must happen after chown, as that can modify the file mode + if err := handleLChmod(hdr, path, hdrInfo); err != nil { + return err + } + + return chtimes(path, boundTime(latestTime(hdr.AccessTime, hdr.ModTime)), boundTime(hdr.ModTime)) +} + type changeWriter struct { tw *tar.Writer source string @@ -440,99 +544,6 @@ func (cw *changeWriter) Close() error { return nil } -func createTarFile(ctx context.Context, path, extractDir string, hdr *tar.Header, reader io.Reader) error { - // hdr.Mode is in linux format, which we can use for syscalls, - // but for os.Foo() calls we need the mode converted to os.FileMode, - // so use hdrInfo.Mode() (they differ for e.g. setuid bits) - hdrInfo := hdr.FileInfo() - - switch hdr.Typeflag { - case tar.TypeDir: - // Create directory unless it exists as a directory already. - // In that case we just want to merge the two - if fi, err := os.Lstat(path); !(err == nil && fi.IsDir()) { - if err := mkdir(path, hdrInfo.Mode()); err != nil { - return err - } - } - - case tar.TypeReg, tar.TypeRegA: - file, err := openFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, hdrInfo.Mode()) - if err != nil { - return err - } - - _, err = copyBuffered(ctx, file, reader) - if err1 := file.Close(); err == nil { - err = err1 - } - if err != nil { - return err - } - - case tar.TypeBlock, tar.TypeChar: - // Handle this is an OS-specific way - if err := handleTarTypeBlockCharFifo(hdr, path); err != nil { - return err - } - - case tar.TypeFifo: - // Handle this is an OS-specific way - if err := handleTarTypeBlockCharFifo(hdr, path); err != nil { - return err - } - - case tar.TypeLink: - targetPath, err := fs.RootPath(extractDir, hdr.Linkname) - if err != nil { - return err - } - if err := os.Link(targetPath, path); err != nil { - return err - } - - case tar.TypeSymlink: - if err := os.Symlink(hdr.Linkname, path); err != nil { - return err - } - - case tar.TypeXGlobalHeader: - log.G(ctx).Debug("PAX Global Extended Headers found and ignored") - return nil - - default: - return errors.Errorf("unhandled tar header type %d\n", hdr.Typeflag) - } - - // Lchown is not supported on Windows. - if runtime.GOOS != "windows" { - if err := os.Lchown(path, hdr.Uid, hdr.Gid); err != nil { - return err - } - } - - for key, value := range hdr.PAXRecords { - if strings.HasPrefix(key, paxSchilyXattr) { - key = key[len(paxSchilyXattr):] - if err := setxattr(path, key, value); err != nil { - if errors.Cause(err) == syscall.ENOTSUP { - log.G(ctx).WithError(err).Warnf("ignored xattr %s in archive", key) - continue - } - return err - } - } - } - - // There is no LChmod, so ignore mode for symlink. Also, this - // must happen after chown, as that can modify the file mode - if err := handleLChmod(hdr, path, hdrInfo); err != nil { - return err - } - - return chtimes(path, boundTime(latestTime(hdr.AccessTime, hdr.ModTime)), boundTime(hdr.ModTime)) -} - func copyBuffered(ctx context.Context, dst io.Writer, src io.Reader) (written int64, err error) { buf := bufferPool.Get().(*[]byte) defer bufferPool.Put(buf) diff --git a/archive/tar_opts.go b/archive/tar_opts.go new file mode 100644 index 000000000..50a260595 --- /dev/null +++ b/archive/tar_opts.go @@ -0,0 +1,4 @@ +package archive + +// ApplyOpt allows setting mutable archive apply properties on creation +type ApplyOpt func(options *ApplyOptions) error diff --git a/archive/tar_opts_unix.go b/archive/tar_opts_unix.go new file mode 100644 index 000000000..717944701 --- /dev/null +++ b/archive/tar_opts_unix.go @@ -0,0 +1,7 @@ +// +build !windows + +package archive + +// ApplyOptions provides additional options for an Apply operation +type ApplyOptions struct { +} diff --git a/archive/tar_opts_windows.go b/archive/tar_opts_windows.go new file mode 100644 index 000000000..baba7eedd --- /dev/null +++ b/archive/tar_opts_windows.go @@ -0,0 +1,28 @@ +// +build windows + +package archive + +// ApplyOptions provides additional options for an Apply operation +type ApplyOptions struct { + ParentLayerPaths []string // Parent layer paths used for Windows layer apply + IsWindowsContainerLayer bool // True if the tar stream to be applied is a Windows Container Layer +} + +// WithParentLayers adds parent layers to the apply process this is required +// for all Windows layers except the base layer. +func WithParentLayers(parentPaths []string) ApplyOpt { + return func(options *ApplyOptions) error { + options.ParentLayerPaths = parentPaths + return nil + } +} + +// AsWindowsContainerLayer indicates that the tar stream to apply is that of +// a Windows Container Layer. The caller must be holding SeBackupPrivilege and +// SeRestorePrivilege. +func AsWindowsContainerLayer() ApplyOpt { + return func(options *ApplyOptions) error { + options.IsWindowsContainerLayer = true + return nil + } +} diff --git a/archive/tar_unix.go b/archive/tar_unix.go index 44b106943..6ecdbcfff 100644 --- a/archive/tar_unix.go +++ b/archive/tar_unix.go @@ -3,6 +3,7 @@ package archive import ( + "context" "os" "sync" "syscall" @@ -128,3 +129,9 @@ func getxattr(path, attr string) ([]byte, error) { func setxattr(path, key, value string) error { return sysx.LSetxattr(path, key, []byte(value), 0) } + +// apply applies a tar stream of an OCI style diff tar. +// See https://github.com/opencontainers/image-spec/blob/master/layer.md#applying-changesets +func apply(ctx context.Context, root string, tr *tar.Reader, options ApplyOptions) (size int64, err error) { + return applyNaive(ctx, root, tr, options) +} diff --git a/archive/tar_windows.go b/archive/tar_windows.go index cb3b6c59a..ea2403e4f 100644 --- a/archive/tar_windows.go +++ b/archive/tar_windows.go @@ -1,15 +1,55 @@ +// +build windows + package archive import ( + "bufio" + "context" + "encoding/base64" "errors" "fmt" + "io" "os" + "path" + "path/filepath" + "strconv" "strings" + "syscall" + "github.com/Microsoft/go-winio" + "github.com/Microsoft/hcsshim" + "github.com/containerd/containerd/log" "github.com/containerd/containerd/sys" "github.com/dmcgowan/go-tar" ) +const ( + // MSWINDOWS pax vendor extensions + hdrMSWindowsPrefix = "MSWINDOWS." + + hdrFileAttributes = hdrMSWindowsPrefix + "fileattr" + hdrSecurityDescriptor = hdrMSWindowsPrefix + "sd" + hdrRawSecurityDescriptor = hdrMSWindowsPrefix + "rawsd" + hdrMountPoint = hdrMSWindowsPrefix + "mountpoint" + hdrEaPrefix = hdrMSWindowsPrefix + "xattr." + + // LIBARCHIVE pax vendor extensions + hdrLibArchivePrefix = "LIBARCHIVE." + + hdrCreateTime = hdrLibArchivePrefix + "creationtime" +) + +var ( + // mutatedFiles is a list of files that are mutated by the import process + // and must be backed up and restored. + mutatedFiles = map[string]string{ + "UtilityVM/Files/EFI/Microsoft/Boot/BCD": "bcd.bak", + "UtilityVM/Files/EFI/Microsoft/Boot/BCD.LOG": "bcd.log.bak", + "UtilityVM/Files/EFI/Microsoft/Boot/BCD.LOG1": "bcd.log1.bak", + "UtilityVM/Files/EFI/Microsoft/Boot/BCD.LOG2": "bcd.log2.bak", + } +) + // tarName returns platform-specific filepath // to canonical posix-style path for tar archival. p is relative // path. @@ -101,3 +141,286 @@ func setxattr(path, key, value string) error { // since xattrs should not exist in windows diff archives return errors.New("xattrs not supported on Windows") } + +// apply applies a tar stream of an OCI style diff tar of a Windows layer. +// See https://github.com/opencontainers/image-spec/blob/master/layer.md#applying-changesets +func apply(ctx context.Context, root string, tr *tar.Reader, options ApplyOptions) (size int64, err error) { + if options.IsWindowsContainerLayer { + return applyWindowsLayer(ctx, root, tr, options) + } + return applyNaive(ctx, root, tr, options) +} + +// applyWindowsLayer applies a tar stream of an OCI style diff tar of a Windows layer. +// See https://github.com/opencontainers/image-spec/blob/master/layer.md#applying-changesets +func applyWindowsLayer(ctx context.Context, root string, tr *tar.Reader, options ApplyOptions) (size int64, err error) { + home, id := filepath.Split(root) + info := hcsshim.DriverInfo{ + HomeDir: home, + } + + w, err := hcsshim.NewLayerWriter(info, id, options.ParentLayerPaths) + if err != nil { + return 0, err + } + defer func() { + if err := w.Close(); err != nil { + log.G(ctx).Errorf("failed to close layer writer: %v", err) + } + }() + + buf := bufio.NewWriter(nil) + hdr, nextErr := tr.Next() + // Iterate through the files in the archive. + for { + select { + case <-ctx.Done(): + return 0, ctx.Err() + default: + } + + if nextErr == io.EOF { + // end of tar archive + break + } + if nextErr != nil { + return 0, nextErr + } + + // Note: path is used instead of filepath to prevent OS specific handling + // of the tar path + base := path.Base(hdr.Name) + if strings.HasPrefix(base, whiteoutPrefix) { + dir := path.Dir(hdr.Name) + originalBase := base[len(whiteoutPrefix):] + originalPath := path.Join(dir, originalBase) + if err := w.Remove(filepath.FromSlash(originalPath)); err != nil { + return 0, err + } + hdr, nextErr = tr.Next() + } else if hdr.Typeflag == tar.TypeLink { + err := w.AddLink(filepath.FromSlash(hdr.Name), filepath.FromSlash(hdr.Linkname)) + if err != nil { + return 0, err + } + hdr, nextErr = tr.Next() + } else { + name, fileSize, fileInfo, err := fileInfoFromHeader(hdr) + if err != nil { + return 0, err + } + if err := w.Add(filepath.FromSlash(name), fileInfo); err != nil { + return 0, err + } + size += fileSize + hdr, nextErr = tarToBackupStreamWithMutatedFiles(buf, w, tr, hdr, root) + } + } + + return +} + +// fileInfoFromHeader retrieves basic Win32 file information from a tar header, using the additional metadata written by +// WriteTarFileFromBackupStream. +func fileInfoFromHeader(hdr *tar.Header) (name string, size int64, fileInfo *winio.FileBasicInfo, err error) { + name = hdr.Name + if hdr.Typeflag == tar.TypeReg || hdr.Typeflag == tar.TypeRegA { + size = hdr.Size + } + fileInfo = &winio.FileBasicInfo{ + LastAccessTime: syscall.NsecToFiletime(hdr.AccessTime.UnixNano()), + LastWriteTime: syscall.NsecToFiletime(hdr.ModTime.UnixNano()), + ChangeTime: syscall.NsecToFiletime(hdr.ChangeTime.UnixNano()), + + // Default CreationTime to ModTime, updated below if MSWINDOWS.createtime exists + CreationTime: syscall.NsecToFiletime(hdr.ModTime.UnixNano()), + } + if attrStr, ok := hdr.PAXRecords[hdrFileAttributes]; ok { + attr, err := strconv.ParseUint(attrStr, 10, 32) + if err != nil { + return "", 0, nil, err + } + fileInfo.FileAttributes = uintptr(attr) + } else { + if hdr.Typeflag == tar.TypeDir { + fileInfo.FileAttributes |= syscall.FILE_ATTRIBUTE_DIRECTORY + } + } + if createStr, ok := hdr.PAXRecords[hdrCreateTime]; ok { + createTime, err := parsePAXTime(createStr) + if err != nil { + return "", 0, nil, err + } + fileInfo.CreationTime = syscall.NsecToFiletime(createTime.UnixNano()) + } + return +} + +// tarToBackupStreamWithMutatedFiles reads data from a tar stream and +// writes it to a backup stream, and also saves any files that will be mutated +// by the import layer process to a backup location. +func tarToBackupStreamWithMutatedFiles(buf *bufio.Writer, w io.Writer, t *tar.Reader, hdr *tar.Header, root string) (nextHdr *tar.Header, err error) { + var ( + bcdBackup *os.File + bcdBackupWriter *winio.BackupFileWriter + ) + if backupPath, ok := mutatedFiles[hdr.Name]; ok { + bcdBackup, err = os.Create(filepath.Join(root, backupPath)) + if err != nil { + return nil, err + } + defer func() { + cerr := bcdBackup.Close() + if err == nil { + err = cerr + } + }() + + bcdBackupWriter = winio.NewBackupFileWriter(bcdBackup, false) + defer func() { + cerr := bcdBackupWriter.Close() + if err == nil { + err = cerr + } + }() + + buf.Reset(io.MultiWriter(w, bcdBackupWriter)) + } else { + buf.Reset(w) + } + + defer func() { + ferr := buf.Flush() + if err == nil { + err = ferr + } + }() + + return writeBackupStreamFromTarFile(buf, t, hdr) +} + +// writeBackupStreamFromTarFile writes a Win32 backup stream from the current tar file. Since this function may process multiple +// tar file entries in order to collect all the alternate data streams for the file, it returns the next +// tar file that was not processed, or io.EOF is there are no more. +func writeBackupStreamFromTarFile(w io.Writer, t *tar.Reader, hdr *tar.Header) (*tar.Header, error) { + bw := winio.NewBackupStreamWriter(w) + var sd []byte + var err error + // Maintaining old SDDL-based behavior for backward compatibility. All new tar headers written + // by this library will have raw binary for the security descriptor. + if sddl, ok := hdr.PAXRecords[hdrSecurityDescriptor]; ok { + sd, err = winio.SddlToSecurityDescriptor(sddl) + if err != nil { + return nil, err + } + } + if sdraw, ok := hdr.PAXRecords[hdrRawSecurityDescriptor]; ok { + sd, err = base64.StdEncoding.DecodeString(sdraw) + if err != nil { + return nil, err + } + } + if len(sd) != 0 { + bhdr := winio.BackupHeader{ + Id: winio.BackupSecurity, + Size: int64(len(sd)), + } + err := bw.WriteHeader(&bhdr) + if err != nil { + return nil, err + } + _, err = bw.Write(sd) + if err != nil { + return nil, err + } + } + var eas []winio.ExtendedAttribute + for k, v := range hdr.PAXRecords { + if !strings.HasPrefix(k, hdrEaPrefix) { + continue + } + data, err := base64.StdEncoding.DecodeString(v) + if err != nil { + return nil, err + } + eas = append(eas, winio.ExtendedAttribute{ + Name: k[len(hdrEaPrefix):], + Value: data, + }) + } + if len(eas) != 0 { + eadata, err := winio.EncodeExtendedAttributes(eas) + if err != nil { + return nil, err + } + bhdr := winio.BackupHeader{ + Id: winio.BackupEaData, + Size: int64(len(eadata)), + } + err = bw.WriteHeader(&bhdr) + if err != nil { + return nil, err + } + _, err = bw.Write(eadata) + if err != nil { + return nil, err + } + } + if hdr.Typeflag == tar.TypeSymlink { + _, isMountPoint := hdr.PAXRecords[hdrMountPoint] + rp := winio.ReparsePoint{ + Target: filepath.FromSlash(hdr.Linkname), + IsMountPoint: isMountPoint, + } + reparse := winio.EncodeReparsePoint(&rp) + bhdr := winio.BackupHeader{ + Id: winio.BackupReparseData, + Size: int64(len(reparse)), + } + err := bw.WriteHeader(&bhdr) + if err != nil { + return nil, err + } + _, err = bw.Write(reparse) + if err != nil { + return nil, err + } + } + if hdr.Typeflag == tar.TypeReg || hdr.Typeflag == tar.TypeRegA { + bhdr := winio.BackupHeader{ + Id: winio.BackupData, + Size: hdr.Size, + } + err := bw.WriteHeader(&bhdr) + if err != nil { + return nil, err + } + _, err = io.Copy(bw, t) + if err != nil { + return nil, err + } + } + // Copy all the alternate data streams and return the next non-ADS header. + for { + ahdr, err := t.Next() + if err != nil { + return nil, err + } + if ahdr.Typeflag != tar.TypeReg || !strings.HasPrefix(ahdr.Name, hdr.Name+":") { + return ahdr, nil + } + bhdr := winio.BackupHeader{ + Id: winio.BackupAlternateData, + Size: ahdr.Size, + Name: ahdr.Name[len(hdr.Name):] + ":$DATA", + } + err = bw.WriteHeader(&bhdr) + if err != nil { + return nil, err + } + _, err = io.Copy(bw, t) + if err != nil { + return nil, err + } + } +}