moby--moby/pkg/containerfs/archiver.go

206 lines
5.7 KiB
Go
Raw Normal View History

package containerfs // import "github.com/docker/docker/pkg/containerfs"
import (
"archive/tar"
pkg/system: move EnsureRemoveAll() to pkg/containerfs pkg/system historically has been a bit of a kitchen-sink of things that were somewhat "system" related, but didn't have a good place for. EnsureRemoveAll() is one of those utilities. EnsureRemoveAll() is used to both unmount and remove a path, for which it depends on both github.com/moby/sys/mount, which in turn depends on github.com/moby/sys/mountinfo. pkg/system is imported in the CLI, but neither EnsureRemoveAll(), nor any of its moby/sys dependencies are used on the client side, so let's move this function somewhere else, to remove those dependencies from the CLI. I looked for plausible locations that were related; it's used in: - daemon - daemon/graphdriver/XXX/ - plugin I considered moving it into a (e.g.) "utils" package within graphdriver (but not a huge fan of "utils" packages), and given that it felt (mostly) related to cleaning up container filesystems, I decided to move it there. Some things to follow-up on after this: - Verify if this function is still needed (it feels a bit like a big hammer in a "YOLO, let's try some things just in case it fails") - Perhaps it should be integrated in `containerfs.Remove()` (so that it's used automatically) - Look if there's other implementations (and if they should be consolidated), although (e.g.) the one in containerd is a copy of ours: https://github.com/containerd/containerd/blob/v1.5.9/pkg/cri/server/helpers_linux.go#L200 Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2022-03-02 21:43:07 +00:00
"errors"
"io"
"os"
"path/filepath"
"time"
"github.com/docker/docker/pkg/archive"
"github.com/docker/docker/pkg/idtools"
"github.com/docker/docker/pkg/system"
"github.com/sirupsen/logrus"
)
// TarFunc provides a function definition for a custom Tar function
type TarFunc func(string, *archive.TarOptions) (io.ReadCloser, error)
// UntarFunc provides a function definition for a custom Untar function
type UntarFunc func(io.Reader, string, *archive.TarOptions) error
// Archiver provides a similar implementation of the archive.Archiver package with the rootfs abstraction
type Archiver struct {
SrcDriver Driver
DstDriver Driver
Tar TarFunc
Untar UntarFunc
IDMapping *idtools.IdentityMapping
}
// TarUntar is a convenience function which calls Tar and Untar, with the output of one piped into the other.
// If either Tar or Untar fails, TarUntar aborts and returns the error.
func (archiver *Archiver) TarUntar(src, dst string) error {
logrus.Debugf("TarUntar(%s %s)", src, dst)
tarArchive, err := archiver.Tar(src, &archive.TarOptions{Compression: archive.Uncompressed})
if err != nil {
return err
}
defer tarArchive.Close()
options := &archive.TarOptions{
UIDMaps: archiver.IDMapping.UIDs(),
GIDMaps: archiver.IDMapping.GIDs(),
}
return archiver.Untar(tarArchive, dst, options)
}
// UntarPath untar a file from path to a destination, src is the source tar file path.
func (archiver *Archiver) UntarPath(src, dst string) error {
tarArchive, err := archiver.SrcDriver.Open(src)
if err != nil {
return err
}
defer tarArchive.Close()
options := &archive.TarOptions{
UIDMaps: archiver.IDMapping.UIDs(),
GIDMaps: archiver.IDMapping.GIDs(),
}
return archiver.Untar(tarArchive, dst, options)
}
// CopyWithTar creates a tar archive of filesystem path `src`, and
// unpacks it at filesystem path `dst`.
// The archive is streamed directly with fixed buffering and no
// intermediary disk IO.
func (archiver *Archiver) CopyWithTar(src, dst string) error {
srcSt, err := archiver.SrcDriver.Stat(src)
if err != nil {
return err
}
if !srcSt.IsDir() {
return archiver.CopyFileWithTar(src, dst)
}
// if this archiver is set up with ID mapping we need to create
// the new destination directory with the remapped root UID/GID pair
// as owner
identity := idtools.Identity{UID: archiver.IDMapping.RootPair().UID, GID: archiver.IDMapping.RootPair().GID}
// Create dst, copy src's content into it
if err := idtools.MkdirAllAndChownNew(dst, 0755, identity); err != nil {
return err
}
logrus.Debugf("Calling TarUntar(%s, %s)", src, dst)
return archiver.TarUntar(src, dst)
}
// CopyFileWithTar emulates the behavior of the 'cp' command-line
// for a single file. It copies a regular file from path `src` to
// path `dst`, and preserves all its metadata.
func (archiver *Archiver) CopyFileWithTar(src, dst string) (retErr error) {
logrus.Debugf("CopyFileWithTar(%s, %s)", src, dst)
srcDriver := archiver.SrcDriver
dstDriver := archiver.DstDriver
srcSt, retErr := srcDriver.Stat(src)
if retErr != nil {
return retErr
}
if srcSt.IsDir() {
pkg/system: move EnsureRemoveAll() to pkg/containerfs pkg/system historically has been a bit of a kitchen-sink of things that were somewhat "system" related, but didn't have a good place for. EnsureRemoveAll() is one of those utilities. EnsureRemoveAll() is used to both unmount and remove a path, for which it depends on both github.com/moby/sys/mount, which in turn depends on github.com/moby/sys/mountinfo. pkg/system is imported in the CLI, but neither EnsureRemoveAll(), nor any of its moby/sys dependencies are used on the client side, so let's move this function somewhere else, to remove those dependencies from the CLI. I looked for plausible locations that were related; it's used in: - daemon - daemon/graphdriver/XXX/ - plugin I considered moving it into a (e.g.) "utils" package within graphdriver (but not a huge fan of "utils" packages), and given that it felt (mostly) related to cleaning up container filesystems, I decided to move it there. Some things to follow-up on after this: - Verify if this function is still needed (it feels a bit like a big hammer in a "YOLO, let's try some things just in case it fails") - Perhaps it should be integrated in `containerfs.Remove()` (so that it's used automatically) - Look if there's other implementations (and if they should be consolidated), although (e.g.) the one in containerd is a copy of ours: https://github.com/containerd/containerd/blob/v1.5.9/pkg/cri/server/helpers_linux.go#L200 Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2022-03-02 21:43:07 +00:00
return errors.New("cannot copy a directory")
}
// Clean up the trailing slash. This must be done in an operating
// system specific manner.
if dst[len(dst)-1] == dstDriver.Separator() {
dst = dstDriver.Join(dst, srcDriver.Base(src))
}
// The original call was system.MkdirAll, which is just
// os.MkdirAll on not-Windows and changed for Windows.
if dstDriver.OS() == "windows" {
// Now we are WCOW
if err := system.MkdirAll(filepath.Dir(dst), 0700); err != nil {
return err
}
} else {
// We can just use the driver.MkdirAll function
if err := dstDriver.MkdirAll(dstDriver.Dir(dst), 0700); err != nil {
return err
}
}
r, w := io.Pipe()
errC := make(chan error, 1)
go func() {
defer close(errC)
errC <- func() error {
defer w.Close()
srcF, err := srcDriver.Open(src)
if err != nil {
return err
}
defer srcF.Close()
Remove local fork of archive/tar package A copy of Go's archive/tar packge was vendored with a patch applied to mitigate CVE-2019-14271. Vendoring standard library packages is not supported by Go in module-aware mode, which is getting in the way of maintenance. A different approach to mitigate the vulnerability is needed which does not involve vendoring parts of the standard library. glibc implements name service lookups such as users, groups and DNS using a scheme known as Name Service Switch. The services are implemented as modules, shared libraries which glibc dynamically links into the process the first time a function requiring the module is called. This is the crux of the vulnerability: if a process linked against glibc chroots, then calls one of the functions implemented with NSS for the first time, glibc may load NSS modules out of the chrooted filesystem. The API underlying the `docker cp` command is implemented by forking a new process which chroots into the container's rootfs and writes a tar stream of files from the container over standard output. It utilizes the Go standard library's archive/tar package to write the tar stream. It makes use of the tar.FileInfoHeader function to construct a tar.Header value from an fs.FileInfo value. In modern versions of Go on *nix platforms, FileInfoHeader will attempt to resolve the file's UID and GID to their respective user and group names by calling the os/user functions LookupId and LookupGroupId. The cgo implementation of os/user on *nix performs lookups by calling the corresponding libc functions. So when linked against glibc, calls to tar.FileInfoHeader after the process has chrooted into the container's rootfs can have the side effect of loading NSS modules from the container! Without any mitigations, a malicious container image author can trivially get arbitrary code execution by leveraging this vulnerability and escape the chroot (which is not a sandbox) into the host. Mitigate the vulnerability without patching or forking archive/tar by hiding the OS-dependent file info from tar.FileInfoHeader which it needs to perform the lookups. Without that information available it falls back to populating the tar.Header with only the information obtainable directly from the FileInfo value without making any calls into os/user. Fixes #42402 Signed-off-by: Cory Snider <csnider@mirantis.com>
2022-01-24 20:33:54 +00:00
hdr, err := archive.FileInfoHeaderNoLookups(srcSt, "")
if err != nil {
return err
}
hdr.Format = tar.FormatPAX
hdr.ModTime = hdr.ModTime.Truncate(time.Second)
hdr.AccessTime = time.Time{}
hdr.ChangeTime = time.Time{}
hdr.Name = dstDriver.Base(dst)
if dstDriver.OS() == "windows" {
hdr.Mode = int64(chmodTarEntry(os.FileMode(hdr.Mode)))
} else {
hdr.Mode = int64(os.FileMode(hdr.Mode))
}
if err := remapIDs(archiver.IDMapping, hdr); err != nil {
return err
}
tw := tar.NewWriter(w)
defer tw.Close()
if err := tw.WriteHeader(hdr); err != nil {
return err
}
if _, err := io.Copy(tw, srcF); err != nil {
return err
}
return nil
}()
}()
defer func() {
if err := <-errC; retErr == nil && err != nil {
retErr = err
}
}()
retErr = archiver.Untar(r, dstDriver.Dir(dst), nil)
if retErr != nil {
r.CloseWithError(retErr)
}
return retErr
}
// IdentityMapping returns the IdentityMapping of the archiver.
func (archiver *Archiver) IdentityMapping() *idtools.IdentityMapping {
return archiver.IDMapping
}
func remapIDs(idMapping *idtools.IdentityMapping, hdr *tar.Header) error {
ids, err := idMapping.ToHost(idtools.Identity{UID: hdr.Uid, GID: hdr.Gid})
hdr.Uid, hdr.Gid = ids.UID, ids.GID
return err
}
// chmodTarEntry is used to adjust the file permissions used in tar header based
// on the platform the archival is done.
func chmodTarEntry(perm os.FileMode) os.FileMode {
// perm &= 0755 // this 0-ed out tar flags (like link, regular file, directory marker etc.)
permPart := perm & os.ModePerm
noPermPart := perm &^ os.ModePerm
// Add the x bit: make everything +x from windows
permPart |= 0111
permPart &= 0755
return noPermPart | permPart
}