diffApply: set dir opaque when overwriting whiteout

Before this, there was a bug triggered under the following conditions:
1. An overlay snapshotter was being used, which caused the optimization
   of preparing a new snapshot off of the base layers to be triggered
2. The base layers contained a directory that had contents
3. One subsequent layer deleted that directory w/out recreating it
4. A later layer recreated the directory

In this case, what happened was a whiteout device would be created as
part of 3 above but then in step 4 the whiteout device would be removed
and replaced with a plain directory. The problem is that such a
directory doesn't block out the files from step 2 and it doesn't know
about them because they are in a lowerdir (not the upperdir being
applied to).

The simplest fix, which this commit implements, is to just set the
directory created in step 4 as opaque, which enables the correct
behavior of blocking out files below it.

This was missed in test coverage before because tests for opaque
handling always combined 3+4 into one layer, whereas the bug requires
they be separate layers. A new integration test has been added to cover
this case.

Signed-off-by: Erik Sipsma <erik@sipsma.dev>
master
Erik Sipsma 2022-02-09 18:59:12 -08:00
parent 2f996517bc
commit 0a2a4fe066
3 changed files with 85 additions and 14 deletions

View File

@ -1070,6 +1070,31 @@ func diffOpTestCases() (tests []integration.Test) {
} }
}()...) }()...)
// Regression tests
tests = append(tests, func() []integration.Test {
base := func() llb.State {
return llb.Scratch().File(llb.Mkdir("/dir", 0755))
}
return []integration.Test{
verifyContents{
// Verifies that when a directory with contents is used a a base layer
// in a merge, subsequent merges that first delete the dir (resulting in
// a whiteout device w/ overlay snapshotters) and then recreate the dir
// correctly set it as opaque.
name: "TestDiffMergeOpaqueRegression",
state: llb.Merge([]llb.State{
base().File(llb.Mkfile("/dir/a", 0644, nil)),
base().File(llb.Rm("/dir")),
base().File(llb.Mkfile("/dir/b", 0644, nil)),
}),
contents: apply(
fstest.CreateDir("/dir", 0755),
fstest.CreateFile("/dir/b", nil, 0644),
),
},
}
}()...)
return tests return tests
} }

View File

