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

feat: default AUFS -> OverlayFS whiteout conversion for squashfs #26

Merged
merged 2 commits into from
Oct 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 64 additions & 5 deletions pkg/mutate/squashfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@ import (
const layerMediaType types.MediaType = "application/vnd.sylabs.image.layer.v1.squashfs"

type squashfsConverter struct {
converter string // Path to converter program.
args []string // Arguments required for converter program.
dir string // Working directory.
converter string // Path to converter program.
args []string // Arguments required for converter program.
dir string // Working directory.
convertWhiteout bool // Convert whiteout markers from AUFS -> OverlayFS
}
dtrudg marked this conversation as resolved.
Show resolved Hide resolved

// SquashfsConverterOpt are used to specify squashfs converter options.
Expand All @@ -45,16 +46,33 @@ func OptSquashfsLayerConverter(converter string) SquashfsConverterOpt {

var errSquashfsConverterNotSupported = errors.New("squashfs converter not supported")

// OptSquashfsSkipWhiteoutConversion is set to skip the default conversion of whiteout /
// opaque markers from AUFS to OverlayFS format.
func OptSquashfsSkipWhiteoutConversion(b bool) SquashfsConverterOpt {
return func(c *squashfsConverter) error {
dtrudg marked this conversation as resolved.
Show resolved Hide resolved
c.convertWhiteout = !b
return nil
}
}

// SquashfsLayer converts the base layer into a layer using the squashfs format. A dir must be
// specified, which is used as a working directory during conversion. The caller is responsible for
// cleaning up dir.
//
// By default, this will attempt to locate a suitable TAR to SquashFS converter such as 'tar2sqfs'
// or `sqfstar` via exec.LookPath. To specify a path to a specific converter program, consider
// using OptSquashfsLayerConverter.
//
// By default, AUFS whiteout markers in the base TAR layer will be converted to OverlayFS whiteout
// markers in the SquashFS layer. This can be disabled, e.g. where it is known that the layer is
// part of a squashed image that will not have any whiteouts, using OptSquashfsSkipWhiteoutConversion.
//
// Note - when whiteout conversion is performed the base layer will be read twice. Callers should
// ensure it is cached, and is not a streaming layer.
func SquashfsLayer(base v1.Layer, dir string, opts ...SquashfsConverterOpt) (v1.Layer, error) {
c := squashfsConverter{
dir: dir,
dir: dir,
convertWhiteout: true,
}

for _, opt := range opts {
Expand Down Expand Up @@ -124,6 +142,47 @@ func (c *squashfsConverter) makeSquashfs(r io.Reader) (string, error) {
return path, nil
}

// Uncompressed returns an io.ReadCloser for the uncompressed layer contents. If
// c.convertWhiteout is true it will convert whiteout markers from AUFS ->
// OverlayFS format. Note that when conversion is performed, the underlying
// layer TAR is read twice.
func (c *squashfsConverter) Uncompressed(l v1.Layer) (io.ReadCloser, error) {
rc, err := l.Uncompressed()
dtrudg marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return nil, err
}

// No conversion - direct tar stream from the layer.
if !c.convertWhiteout {
return rc, nil
}

// Conversion - first, scan for opaque directories and presence of file
// whiteout markers.
opaquePaths, fileWhiteout, err := scanAUFSWhiteouts(rc)
if err != nil {
return nil, err
}
rc.Close()

rc, err = l.Uncompressed()
if err != nil {
return nil, err
}

// Nothing found to filter
if len(opaquePaths) == 0 && !fileWhiteout {
return rc, nil
}

pr, pw := io.Pipe()
go func() {
defer rc.Close()
pw.CloseWithError(whiteoutFilter(rc, pw, opaquePaths))
}()
return pr, nil
}

type squashfsLayer struct {
base v1.Layer
converter *squashfsConverter
Expand Down Expand Up @@ -170,7 +229,7 @@ func (l *squashfsLayer) populate() error {
return nil
}

rc, err := l.base.Uncompressed()
rc, err := l.converter.Uncompressed(l.base)
if err != nil {
return err
}
Expand Down
102 changes: 95 additions & 7 deletions pkg/mutate/squashfs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,18 +49,21 @@ func diffSquashFS(tb testing.TB, pathA, pathB string, diffArgs ...string) {

func Test_SquashfsLayer(t *testing.T) {
tests := []struct {
name string
layer v1.Layer
converter string
diffArgs []string
name string
layer v1.Layer
converter string
noConvertWhiteout bool
diffArgs []string
}{
// HelloWorld layer contains no whiteouts - conversion should have no effect on output.
{
name: "HelloWorldBlob_sqfstar",
layer: testLayer(t, "hello-world-docker-v2-manifest", v1.Hash{
Algorithm: "sha256",
Hex: "7050e35b49f5e348c4809f5eff915842962cb813f32062d3bbdd35c750dd7d01",
}),
converter: "sqfstar",
converter: "sqfstar",
noConvertWhiteout: false,
// Some versions of squashfs-tools do not implement '-root-uid'/'-root-gid', so ignore
// differences in ownership.
diffArgs: []string{"--no-owner"},
Expand All @@ -71,7 +74,89 @@ func Test_SquashfsLayer(t *testing.T) {
Algorithm: "sha256",
Hex: "7050e35b49f5e348c4809f5eff915842962cb813f32062d3bbdd35c750dd7d01",
}),
converter: "tar2sqfs",
converter: "tar2sqfs",
noConvertWhiteout: false,
},
{
name: "HelloWorldBlob_sqfstar_SkipWhiteoutConversion",
layer: testLayer(t, "hello-world-docker-v2-manifest", v1.Hash{
Algorithm: "sha256",
Hex: "7050e35b49f5e348c4809f5eff915842962cb813f32062d3bbdd35c750dd7d01",
}),
converter: "sqfstar",
noConvertWhiteout: true,
// Some versions of squashfs-tools do not implement '-root-uid'/'-root-gid', so ignore
// differences in ownership.
diffArgs: []string{"--no-owner"},
},
{
name: "HelloWorldBlob_tar2sqfs_SkipWhiteoutConversion",
layer: testLayer(t, "hello-world-docker-v2-manifest", v1.Hash{
Algorithm: "sha256",
Hex: "7050e35b49f5e348c4809f5eff915842962cb813f32062d3bbdd35c750dd7d01",
}),
converter: "tar2sqfs",
noConvertWhiteout: true,
},
// AUFS layer contains whiteouts. Should be converted to overlayfs form when noConvertWhiteout = false.
//
// Original (AUFS)
// All regular files.
//
// [drwxr-xr-x] .
// ├── [drwxr-xr-x] dir
// │   └── [-rw-r--r--] .wh..wh..opq
// └── [-rw-r--r--] .wh.file
//
// Converted (OverlayFS)
// .wh.file becomes file as a char 0:0 device
// dir/.wh..wh..opq becomes trusted.overlay.opaque="y" xattr on dir
//
// [drwxr-xr-x] .
// ├── [drwxr-xr-x] dir
// └── [crw-r--r--] file
//
{
name: "AUFSBlob_sqfstar",
layer: testLayer(t, "aufs-docker-v2-manifest", v1.Hash{
Algorithm: "sha256",
Hex: "da55812559dec81445c289c3832cee4a2f725b15aeb258791640185c3126b2bf",
}),
converter: "sqfstar",
noConvertWhiteout: false,
// Some versions of squashfs-tools do not implement '-root-uid'/'-root-gid', so ignore
// differences in ownership.
diffArgs: []string{"--no-owner"},
},
{
name: "AUFSBlob_tar2sqfs",
layer: testLayer(t, "aufs-docker-v2-manifest", v1.Hash{
Algorithm: "sha256",
Hex: "da55812559dec81445c289c3832cee4a2f725b15aeb258791640185c3126b2bf",
}),
converter: "tar2sqfs",
noConvertWhiteout: false,
},
{
name: "AUFSBlob_sqfstar_SkipWhiteoutConversion",
layer: testLayer(t, "aufs-docker-v2-manifest", v1.Hash{
Algorithm: "sha256",
Hex: "da55812559dec81445c289c3832cee4a2f725b15aeb258791640185c3126b2bf",
}),
converter: "sqfstar",
noConvertWhiteout: true,
// Some versions of squashfs-tools do not implement '-root-uid'/'-root-gid', so ignore
// differences in ownership.
diffArgs: []string{"--no-owner"},
},
{
name: "AUFSBlob_tar2sqfs_SkipWhiteoutConversion",
layer: testLayer(t, "aufs-docker-v2-manifest", v1.Hash{
Algorithm: "sha256",
Hex: "da55812559dec81445c289c3832cee4a2f725b15aeb258791640185c3126b2bf",
}),
converter: "tar2sqfs",
noConvertWhiteout: true,
},
}
for _, tt := range tests {
Expand All @@ -82,7 +167,10 @@ func Test_SquashfsLayer(t *testing.T) {
t.Skip(err)
}

l, err := SquashfsLayer(tt.layer, t.TempDir(), OptSquashfsLayerConverter(tt.converter))
l, err := SquashfsLayer(tt.layer, t.TempDir(),
OptSquashfsLayerConverter(tt.converter),
OptSquashfsSkipWhiteoutConversion(tt.noConvertWhiteout),
)
if err != nil {
t.Fatal(err)
}
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
121 changes: 121 additions & 0 deletions pkg/mutate/whiteout.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// Copyright 2023 Sylabs Inc. All rights reserved.
//
// SPDX-License-Identifier: Apache-2.0

package mutate

import (
"archive/tar"
"errors"
"fmt"
"io"
"path/filepath"
"strings"
)

const (
aufsWhiteoutPrefix = ".wh."
aufsOpaqueMarker = ".wh..wh..opq"
)

var errUnexpectedOpaque = errors.New("unexpected opaque marker")

// scanAUFSWhiteouts reads a TAR stream, returning a map of <path>:true for
// directories in the tar that contain an AUFS .wh..wh..opq opaque directory
// marker file, and a boolean indicating the presence of any .wh.<file> markers.
// Note that paths returned are clean, per filepath.Clean.
func scanAUFSWhiteouts(in io.Reader) (map[string]bool, bool, error) {
opaquePaths := map[string]bool{}
fileWhiteout := false

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

if err == io.EOF {
return opaquePaths, fileWhiteout, nil
}
if err != nil {
return nil, false, err
}

base := filepath.Base(header.Name)

if base == aufsOpaqueMarker {
parent := filepath.Dir(header.Name)
opaquePaths[parent] = true
}

if !fileWhiteout && strings.HasPrefix(base, aufsWhiteoutPrefix) {
fileWhiteout = true
}
}
}

// 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 {
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
}

// Must force to PAX format, to accommodate xattrs
header.Format = tar.FormatPAX

clean := filepath.Clean(header.Name)
base := filepath.Base(header.Name)
parent := filepath.Dir(header.Name)

// Don't include .wh..wh..opq opaque directory markers in output.
if base == aufsOpaqueMarker {
// If we don't know the target should be opaque, then provided opaquePaths is incorrect.
if !opaquePaths[parent] {
return fmt.Errorf("%q: %w", parent, errUnexpectedOpaque)
}
continue
}
// Set overlayfs xattr on a dir that was previously found to contain a .wh..wh..opq marker.
if opq := opaquePaths[clean]; opq {
if header.PAXRecords == nil {
header.PAXRecords = map[string]string{}
}
header.PAXRecords["SCHILY.xattr."+"trusted.overlay.opaque"] = "y"
}
// Replace a `.wh.<name>` marker with a char dev 0 at <name>
if strings.HasPrefix(base, aufsWhiteoutPrefix) {
target := filepath.Join(parent, strings.TrimPrefix(base, aufsWhiteoutPrefix))
header.Name = target
header.Typeflag = tar.TypeChar
header.Devmajor = 0
header.Devminor = 0
if err := tw.WriteHeader(header); err != nil {
return err
}
continue
}

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
}
}
}
Loading