Skip to content

Commit

Permalink
feat: OverlayFS -> AUFS whiteout conversion
Browse files Browse the repository at this point in the history
Allow OverlayFS whiteouts to be converted to AUFS, by filtering a tar
stream.

In this direction the conversion is a single pass, single operation.
There is no requirement to pre-scan the tar.

Close #53
  • Loading branch information
dtrudg committed Jun 3, 2024
1 parent d6f6d2d commit 4df812e
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 7 deletions.
2 changes: 1 addition & 1 deletion pkg/mutate/squashfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ func (c *squashfsConverter) Uncompressed(l v1.Layer) (io.ReadCloser, error) {
pr, pw := io.Pipe()
go func() {
defer rc.Close()
pw.CloseWithError(whiteoutFilter(rc, pw, opaquePaths))
pw.CloseWithError(whiteoutsToOverlayFS(rc, pw, opaquePaths))
}()
return pr, nil
}
Expand Down
Binary file not shown.
Binary file not shown.
79 changes: 73 additions & 6 deletions pkg/mutate/whiteout.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
const (
aufsWhiteoutPrefix = ".wh."
aufsOpaqueMarker = ".wh..wh..opq"
schilyOpaqueXattr = "SCHILY.xattr.trusted.overlay.opaque"
)

var errUnexpectedOpaque = errors.New("unexpected opaque marker")
Expand Down Expand Up @@ -52,11 +53,11 @@ func scanAUFSWhiteouts(in io.Reader) (map[string]bool, bool, error) {
}
}

// whiteOutFilter streams a tar file from in to out, replacing AUFS whiteout
// markers with OverlayFS whiteout markers. Due to unrestricted ordering of
// markers vs their target, the list of opaquePaths must be obtained prior to
// filtering and provided to this filter.
func whiteoutFilter(in io.Reader, out io.Writer, opaquePaths map[string]bool) error {
// whiteoutsToOverlayFS streams a tar file from in to out, replacing AUFS
// whiteout markers with OverlayFS whiteout markers. Due to unrestricted
// ordering of markers vs their target, the list of opaquePaths must be obtained
// prior to filtering and provided to this filter.
func whiteoutsToOverlayFS(in io.Reader, out io.Writer, opaquePaths map[string]bool) error {
tr := tar.NewReader(in)
tw := tar.NewWriter(out)
defer tw.Close()
Expand Down Expand Up @@ -91,7 +92,7 @@ func whiteoutFilter(in io.Reader, out io.Writer, opaquePaths map[string]bool) er
if header.PAXRecords == nil {
header.PAXRecords = map[string]string{}
}
header.PAXRecords["SCHILY.xattr."+"trusted.overlay.opaque"] = "y"
header.PAXRecords[schilyOpaqueXattr] = "y"
}
// Replace a `.wh.<name>` marker with a char dev 0 at <name>
if strings.HasPrefix(base, aufsWhiteoutPrefix) {
Expand Down Expand Up @@ -119,3 +120,69 @@ func whiteoutFilter(in io.Reader, out io.Writer, opaquePaths map[string]bool) er
}
}
}

// whiteoutsToAUFS streams a tar file from in to out, replacing OverlayFS
// whiteout markers with AUFS whiteout markers.
func whiteoutsToAUFS(in io.Reader, out io.Writer) error {
tr := tar.NewReader(in)
tw := tar.NewWriter(out)
defer tw.Close()

for {
header, err := tr.Next()

if err == io.EOF {
return nil
}
if err != nil {
return err
}

// <dir> with opaque xattr -> write both <dir> & <dir>/.wh..wh..opq
if header.Typeflag == tar.TypeDir && header.PAXRecords[schilyOpaqueXattr] == "y" {
// Write directory entry, without the xattr.
delete(header.PAXRecords, schilyOpaqueXattr)
if err := tw.WriteHeader(header); err != nil {
return err
}
// Write opaque marker file inside the directory.
// Disable gosec 305: File traversal when extracting zip/tar archive.
// We are modifying an existing tar stream. No extraction here.
//nolint:gosec
if err := tw.WriteHeader(&tar.Header{
Typeflag: tar.TypeReg,
Name: filepath.Join(header.Name, aufsOpaqueMarker),
Size: 0,
Mode: 0o600,
Uid: header.Uid,
Gid: header.Gid,
Uname: header.Uname,
Gname: header.Gname,
AccessTime: header.AccessTime,
ChangeTime: header.ChangeTime,
}); err != nil {
return err
}
continue
}

// <file> as 0:0 char dev -> becomes .wh..wh.<file>
if header.Typeflag == tar.TypeChar && header.Devmajor == 0 && header.Devminor == 0 {
header.Typeflag = tar.TypeReg
header.Name = filepath.Join(filepath.Dir(header.Name), aufsWhiteoutPrefix+filepath.Base(header.Name))
header.Size = 0
header.Mode = 0o600
}

if err := tw.WriteHeader(header); err != nil {
return err
}
// Disable gosec G110: Potential DoS vulnerability via decompression bomb.
// We are just filtering a flow directly from tar reader to tar writer - we aren't reading
// into memory beyond the stdlib buffering.
//nolint:gosec
if _, err := io.Copy(tw, tr); err != nil {
return err
}
}
}
56 changes: 56 additions & 0 deletions pkg/mutate/whiteout_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
package mutate

import (
"bytes"
"maps"
"testing"

v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/sebdah/goldie/v2"
)

func Test_scanAUFSOpaque(t *testing.T) {
Expand Down Expand Up @@ -66,3 +68,57 @@ func Test_scanAUFSOpaque(t *testing.T) {
})
}
}

func Test_WhiteoutRoundTrip(t *testing.T) {
// AUFS layer contains a single opaque marker on dir
// [drwxr-xr-x] .
// ├── [drwxr-xr-x] dir
// │   └── [-rw-r--r--] .wh..wh..opq
// └── [-rw-r--r--] .wh.file
layer := testLayer(t, "aufs-docker-v2-manifest", v1.Hash{
Algorithm: "sha256",
Hex: "da55812559dec81445c289c3832cee4a2f725b15aeb258791640185c3126b2bf",
})

g := goldie.New(t,
goldie.WithTestNameForDir(true),
)

// To an OverlayFS layer
// [drwxr-xr-x] .
// ├── [drwxr-xr-x] ./dir (xattr trusted.overlay.opaque=y)
// └── [crw-r--r--] ./file (0,0 character device)
rc, err := layer.Uncompressed()
if err != nil {
t.Fatal(err)
}
defer rc.Close()

opaques, _, err := scanAUFSWhiteouts(rc)
if err != nil {
t.Fatal(err)
}

rc, err = layer.Uncompressed()
if err != nil {
t.Fatal(err)
}
defer rc.Close()

overlayfsTar := bytes.Buffer{}
if err := whiteoutsToOverlayFS(rc, &overlayfsTar, opaques); err != nil {
t.Fatal(err)
}
g.Assert(t, "overlayfs", overlayfsTar.Bytes())

// Back to an AUFS layer
// [drwxr-xr-x] .
// ├── [drwxr-xr-x] dir
// │   └── [-rw-r--r--] .wh..wh..opq
// └── [-rw-r--r--] .wh.file
aufsTar := bytes.Buffer{}
if err := whiteoutsToAUFS(&overlayfsTar, &aufsTar); err != nil {
t.Fatal(err)
}
g.Assert(t, "aufs", aufsTar.Bytes())
}

0 comments on commit 4df812e

Please sign in to comment.