@ -30,7 +30,7 @@ import (
// that accounts for any hardlinks made from existing snapshots. ctx is expected to have a temporary lease // that accounts for any hardlinks made from existing snapshots. ctx is expected to have a temporary lease
// associated with it. // associated with it.
func (sn *mergeSnapshotter) diffApply(ctx context.Context, dest Mountable, diffs ...Diff) (_ snapshots.Usage, rerr error) { func (sn *mergeSnapshotter) diffApply(ctx context.Context, dest Mountable, diffs ...Diff) (_ snapshots.Usage, rerr error) {
a, err := applierFor(dest, sn.tryCrossSnapshotLink) a, err := applierFor(dest, sn.tryCrossSnapshotLink, sn.userxattr)
if err != nil { if err != nil {
return snapshots.Usage{}, errors.Wrapf(err, "failed to create applier") return snapshots.Usage{}, errors.Wrapf(err, "failed to create applier")
} }
@ -112,8 +112,9 @@ type change struct {
type changeApply struct { type changeApply struct {
*change *change
dstPath string dstPath string
dstStat *syscall.Stat_t dstStat *syscall.Stat_t
setOpaque bool
} }
type inode struct { type inode struct {
@ -137,12 +138,14 @@ type applier struct {
lowerdirs []string // ordered highest -> lowest, the order we want to check them in lowerdirs []string // ordered highest -> lowest, the order we want to check them in
crossSnapshotLinks map[inode]struct{} crossSnapshotLinks map[inode]struct{}
createWhiteoutDelete bool createWhiteoutDelete bool
userxattr bool
dirModTimes map[string]unix.Timespec // map of dstPath -> mtime that should be set on that subPath dirModTimes map[string]unix.Timespec // map of dstPath -> mtime that should be set on that subPath
} }
func applierFor(dest Mountable, tryCrossSnapshotLink bool) (_ *applier, rerr error) { func applierFor(dest Mountable, tryCrossSnapshotLink, userxattr bool) (_ *applier, rerr error) {
a := &applier{ a := &applier{
dirModTimes: make(map[string]unix.Timespec), dirModTimes: make(map[string]unix.Timespec),
userxattr: userxattr,
} }
defer func() { defer func() {
if rerr != nil { if rerr != nil {
@ -263,6 +266,12 @@ func (a *applier) applyDelete(ctx context.Context, ca *changeApply) (bool, error
return false, nil return false, nil
} }
if overwrite && a.createWhiteoutDelete && isWhiteoutDevice(ca.dstStat) && ca.srcStat.Mode&unix.S_IFMT == unix.S_IFDIR {
// If we are overwriting a whiteout device with a directory, we need this new dir to be opaque
// so that any files from lowerdirs under it are not visible.
ca.setOpaque = true
}
if err := os.RemoveAll(ca.dstPath); err != nil { if err := os.RemoveAll(ca.dstPath); err != nil {
return false, errors.Wrap(err, "failed to remove during apply") return false, errors.Wrap(err, "failed to remove during apply")
} }
@ -376,9 +385,9 @@ func (a *applier) applyCopy(ctx context.Context, ca *changeApply) error {
} }
for _, xattr := range xattrs { for _, xattr := range xattrs {
if isOpaqueXattr(xattr) { if isOpaqueXattr(xattr) {
// Don't recreate opaque xattrs during merge. These should only be set when using overlay snapshotters, // Don't recreate opaque xattrs during merge based on the source file. The differs take care of converting
// in which case we are converting from the "opaque whiteout" format to the "explicit whiteout" format during // source path from the "opaque whiteout" format to the "explicit whiteout" format. The only time we set
// the merge (as taken care of by the overlay differ). // opaque xattrs is handled after this loop below.
continue continue
} }
xattrVal, err := sysx.LGetxattr(ca.srcPath, xattr) xattrVal, err := sysx.LGetxattr(ca.srcPath, xattr)
@ -392,6 +401,14 @@ func (a *applier) applyCopy(ctx context.Context, ca *changeApply) error {
} }
} }
if ca.setOpaque {
// This is set in the case where we are creating a directory that is replacing a whiteout device
xattr := opaqueXattr(a.userxattr)
if err := sysx.LSetxattr(ca.dstPath, xattr, []byte{'y'}, 0); err != nil {
return errors.Wrapf(err, "failed to set opaque xattr %q of path %s", xattr, ca.dstPath)
}
}
if err := os.Lchown(ca.dstPath, int(ca.srcStat.Uid), int(ca.srcStat.Gid)); err != nil { if err := os.Lchown(ca.dstPath, int(ca.srcStat.Uid), int(ca.srcStat.Gid)); err != nil {
return errors.Wrap(err, "failed to chown during apply") return errors.Wrap(err, "failed to chown during apply")
} }
@ -773,8 +790,13 @@ func safeJoin(root, path string) (string, error) {
return filepath.Join(parent, base), nil return filepath.Join(parent, base), nil
} }
const (
trustedOpaqueXattr = "trusted.overlay.opaque"
userOpaqueXattr = "user.overlay.opaque"
)
func isOpaqueXattr(s string) bool { func isOpaqueXattr(s string) bool {
for _, k := range []string{"trusted.overlay.opaque", "user.overlay.opaque"} { for _, k := range []string{trustedOpaqueXattr, userOpaqueXattr} {
if s == k { if s == k {
return true return true
} }
@ -782,6 +804,13 @@ func isOpaqueXattr(s string) bool {
return false return false
} }
func opaqueXattr(userxattr bool) string {
if userxattr {
return userOpaqueXattr
}
return trustedOpaqueXattr
}
// needsUserXAttr checks whether overlay mounts should be provided the userxattr option. We can't use // needsUserXAttr checks whether overlay mounts should be provided the userxattr option. We can't use
// NeedsUserXAttr from the overlayutils package directly because we don't always have direct knowledge // NeedsUserXAttr from the overlayutils package directly because we don't always have direct knowledge
// of the root of the snapshotter state (such as when using a remote snapshotter). Instead, we create // of the root of the snapshotter state (such as when using a remote snapshotter). Instead, we create
@ -820,3 +849,8 @@ func needsUserXAttr(ctx context.Context, sn Snapshotter, lm leases.Manager) (boo
} }
return userxattr, nil return userxattr, nil
} }
func isWhiteoutDevice(st *syscall.Stat_t) bool {
// it's a whiteout if it's a char device and has a major/minor of 0/0
return st != nil && st.Mode&unix.S_IFMT == unix.S_IFCHR && st.Rdev == unix.Mkdev(0, 0)
}

View File

@ -58,9 +58,12 @@ type mergeSnapshotter struct {
// Whether we should try to implement merges by hardlinking between underlying directories // Whether we should try to implement merges by hardlinking between underlying directories
tryCrossSnapshotLink bool tryCrossSnapshotLink bool
// Whether the snapshotter is overlay-based, which enables some some optimizations like // Whether the optimization of preparing on top of base layers is supported (see Merge method).
// using the first merge input as the parent snapshot. skipBaseLayers bool
overlayBased bool
// Whether we should use the "user.*" namespace when writing overlay xattrs. If false,
// "trusted.*" is used instead.
userxattr bool
} }
func NewMergeSnapshotter(ctx context.Context, sn Snapshotter, lm leases.Manager) MergeSnapshotter { func NewMergeSnapshotter(ctx context.Context, sn Snapshotter, lm leases.Manager) MergeSnapshotter {
@ -68,16 +71,24 @@ func NewMergeSnapshotter(ctx context.Context, sn Snapshotter, lm leases.Manager)
_, tryCrossSnapshotLink := hardlinkMergeSnapshotters[name] _, tryCrossSnapshotLink := hardlinkMergeSnapshotters[name]
_, overlayBased := overlayBasedSnapshotters[name] _, overlayBased := overlayBasedSnapshotters[name]
skipBaseLayers := overlayBased // default to skipping base layer for overlay-based snapshotters
var userxattr bool
if overlayBased && userns.RunningInUserNS() { if overlayBased && userns.RunningInUserNS() {
// When using an overlay-based snapshotter, if we are running rootless on a pre-5.11 // When using an overlay-based snapshotter, if we are running rootless on a pre-5.11
// kernel, we will not have userxattr. This results in opaque xattrs not being visible // kernel, we will not have userxattr. This results in opaque xattrs not being visible
// to us and thus breaking the overlay-optimized differ. // to us and thus breaking the overlay-optimized differ.
userxattr, err := needsUserXAttr(ctx, sn, lm) var err error
userxattr, err = needsUserXAttr(ctx, sn, lm)
if err != nil { if err != nil {
bklog.G(ctx).Debugf("failed to check user xattr: %v", err) bklog.G(ctx).Debugf("failed to check user xattr: %v", err)
tryCrossSnapshotLink = false tryCrossSnapshotLink = false
skipBaseLayers = false
} else { } else {
tryCrossSnapshotLink = userxattr tryCrossSnapshotLink = userxattr
// Disable skipping base layers when in pre-5.11 rootless mode. Skipping the base layers
// necessitates the ability to set opaque xattrs sometimes, which only works in 5.11+
// kernels that support userxattr.
skipBaseLayers = userxattr
} }
} }
@ -85,13 +96,14 @@ func NewMergeSnapshotter(ctx context.Context, sn Snapshotter, lm leases.Manager)
Snapshotter: sn, Snapshotter: sn,
lm: lm, lm: lm,
tryCrossSnapshotLink: tryCrossSnapshotLink, tryCrossSnapshotLink: tryCrossSnapshotLink,
overlayBased: overlayBased, skipBaseLayers: skipBaseLayers,
userxattr: userxattr,
} }
} }
func (sn *mergeSnapshotter) Merge(ctx context.Context, key string, diffs []Diff, opts ...snapshots.Opt) error { func (sn *mergeSnapshotter) Merge(ctx context.Context, key string, diffs []Diff, opts ...snapshots.Opt) error {
var baseKey string var baseKey string
if sn.overlayBased { if sn.skipBaseLayers {
// Overlay-based snapshotters can skip the base snapshot of the merge (if one exists) and just use it as the // Overlay-based snapshotters can skip the base snapshot of the merge (if one exists) and just use it as the
// parent of the merge snapshot. Other snapshotters will start empty (with baseKey set to ""). // parent of the merge snapshot. Other snapshotters will start empty (with baseKey set to "").
// Find the baseKey by following the chain of diffs for as long as it follows the pattern of the current lower // Find the baseKey by following the chain of diffs for as long as it follows the pattern of the current lower