Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFC] overlay: make sure directories omitted from layers have the right permissions #1653

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 12 additions & 0 deletions drivers/aufs/aufs.go
Expand Up @@ -38,6 +38,7 @@ import (
"time"

graphdriver "github.com/containers/storage/drivers"
"github.com/containers/storage/drivers/unionbackfill"
"github.com/containers/storage/pkg/archive"
"github.com/containers/storage/pkg/chrootarchive"
"github.com/containers/storage/pkg/directory"
Expand All @@ -46,6 +47,7 @@ import (
mountpk "github.com/containers/storage/pkg/mount"
"github.com/containers/storage/pkg/parsers"
"github.com/containers/storage/pkg/system"
"github.com/containers/storage/pkg/tarbackfill"
"github.com/containers/storage/pkg/unshare"
"github.com/opencontainers/selinux/go-selinux/label"
"github.com/sirupsen/logrus"
Expand Down Expand Up @@ -564,6 +566,16 @@ func (a *Driver) applyDiff(id string, idMappings *idtools.IDMappings, diff io.Re
if idMappings == nil {
idMappings = &idtools.IDMappings{}
}
parentDiffDirs, err := a.getParentLayerPaths(id)
if err != nil {
return err
}
if len(parentDiffDirs) > 0 {
backfiller := unionbackfill.NewBackfiller(idMappings, parentDiffDirs)
rc := tarbackfill.NewIOReaderWithBackfiller(diff, backfiller)
defer rc.Close()
diff = rc
}
return chrootarchive.UntarUncompressed(diff, path.Join(a.rootPath(), "diff", id), &archive.TarOptions{
UIDMaps: idMappings.UIDs(),
GIDMaps: idMappings.GIDs(),
Expand Down
1 change: 1 addition & 0 deletions drivers/driver.go
Expand Up @@ -188,6 +188,7 @@ type DriverWithDifferOutput struct {
Metadata string
BigData map[string][]byte
TarSplit []byte
ImplicitDirs []string
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some documentation of what this field contains would be nice.

TOCDigest digest.Digest
}

Expand Down
58 changes: 57 additions & 1 deletion drivers/overlay/overlay.go
Expand Up @@ -21,6 +21,7 @@ import (
graphdriver "github.com/containers/storage/drivers"
"github.com/containers/storage/drivers/overlayutils"
"github.com/containers/storage/drivers/quota"
"github.com/containers/storage/drivers/unionbackfill"
"github.com/containers/storage/pkg/archive"
"github.com/containers/storage/pkg/chrootarchive"
"github.com/containers/storage/pkg/directory"
Expand All @@ -30,6 +31,7 @@ import (
"github.com/containers/storage/pkg/mount"
"github.com/containers/storage/pkg/parsers"
"github.com/containers/storage/pkg/system"
"github.com/containers/storage/pkg/tarbackfill"
"github.com/containers/storage/pkg/unshare"
units "github.com/docker/go-units"
"github.com/hashicorp/go-multierror"
Expand Down Expand Up @@ -2082,6 +2084,49 @@ func (d *Driver) ApplyDiffFromStagingDirectory(id, parent, stagingDirectory stri
return err
}

lowerDiffDirs, err := d.getLowerDiffPaths(id)
if err != nil {
return err
}
if len(lowerDiffDirs) > 0 {
backfiller := unionbackfill.NewBackfiller(options.Mappings, lowerDiffDirs)
for _, implicitDir := range diffOutput.ImplicitDirs {
hdr, err := backfiller.Backfill(implicitDir)
if err != nil {
return err
}
if hdr == nil {
continue
}
path := filepath.Join(stagingDirectory, implicitDir)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a vague feeling that the code to apply a *tar.Header to a filesystem object should already exist somewhere, and that duplicating it is not the ideal approach.

I have no idea where is the best place for that code (pkg/archive?) and what all options need to be provided to it, though…

As specific examples, looking at overlay.ApplyDiffWithDiffer, it seems that d.options.ignoreChownErrors or options.ForceMask are not implemented in this new copy. I guess ForceMask is irrelevant because it should have been applied do the parent layer correctly, already… can that be assumed even with additional layer stores?

idPair := idtools.IDPair{UID: hdr.Uid, GID: hdr.Gid}
if options.Mappings != nil {
if mapped, err := options.Mappings.ToHost(idPair); err == nil {
idPair = mapped
}
}
if err := os.Chown(path, idPair.UID, idPair.GID); err != nil {
return err
}
for xattr, xval := range hdr.Xattrs {
if err := system.Lsetxattr(path, xattr, []byte(xval), 0); err != nil {
return err
}
}
if err := os.Chmod(path, os.FileMode(hdr.Mode)&os.ModePerm); err != nil {
return err
}
atime := hdr.AccessTime
mtime := hdr.ModTime
if atime.IsZero() {
atime = mtime
}
if err := os.Chtimes(path, atime, mtime); err != nil {
return err
}
}
}

diffOutput.UncompressedDigest = diffOutput.TOCDigest

return os.Rename(stagingDirectory, diffPath)
Expand Down Expand Up @@ -2116,7 +2161,18 @@ func (d *Driver) ApplyDiff(id, parent string, options graphdriver.ApplyDiffOpts)

logrus.Debugf("Applying tar in %s", applyDir)
// Overlay doesn't need the parent id to apply the diff
if err := untar(options.Diff, applyDir, &archive.TarOptions{
diff := options.Diff
lowerDiffDirs, err := d.getLowerDiffPaths(id)
if err != nil {
return 0, err
}
if len(lowerDiffDirs) > 0 {
backfiller := unionbackfill.NewBackfiller(idMappings, lowerDiffDirs)
rc := tarbackfill.NewIOReaderWithBackfiller(diff, backfiller)
defer rc.Close()
diff = rc
}
if err := untar(diff, applyDir, &archive.TarOptions{
UIDMaps: idMappings.UIDs(),
GIDMaps: idMappings.GIDs(),
IgnoreChownErrors: d.options.ignoreChownErrors,
Expand Down
106 changes: 106 additions & 0 deletions drivers/unionbackfill/backfill.go
@@ -0,0 +1,106 @@
package unionbackfill

import (
"archive/tar"
"io/fs"
"os"
"path"
"path/filepath"
"strings"

"github.com/containers/storage/pkg/archive"
"github.com/containers/storage/pkg/idtools"
"github.com/containers/storage/pkg/system"
)

// NewBackfiller supplies a backfiller whose Backfill method provides the
// ownership/permissions/attributes of a directory from a lower layer so that
// we don't have to create it in an upper layer using default values that will
// be mistaken for a reason that the directory was pulled up to that layer.
func NewBackfiller(idmap *idtools.IDMappings, lowerDiffDirs []string) *backfiller {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be a public c/storage API, or would something like c/storage/internal/unionbackfill work?

(I don’t have a strong opinion, I just want this to be an intentional decision.)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If lowerDiffDirs is expected to be from ordered from children to parents (is it?), that would be useful to document at least at subpackage/API boundaries like this.

if idmap != nil {
uidMaps, gidMaps := idmap.UIDs(), idmap.GIDs()
if len(uidMaps) > 0 || len(gidMaps) > 0 {
idmap = idtools.NewIDMappingsFromMaps(append([]idtools.IDMap{}, uidMaps...), append([]idtools.IDMap{}, gidMaps...))
}
}
return &backfiller{idmap: idmap, lowerDiffDirs: append([]string{}, lowerDiffDirs...)}
Comment on lines +24 to +27
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Absolutely non-blocking: This might be a tiny bit more readable when using x/exp/slices.Clone; that will be in the standard library in future Go 1.21. OTOH until then the x/exp package is technically without any stability promise…)

}

type backfiller struct {
idmap *idtools.IDMappings
lowerDiffDirs []string
}

// Backfill supplies the ownership/permissions/attributes of a directory from a
// lower layer so that we don't have to create it in an upper layer using
// default values that will be mistaken for a reason that the directory was
// pulled up to that layer.
func (b *backfiller) Backfill(pathname string) (*tar.Header, error) {
for _, lowerDiffDir := range b.lowerDiffDirs {
candidate := filepath.Join(lowerDiffDir, pathname)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At least pkg/tarbackfill seems to pass a mostly-unsanitized tar.Header.Name value here; so it seems to me that this can be used to escape the layer and interact with external directories.

… actually even with a pathname without any .. fields, it seems to me that pathname could refer to in-layer symlinks (pathname = "a/passwd", with a parent layer set up so that /a is a symlink to ../../../../.../etc). So this might need cyphar/filepath-securejoin, or some other way to restrict path resolution to lowerDiffDir.

// if the asked-for path is in this lower, return a tar header for it
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can’t think of anything that would rule out us finding a whiteout.

I’m not sure that needs a special handling, but at least I think this can exit early because using any of the whiteout’s, or pre-whiteout directory’s, permissions would not be beneficial.

if st, err := os.Lstat(candidate); err == nil {
var linkTarget string
if st.Mode()&fs.ModeType == fs.ModeSymlink {
target, err := os.Readlink(candidate)
if err != nil {
return nil, err
}
linkTarget = target
}
hdr, err := tar.FileInfoHeader(st, linkTarget)
if err != nil {
return nil, err
}
// this is where we'd delete "opaque" from the header, if FileInfoHeader read xattrs
hdr.Name = strings.Trim(filepath.ToSlash(pathname), "/")
if st.Mode()&fs.ModeType == fs.ModeDir {
hdr.Name += "/"
}
if b.idmap != nil && !b.idmap.Empty() {
if uid, gid, err := b.idmap.ToContainer(idtools.IDPair{UID: hdr.Uid, GID: hdr.Gid}); err == nil {
hdr.Uid, hdr.Gid = uid, gid
}
}
return hdr, nil
Comment on lines +44 to +66
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again a vague feeling this probably should be shared.

At least archive.FileInfoHeader seems beneficial; perhaps a larger part of archive.tarAppender.addTarFile.

}
// if the directory or any of its parents is marked opaque, we're done looking at lowers
p := strings.Trim(pathname, "/")
subpathname := ""
for {
dir, subdir := filepath.Split(p)
dir = strings.Trim(dir, "/")
if dir == p {
break
}
// kernel overlay style
xval, err := system.Lgetxattr(filepath.Join(lowerDiffDir, dir), archive.GetOverlayXattrName("opaque"))
if err == nil && len(xval) == 1 && xval[0] == 'y' {
return nil, nil
}
// aufs or fuse-overlayfs using aufs-like whiteouts
if _, err := os.Stat(filepath.Join(lowerDiffDir, dir, archive.WhiteoutOpaqueDir)); err == nil {
return nil, nil
}
// kernel overlay "redirect" - starting with the next lower layer, we'll need to look elsewhere
subpathname = strings.Trim(path.Join(subdir, subpathname), "/")
xval, err = system.Lgetxattr(filepath.Join(lowerDiffDir, dir), archive.GetOverlayXattrName("redirect"))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to worry about images shipping this attribute in layers? At least from a quick check of archive.createTarFile, setting the overlay attribute (either one) in a tar header seems not to be restricted.

(Ultimately, AFAICS the path always goes through path.Join = path.Clean, and if it is relative, it is relative to some other presumably-safe value [1], so this seems harmless anyway… just something I’d like to not forget about.)

[1] Not actually, but that can be fixed elsewhere.

if err == nil && len(xval) > 0 {
subdir := string(xval)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
subdir := string(xval)
redirectedDir := string(xval)

or some other name expressing the relationship to dir? I was confused by reusing the subdir name, which is relative to dir, for a value that replaces dir.

if path.IsAbs(subdir) {
// path is relative to the root of the mount point
pathname = path.Join(subdir, subpathname)
} else {
// path is relative to the current directory
parent, _ := filepath.Split(dir)
parent = strings.Trim(parent, "/")
pathname = path.Join(parent, subdir, subpathname)
}
break
}
p = dir
}
}
return nil, nil
}