From 74caadb0afd7996a291d00f0e78b250568b71696 Mon Sep 17 00:00:00 2001 From: Ramkumar Chinchani Date: Sat, 2 Nov 2024 06:07:40 +0000 Subject: [PATCH] feat: initial commit for erofs support Before this commit, only squashfs was supported. However, there are other filesystems such as erofs that fit the same theme, and additional filesystem support requires refactoring and exposing a more generic filesystem interface. pkg/fs/fs.go - Filesystem interface pkg/squashfs - squashfs pkg/erofs - erofs pkg/common - filesystem-agnostic common routines pkg/verity - verity routines Signed-off-by: Ramkumar Chinchani --- .github/workflows/build.yaml | 2 +- atomfs.go | 1 + cmd/atomfs/mount.go | 13 +- cmd/atomfs/umount.go | 14 +- cmd/atomfs/verify.go | 16 +- go.mod | 18 +- go.sum | 29 +- pkg/common/common_test.go | 49 ++ pkg/common/exclude.go | 84 +++ pkg/common/fs.go | 47 ++ pkg/common/fuse.go | 5 + pkg/common/mount.go | 114 +++ utils.go => pkg/common/utils.go | 2 +- pkg/erofs/erofs.go | 652 ++++++++++++++++++ pkg/erofs/fs.go | 72 ++ pkg/erofs/mediatype.go | 30 + pkg/erofs/superblock.go | 216 ++++++ pkg/erofs/verity.go | 8 + pkg/erofs/verity_test.go | 94 +++ pkg/fs/fs.go | 49 ++ {log => pkg/log}/log.go | 0 molecule.go => pkg/molecule/molecule.go | 40 +- .../molecule/molecule_test.go | 2 +- oci.go => pkg/molecule/oci.go | 4 +- {mount => pkg/mount}/mountinfo.go | 0 {oci => pkg/oci}/oci.go | 0 pkg/squashfs/fs.go | 72 ++ {squashfs => pkg/squashfs}/mediatype.go | 16 +- {squashfs => pkg/squashfs}/squashfs.go | 154 +---- {squashfs => pkg/squashfs}/superblock.go | 0 pkg/squashfs/verity.go | 15 + {squashfs => pkg/squashfs}/verity_test.go | 47 +- pkg/verity/metadata.go | 16 + {squashfs => pkg/verity}/verity.go | 199 +----- {squashfs => pkg/verity}/verity_static.go | 2 +- test/lxc.conf | 46 ++ 36 files changed, 1705 insertions(+), 423 deletions(-) create mode 100644 atomfs.go create mode 100644 pkg/common/common_test.go create mode 100644 pkg/common/exclude.go create mode 100644 pkg/common/fs.go create mode 100644 pkg/common/fuse.go create mode 100644 pkg/common/mount.go rename utils.go => pkg/common/utils.go (98%) create mode 100644 pkg/erofs/erofs.go create mode 100644 pkg/erofs/fs.go create mode 100644 pkg/erofs/mediatype.go create mode 100644 pkg/erofs/superblock.go create mode 100644 pkg/erofs/verity.go create mode 100644 pkg/erofs/verity_test.go create mode 100644 pkg/fs/fs.go rename {log => pkg/log}/log.go (100%) rename molecule.go => pkg/molecule/molecule.go (89%) rename molecule_test.go => pkg/molecule/molecule_test.go (97%) rename oci.go => pkg/molecule/oci.go (96%) rename {mount => pkg/mount}/mountinfo.go (100%) rename {oci => pkg/oci}/oci.go (100%) create mode 100644 pkg/squashfs/fs.go rename {squashfs => pkg/squashfs}/mediatype.go (61%) rename {squashfs => pkg/squashfs}/squashfs.go (84%) rename {squashfs => pkg/squashfs}/superblock.go (100%) create mode 100644 pkg/squashfs/verity.go rename {squashfs => pkg/squashfs}/verity_test.go (74%) create mode 100644 pkg/verity/metadata.go rename {squashfs => pkg/verity}/verity.go (66%) rename {squashfs => pkg/verity}/verity_static.go (94%) create mode 100644 test/lxc.conf diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 7b18cfc..55788cb 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -20,7 +20,7 @@ jobs: sudo apt-get update sudo apt-get install bats fuse3 make libcryptsetup-dev libgpgme-dev \ libcap-dev lxc libdevmapper-dev libacl1-dev libarchive-tools \ - squashfuse squashfs-tools + squashfuse squashfs-tools erofs-utils - name: setup lxc run: | chmod ugo+x $HOME diff --git a/atomfs.go b/atomfs.go new file mode 100644 index 0000000..2769f4a --- /dev/null +++ b/atomfs.go @@ -0,0 +1 @@ +package atomfs diff --git a/cmd/atomfs/mount.go b/cmd/atomfs/mount.go index 4eec79d..ec97a10 100644 --- a/cmd/atomfs/mount.go +++ b/cmd/atomfs/mount.go @@ -9,9 +9,8 @@ import ( "github.com/pkg/errors" "github.com/urfave/cli" - - "machinerun.io/atomfs" - "machinerun.io/atomfs/squashfs" + "machinerun.io/atomfs/pkg/common" + "machinerun.io/atomfs/pkg/molecule" ) var mountCmd = cli.Command{ @@ -51,7 +50,7 @@ func findImage(ctx *cli.Context) (string, string, error) { } ocidir := r[0] tag := r[1] - if !atomfs.PathExists(ocidir) { + if !common.PathExists(ocidir) { return "", "", fmt.Errorf("oci directory %s does not exist: %w", ocidir, mountUsage(ctx.App.Name)) } return ocidir, tag, nil @@ -94,7 +93,7 @@ func doMount(ctx *cli.Context) error { return fmt.Errorf("--persist requires an argument") } } - opts := atomfs.MountOCIOpts{ + opts := molecule.MountOCIOpts{ OCIDir: absOCIDir, Tag: tag, Target: absTarget, @@ -104,7 +103,7 @@ func doMount(ctx *cli.Context) error { MetadataDir: ctx.String("metadir"), // nil here means /run/atomfs } - mol, err := atomfs.BuildMoleculeFromOCI(opts) + mol, err := molecule.BuildMoleculeFromOCI(opts) if err != nil { return errors.Wrapf(err, "couldn't build molecule with opts %+v", opts) } @@ -132,7 +131,7 @@ func amPrivileged() bool { func squashUmount(p string) error { if amPrivileged() { - return squashfs.Umount(p) + return common.Umount(p) } return RunCommand("fusermount", "-u", p) } diff --git a/cmd/atomfs/umount.go b/cmd/atomfs/umount.go index fde7340..4342fe8 100644 --- a/cmd/atomfs/umount.go +++ b/cmd/atomfs/umount.go @@ -7,8 +7,7 @@ import ( "syscall" "github.com/urfave/cli" - "machinerun.io/atomfs" - "machinerun.io/atomfs/mount" + "machinerun.io/atomfs/pkg/common" ) var umountCmd = cli.Command{ @@ -28,11 +27,6 @@ func umountUsage(me string) error { return fmt.Errorf("Usage: %s umount mountpoint", me) } -func isMountpoint(p string) bool { - mounted, err := mount.IsMountpoint(p) - return err == nil && mounted -} - func doUmount(ctx *cli.Context) error { if ctx.NArg() < 1 { return umountUsage(ctx.App.Name) @@ -62,11 +56,11 @@ func doUmount(ctx *cli.Context) error { // $metadir/meta/config.json // TODO: want to know mountnsname for a target mountpoint... not for our current proc??? - mountNSName, err := atomfs.GetMountNSName() + mountNSName, err := common.GetMountNSName() if err != nil { errs = append(errs, fmt.Errorf("Failed to get mount namespace name")) } - metadir := filepath.Join(atomfs.RuntimeDir(ctx.String("metadir")), "meta", mountNSName, atomfs.ReplacePathSeparators(mountpoint)) + metadir := filepath.Join(common.RuntimeDir(ctx.String("metadir")), "meta", mountNSName, common.ReplacePathSeparators(mountpoint)) mountsdir := filepath.Join(metadir, "mounts") mounts, err := os.ReadDir(mountsdir) @@ -77,7 +71,7 @@ func doUmount(ctx *cli.Context) error { for _, m := range mounts { p := filepath.Join(mountsdir, m.Name()) - if !m.IsDir() || !isMountpoint(p) { + if !m.IsDir() || !common.IsMountpoint(p) { continue } diff --git a/cmd/atomfs/verify.go b/cmd/atomfs/verify.go index c587405..212bf4e 100644 --- a/cmd/atomfs/verify.go +++ b/cmd/atomfs/verify.go @@ -6,10 +6,10 @@ import ( "strings" "github.com/urfave/cli" - "machinerun.io/atomfs" - "machinerun.io/atomfs/log" - "machinerun.io/atomfs/mount" - "machinerun.io/atomfs/squashfs" + "machinerun.io/atomfs/pkg/common" + "machinerun.io/atomfs/pkg/log" + "machinerun.io/atomfs/pkg/mount" + "machinerun.io/atomfs/pkg/verity" ) var verifyCmd = cli.Command{ @@ -45,16 +45,16 @@ func doVerify(ctx *cli.Context) error { } } - if !isMountpoint(mountpoint) { + if !common.IsMountpoint(mountpoint) { return fmt.Errorf("%s is not a mountpoint", mountpoint) } - mountNSName, err := atomfs.GetMountNSName() + mountNSName, err := common.GetMountNSName() if err != nil { return err } - metadir := filepath.Join(atomfs.RuntimeDir(ctx.String("metadir")), "meta", mountNSName, atomfs.ReplacePathSeparators(mountpoint)) + metadir := filepath.Join(common.RuntimeDir(ctx.String("metadir")), "meta", mountNSName, common.ReplacePathSeparators(mountpoint)) mountsdir := filepath.Join(metadir, "mounts") mounts, err := mount.ParseMounts("/proc/self/mountinfo") @@ -83,7 +83,7 @@ func doVerify(ctx *cli.Context) error { continue } checkedCount = checkedCount + 1 - err = squashfs.ConfirmExistingVerityDeviceCurrentValidity(m.Source) + err = verity.ConfirmExistingVerityDeviceCurrentValidity(m.Source) if err != nil { fmt.Printf("%s: CORRUPTION FOUND\n", m.Source) allOK = false diff --git a/go.mod b/go.mod index b15ea8c..96aad10 100644 --- a/go.mod +++ b/go.mod @@ -1,17 +1,19 @@ module machinerun.io/atomfs +go 1.21 + require ( github.com/Masterminds/semver/v3 v3.2.1 github.com/apex/log v1.9.0 github.com/freddierice/go-losetup v0.0.0-20220711213114-2a14873012db github.com/martinjungblut/go-cryptsetup v0.0.0-20220520180014-fd0874fd07a6 github.com/opencontainers/go-digest v1.0.0 - github.com/opencontainers/image-spec v1.1.0-rc2 + github.com/opencontainers/image-spec v1.1.0 github.com/opencontainers/umoci v0.4.8-0.20220412065115-12453f247749 github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.8.1 github.com/urfave/cli v1.22.12 - golang.org/x/sys v0.6.0 + golang.org/x/sys v0.26.0 ) require ( @@ -20,20 +22,16 @@ require ( github.com/cyphar/filepath-securejoin v0.2.3 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/docker/go-units v0.5.0 // indirect - github.com/google/go-cmp v0.5.6 // indirect github.com/klauspost/compress v1.15.15 // indirect github.com/klauspost/pgzip v1.2.6-0.20220930104621-17e8dac29df8 // indirect github.com/opencontainers/runc v1.1.4 // indirect - github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417 // indirect + github.com/opencontainers/runtime-spec v1.1.0-rc.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rootless-containers/proto/go-proto v0.0.0-20210921234734-69430b6543fb // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/sirupsen/logrus v1.9.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect github.com/vbatts/go-mtree v0.5.2 // indirect - golang.org/x/crypto v0.5.0 // indirect - golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect - google.golang.org/protobuf v1.28.1 // indirect + golang.org/x/crypto v0.28.0 // indirect + google.golang.org/protobuf v1.33.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) - -go 1.21 diff --git a/go.sum b/go.sum index 375e7bf..75f7909 100644 --- a/go.sum +++ b/go.sum @@ -42,9 +42,8 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= @@ -85,13 +84,14 @@ github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1Cpa github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/opencontainers/image-spec v1.1.0-rc2 h1:2zx/Stx4Wc5pIPDvIxHXvXtQFW/7XWJGmnM7r3wg034= -github.com/opencontainers/image-spec v1.1.0-rc2/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/opencontainers/runc v1.1.1/go.mod h1:Tj1hFw6eFWp/o33uxGf5yF2BX5yz2Z6iptFpuvbbKqc= github.com/opencontainers/runc v1.1.4 h1:nRCz/8sKg6K6jgYAFLDlXzPeITBZJyX28DBVhWD+5dg= github.com/opencontainers/runc v1.1.4/go.mod h1:1J5XiS+vdZ3wCyZybsuxXZWGrgSr8fFJHLXuG2PsnNg= -github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417 h1:3snG66yBm59tKhhSPQrQ/0bCrv1LQbKt40LnUPiUxdc= github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.1.0-rc.1 h1:wHa9jroFfKGQqFHj0I1fMRKLl0pfj+ynAqBxo3v6u9w= +github.com/opencontainers/runtime-spec v1.1.0-rc.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI= github.com/opencontainers/umoci v0.4.8-0.20220412065115-12453f247749 h1:EECxchxtKj3Xb7sl9bS/mZp7FtnF6riC9XDEBO6XXrM= github.com/opencontainers/umoci v0.4.8-0.20220412065115-12453f247749/go.mod h1:+wlU3qzSMNKO4Wq18nhiFzDG/DMRr0/FkL+yrRMj5XM= @@ -112,8 +112,8 @@ github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAm github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= -github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM= github.com/smartystreets/gunit v1.0.0/go.mod h1:qwPWnhz6pn0NnRBP++URONOVyNkPyr4SauJk4cUOwJs= @@ -151,8 +151,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= -golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -174,21 +174,20 @@ golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= -google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= diff --git a/pkg/common/common_test.go b/pkg/common/common_test.go new file mode 100644 index 0000000..2d99a2f --- /dev/null +++ b/pkg/common/common_test.go @@ -0,0 +1,49 @@ +package common + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +type uidmapTestcase struct { + uidmap string + expected bool +} + +var uidmapTests = []uidmapTestcase{ + { + uidmap: ` 0 0 4294967295`, + expected: true, + }, + { + uidmap: ` 0 0 1000 +2000 2000 1`, + expected: false, + }, + { + uidmap: ` 0 0 1000`, + expected: false, + }, + { + uidmap: ` 10 0 4294967295`, + expected: false, + }, + { + uidmap: ` 0 10 4294967295`, + expected: false, + }, + { + uidmap: ` 0 0 1`, + expected: false, + }, +} + +func TestAmHostRoot(t *testing.T) { + t.Parallel() + assert := assert.New(t) + for _, testcase := range uidmapTests { + v := uidmapIsHost(testcase.uidmap) + assert.Equal(v, testcase.expected) + } +} diff --git a/pkg/common/exclude.go b/pkg/common/exclude.go new file mode 100644 index 0000000..afc4503 --- /dev/null +++ b/pkg/common/exclude.go @@ -0,0 +1,84 @@ +package common + +import ( + "bytes" + "path" + "path/filepath" + "strings" +) + +// ExcludePaths represents a list of paths to exclude in a filesystem listing. +// Users should do something like filepath.Walk() over the whole filesystem, +// calling AddExclude() or AddInclude() based on whether they want to include +// or exclude a particular file. Note that if e.g. /usr is excluded, then +// everyting underneath is also implicitly excluded. The +// AddExclude()/AddInclude() methods do the math to figure out what is the +// correct set of things to exclude or include based on what paths have been +// previously included or excluded. +type ExcludePaths struct { + exclude map[string]bool + include []string +} + +func NewExcludePaths() *ExcludePaths { + return &ExcludePaths{ + exclude: map[string]bool{}, + include: []string{}, + } +} + +func (eps *ExcludePaths) AddExclude(p string) { + for _, inc := range eps.include { + // If /usr/bin/ls has changed but /usr hasn't, we don't want to list + // /usr in the include paths any more, so let's be sure to only + // add things which aren't prefixes. + if strings.HasPrefix(inc, p) { + return + } + } + eps.exclude[p] = true +} + +func (eps *ExcludePaths) AddInclude(orig string, isDir bool) { + // First, remove this thing and all its parents from exclude. + p := orig + + // normalize to the first dir + if !isDir { + p = path.Dir(p) + } + for { + // our paths are all absolute, so this is a base case + if p == "/" { + break + } + + delete(eps.exclude, p) + p = filepath.Dir(p) + } + + // now add it to the list of includes, so we don't accidentally re-add + // anything above. + eps.include = append(eps.include, orig) +} + +func (eps *ExcludePaths) String() (string, error) { + var buf bytes.Buffer + for p := range eps.exclude { + _, err := buf.WriteString(p) + if err != nil { + return "", err + } + _, err = buf.WriteString("\n") + if err != nil { + return "", err + } + } + + _, err := buf.WriteString("\n") + if err != nil { + return "", err + } + + return buf.String(), nil +} diff --git a/pkg/common/fs.go b/pkg/common/fs.go new file mode 100644 index 0000000..0d98b5f --- /dev/null +++ b/pkg/common/fs.go @@ -0,0 +1,47 @@ +package common + +import ( + "os" + "strings" +) + +func FileChanged(a os.FileInfo, path string) bool { + b, err := os.Lstat(path) + if err != nil { + return true + } + return !os.SameFile(a, b) +} + +// Takes /proc/self/uid_map contents as one string +// Returns true if this is a uidmap representing the whole host +// uid range. +func uidmapIsHost(oneline string) bool { + oneline = strings.TrimSuffix(oneline, "\n") + if len(oneline) == 0 { + return false + } + lines := strings.Split(oneline, "\n") + if len(lines) != 1 { + return false + } + words := strings.Fields(lines[0]) + if len(words) != 3 || words[0] != "0" || words[1] != "0" || words[2] != "4294967295" { + return false + } + + return true +} + +func AmHostRoot() bool { + // if not uid 0, not host root + if os.Geteuid() != 0 { + return false + } + // if uid_map doesn't map 0 to 0, not host root + bytes, err := os.ReadFile("/proc/self/uid_map") + if err != nil { + return false + } + return uidmapIsHost(string(bytes)) +} diff --git a/pkg/common/fuse.go b/pkg/common/fuse.go new file mode 100644 index 0000000..034bbbf --- /dev/null +++ b/pkg/common/fuse.go @@ -0,0 +1,5 @@ +package common + +import "os/exec" + +type FuseCmd func(fsImgFile, extractDir string) (*exec.Cmd, error) diff --git a/pkg/common/mount.go b/pkg/common/mount.go new file mode 100644 index 0000000..33e9a55 --- /dev/null +++ b/pkg/common/mount.go @@ -0,0 +1,114 @@ +package common + +import ( + "os" + "path/filepath" + "strings" + + "github.com/pkg/errors" + "golang.org/x/sys/unix" + "machinerun.io/atomfs/pkg/mount" + "machinerun.io/atomfs/pkg/verity" +) + +func HostMount(fsImgFile string, fsType string, mountpoint string, rootHash string, veritySize int64, verityOffset uint64) error { + return verity.VerityHostMount(fsImgFile, fsType, mountpoint, rootHash, veritySize, verityOffset) +} + +// Mount a filesystem as container root, without host root +// privileges. We do this using fuse "cmd" which is passed in from actual filesystem backends. +func GuestMount(fsImgFile string, mountpoint string, fuseCmd FuseCmd) error { + if IsMountpoint(mountpoint) { + return errors.Errorf("%s is already mounted", mountpoint) + } + + abs, err := filepath.Abs(fsImgFile) + if err != nil { + return errors.Errorf("Failed to get absolute path for %s: %v", fsImgFile, err) + } + fsImgFile = abs + + abs, err = filepath.Abs(mountpoint) + if err != nil { + return errors.Errorf("Failed to get absolute path for %s: %v", mountpoint, err) + } + mountpoint = abs + + cmd, err := fuseCmd(fsImgFile, mountpoint) + if err != nil { + return err + } + + if err := cmd.Process.Release(); err != nil { + return errors.Errorf("Failed to release process after guestmount %s: %v", fsImgFile, err) + } + return nil +} + +func Umount(mountpoint string) error { + mounts, err := mount.ParseMounts("/proc/self/mountinfo") + if err != nil { + return err + } + + // first, find the verity device that backs the mount + theMount, found := mounts.FindMount(mountpoint) + if !found { + return errors.Errorf("%s is not a mountpoint", mountpoint) + } + + err = unix.Unmount(mountpoint, 0) + if err != nil { + return errors.Wrapf(err, "failed unmounting %v", mountpoint) + } + + if _, err := os.Stat(theMount.Source); err != nil { + if os.IsNotExist(err) { + return nil + } + return errors.WithStack(err) + } + + // was this a verity mount or a regular loopback mount? (if it's a + // regular loopback mount, we detached it above, so need to do anything + // special here; verity doesn't play as nicely) + if strings.HasSuffix(theMount.Source, verity.VeritySuffix) { + err = verity.VerityUnmount(theMount.Source) + if err != nil { + return errors.Wrapf(err, "failed verity-unmounting %v", theMount.Source) + } + } + + return nil +} + +func IsMountpoint(dest string) bool { + mounted, err := mount.IsMountpoint(dest) + return err == nil && mounted +} + +func IsMountedAtDir(_, dest string) (bool, error) { + dstat, err := os.Stat(dest) + if os.IsNotExist(err) { + return false, nil + } + if !dstat.IsDir() { + return false, nil + } + mounts, err := mount.ParseMounts("/proc/self/mountinfo") + if err != nil { + return false, err + } + + fdest, err := filepath.Abs(dest) + if err != nil { + return false, err + } + for _, m := range mounts { + if m.Target == fdest { + return true, nil + } + } + + return false, nil +} diff --git a/utils.go b/pkg/common/utils.go similarity index 98% rename from utils.go rename to pkg/common/utils.go index 3788018..324eff9 100644 --- a/utils.go +++ b/pkg/common/utils.go @@ -1,4 +1,4 @@ -package atomfs +package common import ( "fmt" diff --git a/pkg/erofs/erofs.go b/pkg/erofs/erofs.go new file mode 100644 index 0000000..191abae --- /dev/null +++ b/pkg/erofs/erofs.go @@ -0,0 +1,652 @@ +// This package is a small go "library" (read: exec wrapper) around the +// mkfs.erofs binary that provides some useful primitives. +package erofs + +import ( + "bytes" + "fmt" + "io" + "os" + "os/exec" + "path" + "path/filepath" + "runtime" + "strings" + "sync" + "syscall" + "time" + + "github.com/pkg/errors" + "golang.org/x/sys/unix" + "machinerun.io/atomfs/pkg/common" + "machinerun.io/atomfs/pkg/log" + vrty "machinerun.io/atomfs/pkg/verity" +) + +type erofsFuseInfoStruct struct { + Path string + Version string + SupportsNotify bool +} + +var once sync.Once +var erofsFuseInfo = erofsFuseInfoStruct{"", "", false} + +func MakeErofs(tempdir string, rootfs string, eps *common.ExcludePaths, verity vrty.VerityMetadata) (io.ReadCloser, string, string, error) { + var excludesFile string + var err error + var toExclude string + var rootHash string + + if eps != nil { + toExclude, err = eps.String() + if err != nil { + return nil, "", rootHash, errors.Wrapf(err, "couldn't create exclude path list") + } + } + + if len(toExclude) != 0 { + excludes, err := os.CreateTemp(tempdir, "stacker-erofs-exclude-") + if err != nil { + return nil, "", rootHash, err + } + defer os.Remove(excludes.Name()) + + excludesFile = excludes.Name() + _, err = excludes.WriteString(toExclude) + excludes.Close() + if err != nil { + return nil, "", rootHash, err + } + } + + tmpErofs, err := os.CreateTemp(tempdir, "stacker-erofs-img-") + if err != nil { + return nil, "", rootHash, err + } + // the following achieves the effect of creating a temporary file name + // without actually creating the file;the goal being to provide a temporary + // filename to provide to `mkfs.XXX` tool so we have a predictable name to + // consume after `mkfs.XXX` has completed its task. + // + // NB: there's a TOCTOU here; something else can predict and produce + // output in the tempfile name we created after we delete it and before + // `mkfs.XXX` runs. + tmpErofs.Close() + os.Remove(tmpErofs.Name()) + + defer os.Remove(tmpErofs.Name()) + + args := []string{tmpErofs.Name(), rootfs} + compression := LZ4HCCompression + zstdOk, parallelOk := mkerofsSupportsFeature() + if zstdOk { + args = append(args, "-z", "zstd") + compression = ZstdCompression + } + if parallelOk { + args = append(args, "--workers", fmt.Sprintf("%d", runtime.NumCPU())) + } + if len(toExclude) != 0 { + args = append(args, "--exclude-path", excludesFile) + } + cmd := exec.Command("mkfs.erofs", args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err = cmd.Run(); err != nil { + return nil, "", rootHash, errors.Wrap(err, "couldn't build erofs") + } + + if verity { + rootHash, err = vrty.AppendVerityData(tmpErofs.Name()) + if err != nil { + return nil, "", rootHash, err + } + } + + blob, err := os.Open(tmpErofs.Name()) + if err != nil { + return nil, "", rootHash, errors.WithStack(err) + } + + return blob, GenerateErofsMediaType(compression, verity), rootHash, nil +} + +func findErofsFuseInfo() { + var erofsPath string + if p := which("erofsfuse"); p != "" { + erofsPath = p + } else { + erofsPath = which("erofsfuse") + } + if erofsPath == "" { + return + } + version, supportsNotify := erofsfuseSupportsMountNotification(erofsPath) + log.Infof("Found erofsfuse at %s (version=%s notify=%t)", erofsPath, version, supportsNotify) + erofsFuseInfo = erofsFuseInfoStruct{erofsPath, version, supportsNotify} +} + +// erofsfuseSupportsMountNotification - returns true if erofsfuse supports mount +// notification, false otherwise +// erofsfuse is the path to the erofsfuse binary +func erofsfuseSupportsMountNotification(erofsfuse string) (string, bool) { + cmd := exec.Command(erofsfuse) + + // `erofsfuse` always returns an error... so we ignore it. + out, _ := cmd.CombinedOutput() + + firstLine := strings.Split(string(out[:]), "\n")[0] + version := strings.Split(firstLine, " ")[1] + + return version, false +} + +var erofsNotFound = errors.Errorf("erofsfuse program not found") + +// erofsFuse - mount erofsFile to extractDir +// return a pointer to the erofsfuse cmd. +// The caller of the this is responsible for the process created. +func erofsFuse(erofsFile, extractDir string) (*exec.Cmd, error) { + var cmd *exec.Cmd + + once.Do(findErofsFuseInfo) + if erofsFuseInfo.Path == "" { + return cmd, erofsNotFound + } + + notifyOpts := "" + notifyPath := "" + if erofsFuseInfo.SupportsNotify { + sockdir, err := os.MkdirTemp("", "sock") + if err != nil { + return cmd, err + } + defer os.RemoveAll(sockdir) + notifyPath = filepath.Join(sockdir, "notifypipe") + if err := syscall.Mkfifo(notifyPath, 0640); err != nil { + return cmd, err + } + notifyOpts = "notify_pipe=" + notifyPath + } + + // given extractDir of path/to/some/dir[/], log to path/to/some/.dir-erofs.log + extractDir = strings.TrimSuffix(extractDir, "/") + + var cmdOut io.Writer + var err error + + logf := filepath.Join(path.Dir(extractDir), "."+filepath.Base(extractDir)+"-erofsfuse.log") + if cmdOut, err = os.OpenFile(logf, os.O_RDWR|os.O_TRUNC|os.O_CREATE, 0644); err != nil { + log.Infof("Failed to open %s for write: %v", logf, err) + return cmd, err + } + + fiPre, err := os.Lstat(extractDir) + if err != nil { + return cmd, errors.Wrapf(err, "Failed stat'ing %q", extractDir) + } + if fiPre.Mode()&os.ModeSymlink != 0 { + return cmd, errors.Errorf("Refusing to mount onto a symbolic linkd") + } + + // It would be nice to only enable debug (or maybe to only log to file at all) + // if 'stacker --debug', but we do not have access to that info here. + // to debug erofsfuse, use "allow_other,debug" + optionArgs := "debug" + if notifyOpts != "" { + optionArgs += "," + notifyOpts + } + cmd = exec.Command(erofsFuseInfo.Path, "-f", "-o", optionArgs, erofsFile, extractDir) + cmd.Stdin = nil + cmd.Stdout = cmdOut + cmd.Stderr = cmdOut + cmdOut.Write([]byte(fmt.Sprintf("# %s\n", strings.Join(cmd.Args, " ")))) + if err != nil { + return cmd, errors.Wrapf(err, "Failed writing to %s", logf) + } + log.Debugf("Extracting %s -> %s with %s [%s]", erofsFile, extractDir, erofsFuseInfo.Path, logf) + err = cmd.Start() + if err != nil { + return cmd, err + } + + // now poll/wait for one of 3 things to happen + // a. child process exits - if it did, then some error has occurred. + // b. the directory Entry is different than it was before the call + // to erofsfuse. We have to do this because we do not have another + // way to know when the mount has been populated. + // https://github.com/vasi/erofsfuse/issues/49 + // c. a timeout (timeLimit) was hit + startTime := time.Now() + timeLimit := 30 * time.Second + alarmCh := make(chan struct{}) + go func() { + _ = cmd.Wait() + close(alarmCh) + }() + if erofsFuseInfo.SupportsNotify { + notifyCh := make(chan byte) + log.Infof("%s supports notify pipe, watching %q", erofsFuseInfo.Path, notifyPath) + go func() { + f, err := os.Open(notifyPath) + if err != nil { + return + } + defer f.Close() + b1 := make([]byte, 1) + for { + n1, err := f.Read(b1) + if err != nil { + return + } + if err == nil && n1 >= 1 { + break + } + } + notifyCh <- b1[0] + }() + + select { + case <-alarmCh: + cmd.Process.Kill() + return cmd, errors.Wrapf(err, "Gave up on erofsfuse mount of %s with %s after %s", erofsFile, erofsFuseInfo.Path, timeLimit) + case ret := <-notifyCh: + if ret == 's' { + return cmd, nil + } else { + return cmd, errors.Errorf("erofsfuse returned an error, check %s", logf) + } + } + } + for count := 0; !common.FileChanged(fiPre, extractDir); count++ { + if cmd.ProcessState != nil { + // process exited, the Wait() call in the goroutine above + // caused ProcessState to be populated. + return cmd, errors.Errorf("erofsfuse mount of %s with %s exited unexpectedly with %d", erofsFile, erofsFuseInfo.Path, cmd.ProcessState.ExitCode()) + } + if time.Since(startTime) > timeLimit { + cmd.Process.Kill() + return cmd, errors.Wrapf(err, "Gave up on erofsfuse mount of %s with %s after %s", erofsFile, erofsFuseInfo.Path, timeLimit) + } + if count%10 == 1 { + log.Debugf("%s is not yet mounted...(%s)", extractDir, time.Since(startTime)) + } + time.Sleep(time.Duration(50 * time.Millisecond)) + } + + return cmd, nil +} + +type ExtractPolicy struct { + Extractors []ErofsExtractor + Extractor ErofsExtractor + Excuses map[string]error + initialized bool + mutex sync.Mutex +} + +var exPolInfo struct { + once sync.Once + err error + policy *ExtractPolicy +} + +type ErofsExtractor interface { + Name() string + IsAvailable() error + // Mount - Mount or extract path to dest. + // Return nil on "already extracted" + // Return error on failure. + Mount(path, dest string) error +} + +func NewExtractPolicy(args ...string) (*ExtractPolicy, error) { + p := &ExtractPolicy{ + Extractors: []ErofsExtractor{}, + Excuses: map[string]error{}, + } + + allEx := []ErofsExtractor{ + &KernelExtractor{}, + &ErofsFuseExtractor{}, + &FsckErofsExtractor{}, + } + byName := map[string]ErofsExtractor{} + for _, i := range allEx { + byName[i.Name()] = i + } + + for _, i := range args { + extractor, ok := byName[i] + if !ok { + return nil, errors.Errorf("Unknown extractor: '%s'", i) + } + excuse := extractor.IsAvailable() + if excuse != nil { + p.Excuses[i] = excuse + continue + } + p.Extractors = append(p.Extractors, extractor) + } + return p, nil +} + +type FsckErofsExtractor struct { + mutex sync.Mutex +} + +func (k *FsckErofsExtractor) Name() string { + return "fsck.erofs" +} + +func (k *FsckErofsExtractor) IsAvailable() error { + if which("fsck.erofs") == "" { + return errors.Errorf("no 'fsck.erofs' in PATH") + } + return nil +} + +func (k *FsckErofsExtractor) Mount(squashFile, extractDir string) error { + k.mutex.Lock() + defer k.mutex.Unlock() + + // check if already extracted + empty, err := isEmptyDir(extractDir) + if err != nil { + return errors.Wrapf(err, "Error checking for empty dir") + } + if !empty { + return nil + } + + log.Debugf("fsck.erofs %s -> %s", squashFile, extractDir) + cmd := exec.Command("fsck.erofs", "-d", "--extract", extractDir, squashFile) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Stdin = nil + err = cmd.Run() + + // on failure, remove the directory + if err != nil { + if rmErr := os.RemoveAll(extractDir); rmErr != nil { + log.Errorf("Failed to remove %s after failed extraction of %s: %v", extractDir, squashFile, rmErr) + } + return err + } + + // assert that extraction must create files. This way we can assume non-empty dir above + // was populated by fsck.erofs. + empty, err = isEmptyDir(extractDir) + if err != nil { + return errors.Errorf("Failed to read %s after successful extraction of %s: %v", + extractDir, squashFile, err) + } + if empty { + return errors.Errorf("%s was an empty fs image", squashFile) + } + + return nil +} + +type KernelExtractor struct { + mutex sync.Mutex +} + +func (k *KernelExtractor) Name() string { + return "kmount" +} + +func (k *KernelExtractor) IsAvailable() error { + if !common.AmHostRoot() { + return errors.Errorf("not host root") + } + return nil +} + +func (k *KernelExtractor) Mount(squashFile, extractDir string) error { + k.mutex.Lock() + defer k.mutex.Unlock() + + if mounted, err := common.IsMountedAtDir(squashFile, extractDir); err != nil { + return err + } else if mounted { + return nil + } + + ecmd := []string{"mount", "-terofs", "-oloop,ro", squashFile, extractDir} + var output bytes.Buffer + cmd := exec.Command(ecmd[0], ecmd[1:]...) + cmd.Stdin = nil + cmd.Stdout = &output + cmd.Stderr = cmd.Stdout + err := cmd.Run() + if err == nil { + return nil + } + + var retErr error + + exitError, ok := err.(*exec.ExitError) + if !ok { + retErr = errors.Errorf("kmount(%s) had unexpected error (no-rc), in exec (%v): %v", + squashFile, ecmd, err) + } else if status, ok := exitError.Sys().(syscall.WaitStatus); !ok { + retErr = errors.Errorf("kmount(%s) had unexpected error (no-status), in exec (%v): %v", + squashFile, ecmd, err) + } else { + retErr = errors.Errorf("kmount(%s) exited %d: %v", squashFile, status.ExitStatus(), output.String()) + } + + return retErr +} + +type ErofsFuseExtractor struct { + mutex sync.Mutex +} + +func (k *ErofsFuseExtractor) Name() string { + return "erofsfuse" +} + +func (k *ErofsFuseExtractor) IsAvailable() error { + once.Do(findErofsFuseInfo) + if erofsFuseInfo.Path == "" { + return errors.Errorf("no 'erofsfuse' in PATH") + } + return nil +} + +func (k *ErofsFuseExtractor) Mount(erofsFile, extractDir string) error { + k.mutex.Lock() + defer k.mutex.Unlock() + + if mounted, err := common.IsMountedAtDir(erofsFile, extractDir); mounted && err == nil { + log.Debugf("[%s] %s already mounted -> %s", k.Name(), erofsFile, extractDir) + return nil + } else if err != nil { + return err + } + + cmd, err := erofsFuse(erofsFile, extractDir) + if err != nil { + return err + } + + log.Debugf("erofsfuse mounted (%d) %s -> %s", cmd.Process.Pid, erofsFile, extractDir) + if err := cmd.Process.Release(); err != nil { + return errors.Errorf("Failed to release process %s: %v", cmd, err) + } + return nil +} + +// ExtractSingleErofsPolicy - extract squashfile to extractDir +func ExtractSingleErofsPolicy(squashFile, extractDir string, policy *ExtractPolicy) error { + const initName = "init" + if policy == nil { + return errors.Errorf("policy cannot be nil") + } + + // avoid taking a lock if already initialized (possibly premature optimization) + if !policy.initialized { + policy.mutex.Lock() + // We may have been waiting on the initializer. If so, then the policy will now be initialized. + // if not, then we are the initializer. + if !policy.initialized { + defer policy.mutex.Unlock() + defer func() { + policy.initialized = true + }() + } else { + policy.mutex.Unlock() + } + } + + err := os.MkdirAll(extractDir, 0755) + if err != nil { + return err + } + + fdest, err := filepath.Abs(extractDir) + if err != nil { + return err + } + + if policy.initialized { + if err, ok := policy.Excuses[initName]; ok { + return err + } + return policy.Extractor.Mount(squashFile, fdest) + } + + // At this point we are the initialzer + if policy.Excuses == nil { + policy.Excuses = map[string]error{} + } + + if len(policy.Extractors) == 0 { + policy.Excuses[initName] = errors.Errorf("policy had no extractors") + return policy.Excuses[initName] + } + + var extractor ErofsExtractor + allExcuses := []string{} + for _, extractor = range policy.Extractors { + err = extractor.Mount(squashFile, fdest) + if err == nil { + policy.Extractor = extractor + log.Debugf("Selected erofs extractor %s", extractor.Name()) + return nil + } + policy.Excuses[extractor.Name()] = err + } + + for n, exc := range policy.Excuses { + allExcuses = append(allExcuses, fmt.Sprintf("%s: %v", n, exc)) + } + + // nothing worked. populate Excuses[initName] + policy.Excuses[initName] = errors.Errorf("No suitable extractor found:\n %s", strings.Join(allExcuses, "\n ")) + return policy.Excuses[initName] +} + +// ExtractSingleErofs - extract the squashFile to extractDir +// Initialize a extractPolicy struct and then call ExtractSingleErofsPolicy +// wik()th that. +func ExtractSingleErofs(squashFile string, extractDir string) error { + exPolInfo.once.Do(func() { + const envName = "STACKER_EROFS_EXTRACT_POLICY" + const defPolicy = "kmount erofsfuse fsc.erofs" + val := os.Getenv(envName) + if val == "" { + val = defPolicy + } + exPolInfo.policy, exPolInfo.err = NewExtractPolicy(strings.Fields(val)...) + if exPolInfo.err == nil { + for k, v := range exPolInfo.policy.Excuses { + log.Debugf(" erofs extractor %s is not available: %v", k, v) + } + } + }) + + if exPolInfo.err != nil { + return exPolInfo.err + } + + return ExtractSingleErofsPolicy(squashFile, extractDir, exPolInfo.policy) +} + +var checkSupported sync.Once +var zstdIsSuspported bool +var parallelIsSupported bool + +func mkerofsSupportsFeature() (bool, bool) { + checkSupported.Do(func() { + var stdoutBuffer strings.Builder + var stderrBuffer strings.Builder + + cmd := exec.Command("mkfs.erofs", "--help") + cmd.Stdout = &stdoutBuffer + cmd.Stderr = &stderrBuffer + + // Ignore errs here as `mkerofs --help` exit status code is 1 + _ = cmd.Run() + + if strings.Contains(stdoutBuffer.String(), "zstd") || + strings.Contains(stderrBuffer.String(), "zstd") { + zstdIsSuspported = true + } + + if strings.Contains(stdoutBuffer.String(), "workers") || + strings.Contains(stderrBuffer.String(), "workers") { + parallelIsSupported = true + } + }) + + return zstdIsSuspported, parallelIsSupported +} + +func isEmptyDir(path string) (bool, error) { + fh, err := os.Open(path) + if err != nil { + return false, err + } + + _, err = fh.ReadDir(1) + if err == io.EOF { + return true, nil + } + return false, err +} + +// which - like the unix utility, return empty string for not-found. +// this might fit well in lib/, but currently lib's test imports +// erofs creating a import loop. +func which(name string) string { + return whichSearch(name, strings.Split(os.Getenv("PATH"), ":")) +} + +func whichSearch(name string, paths []string) string { + var search []string + + if strings.ContainsRune(name, os.PathSeparator) { + if filepath.IsAbs(name) { + search = []string{name} + } else { + search = []string{"./" + name} + } + } else { + search = []string{} + for _, p := range paths { + search = append(search, filepath.Join(p, name)) + } + } + + for _, fPath := range search { + if err := unix.Access(fPath, unix.X_OK); err == nil { + return fPath + } + } + + return "" +} diff --git a/pkg/erofs/fs.go b/pkg/erofs/fs.go new file mode 100644 index 0000000..8f39df4 --- /dev/null +++ b/pkg/erofs/fs.go @@ -0,0 +1,72 @@ +package erofs + +import ( + "io" + "os" + + "github.com/pkg/errors" + "machinerun.io/atomfs/pkg/common" + "machinerun.io/atomfs/pkg/verity" +) + +type erofs struct { +} + +func New() *erofs { + return &erofs{} +} + +func (er *erofs) Make(tempdir string, rootfs string, eps *common.ExcludePaths, verity verity.VerityMetadata) (io.ReadCloser, string, string, error) { + return MakeErofs(tempdir, rootfs, eps, verity) +} + +func (er *erofs) ExtractSingle(fsImgFile string, extractDir string) error { + return ExtractSingleErofs(fsImgFile, extractDir) +} + +func (er *erofs) Mount(fsImgFile, mountpoint, rootHash string) error { + if !common.AmHostRoot() { + return er.guestMount(fsImgFile, mountpoint) + } + err := er.hostMount(fsImgFile, mountpoint, rootHash) + if err == nil || rootHash != "" { + return err + } + return er.guestMount(fsImgFile, mountpoint) +} + +func fsImgVerityLocation(fsImgFile string) (int64, uint64, error) { + fi, err := os.Stat(fsImgFile) + if err != nil { + return -1, 0, errors.WithStack(err) + } + + sblock, err := readSuperblock(fsImgFile) + if err != nil { + return -1, 0, err + } + + verityOffset, err := verityDataLocation(sblock) + if err != nil { + return -1, 0, err + } + + return fi.Size(), verityOffset, nil +} + +func (er *erofs) hostMount(fsImgFile string, mountpoint string, rootHash string) error { + veritySize, verityOffset, err := fsImgVerityLocation(fsImgFile) + if err != nil { + return err + } + + return common.HostMount(fsImgFile, "erofs", mountpoint, rootHash, veritySize, verityOffset) +} + +func (er *erofs) guestMount(fsImgFile string, mountpoint string) error { + return common.GuestMount(fsImgFile, mountpoint, erofsFuse) +} + +func (er *erofs) Umount(mountpoint string) error { + return common.Umount(mountpoint) +} diff --git a/pkg/erofs/mediatype.go b/pkg/erofs/mediatype.go new file mode 100644 index 0000000..e6975a1 --- /dev/null +++ b/pkg/erofs/mediatype.go @@ -0,0 +1,30 @@ +package erofs + +import ( + "fmt" + "strings" + + vrty "machinerun.io/atomfs/pkg/verity" +) + +type ErofsCompression string + +const ( + BaseMediaTypeLayerErofs = "application/vnd.stacker.image.layer.erofs" + + LZ4HCCompression ErofsCompression = "lz4hc" + LZ4Compression ErofsCompression = "lz4" + ZstdCompression ErofsCompression = "zstd" +) + +func IsErofsMediaType(mediaType string) bool { + return strings.HasPrefix(mediaType, BaseMediaTypeLayerErofs) +} + +func GenerateErofsMediaType(comp ErofsCompression, verity vrty.VerityMetadata) string { + verityString := "" + if verity { + verityString = fmt.Sprintf("+%s", vrty.VeritySuffix) + } + return fmt.Sprintf("%s+%s%s", BaseMediaTypeLayerErofs, comp, verityString) +} diff --git a/pkg/erofs/superblock.go b/pkg/erofs/superblock.go new file mode 100644 index 0000000..0736e3f --- /dev/null +++ b/pkg/erofs/superblock.go @@ -0,0 +1,216 @@ +package erofs + +import ( + "encoding/binary" + "fmt" + "hash/crc32" + "io" + "os" + + "github.com/pkg/errors" +) + +/* + +https://docs.kernel.org/filesystems/erofs.html + +On-disk details + + |-> aligned with the block size + ____________________________________________________________ +| |SB| | ... | Metadata | ... | Data | Metadata | ... | Data | +|_|__|_|_____|__________|_____|______|__________|_____|______| +0 +1K + +*/ + +const ( + // Definitions for superblock. + superblockMagicV1 = 0xe0f5e1e2 + superblockMagic = superblockMagicV1 + superblockOffset = 1024 + blockSize = 4096 + + // Inode slot size in bit shift. + InodeSlotBits = 5 + + // Max file name length. + MaxNameLen = 255 +) + +// Bit definitions for Inode*::Format. +const ( + InodeLayoutBit = 0 + InodeLayoutBits = 1 + + InodeDataLayoutBit = 1 + InodeDataLayoutBits = 3 +) + +// Inode layouts. +const ( + InodeLayoutCompact = 0 + InodeLayoutExtended = 1 +) + +// Inode data layouts. +const ( + InodeDataLayoutFlatPlain = iota + InodeDataLayoutFlatCompressionLegacy + InodeDataLayoutFlatInline + InodeDataLayoutFlatCompression + InodeDataLayoutChunkBased + InodeDataLayoutMax +) + +// Features w/ backward compatibility. +// This is not exhaustive, unused features are not listed. +const ( + FeatureCompatSuperBlockChecksum = 0x00000001 +) + +// Features w/o backward compatibility. +// +// Any features that aren't in FeatureIncompatSupported are incompatible +// with this implementation. +// +// This is not exhaustive, unused features are not listed. +const ( + FeatureIncompatSupported = 0x0 +) + +// Sizes of on-disk structures in bytes. +const ( + superblockSize = 128 + InodeCompactSize = 32 + InodeExtendedSize = 64 + DirentSize = 12 +) + +type superblock struct { + Magic uint32 + Checksum uint32 + FeatureCompat uint32 + BlockSizeBits uint8 + ExtSlots uint8 + RootNid uint16 + Inodes uint64 + BuildTime uint64 + BuildTimeNsec uint32 + Blocks uint32 + MetaBlockAddr uint32 + XattrBlockAddr uint32 + UUID [16]uint8 + VolumeName [16]uint8 + FeatureIncompat uint32 + Union1 uint16 + ExtraDevices uint16 + DevTableSlotOff uint16 + Reserved [38]uint8 +} + +func verifyChecksum(sb *superblock, sbBlock []byte) error { + if sb.FeatureCompat&FeatureCompatSuperBlockChecksum == 0 { + return nil + } + + sbsum := sb.Checksum + + // zero out Checksum field + sbBlock[superblockOffset+4] = 0 + sbBlock[superblockOffset+5] = 0 + sbBlock[superblockOffset+6] = 0 + sbBlock[superblockOffset+7] = 0 + + table := crc32.MakeTable(crc32.Castagnoli) + + checksum := crc32.Checksum(sbBlock[superblockOffset:superblockOffset+superblockSize], table) + checksum = ^crc32.Update(checksum, table, sbBlock[superblockOffset+superblockSize:]) + if checksum != sbsum { + return fmt.Errorf("invalid checksum: 0x%x, expected: 0x%x", checksum, sbsum) + } + + sb.Checksum = sbsum + + return nil +} + +func parseSuperblock(b []byte) (*superblock, error) { + if len(b) != superblockSize { + return nil, errors.Errorf("superblock had %d bytes instead of expected %d", len(b), superblockSize) + } + + magic := binary.LittleEndian.Uint32(b[0:4]) + if magic != superblockMagic { + return nil, errors.Errorf("superblock had magic of %d instead of expected %d", magic, superblockMagic) + } + + sb := &superblock{ + Magic: magic, // b[0:4] + Checksum: binary.LittleEndian.Uint32(b[4:8]), + FeatureCompat: binary.LittleEndian.Uint32(b[8:12]), + BlockSizeBits: b[12], // b[12:13] + ExtSlots: b[13], // b[13:14] + RootNid: binary.LittleEndian.Uint16(b[14:16]), + Inodes: binary.LittleEndian.Uint64(b[16:24]), + BuildTime: binary.LittleEndian.Uint64(b[24:32]), + BuildTimeNsec: binary.LittleEndian.Uint32(b[32:36]), + Blocks: binary.LittleEndian.Uint32(b[36:40]), + MetaBlockAddr: binary.LittleEndian.Uint32(b[40:44]), + XattrBlockAddr: binary.LittleEndian.Uint32(b[44:48]), + UUID: [16]byte(b[48:64]), + VolumeName: [16]byte(b[64:80]), + FeatureIncompat: binary.LittleEndian.Uint32(b[80:84]), + Union1: binary.LittleEndian.Uint16(b[84:86]), + ExtraDevices: binary.LittleEndian.Uint16(b[86:88]), + DevTableSlotOff: binary.LittleEndian.Uint16(b[88:90]), + Reserved: [38]byte(b[90:128]), + } + + if featureIncompat := sb.FeatureIncompat & ^uint32(FeatureIncompatSupported); featureIncompat != 0 { + return nil, errors.Errorf("unsupported incompatible features detected: 0x%x", featureIncompat) + } + + if (1< %s", k.Name(), squashFile, extractDir) return nil } else if err != nil { @@ -678,6 +583,9 @@ func ExtractSingleSquash(squashFile string, extractDir string) error { return ExtractSingleSquashPolicy(squashFile, extractDir, exPolInfo.policy) } +var checkZstdSupported sync.Once +var zstdIsSuspported bool + func mksquashfsSupportsZstd() bool { checkZstdSupported.Do(func() { var stdoutBuffer strings.Builder diff --git a/squashfs/superblock.go b/pkg/squashfs/superblock.go similarity index 100% rename from squashfs/superblock.go rename to pkg/squashfs/superblock.go diff --git a/pkg/squashfs/verity.go b/pkg/squashfs/verity.go new file mode 100644 index 0000000..ba99840 --- /dev/null +++ b/pkg/squashfs/verity.go @@ -0,0 +1,15 @@ +package squashfs + +// verityDataLocation returns the end of filesystem image where the verity data +// can be appended. +// squashfs image must be padded to be 4K aligned. +func verityDataLocation(sblock *superblock) (uint64, error) { + squashLen := sblock.size + + // squashfs is padded out to the nearest 4k + if squashLen%4096 != 0 { + squashLen = squashLen + (4096 - squashLen%4096) + } + + return squashLen, nil +} diff --git a/squashfs/verity_test.go b/pkg/squashfs/verity_test.go similarity index 74% rename from squashfs/verity_test.go rename to pkg/squashfs/verity_test.go index 1ee55b7..017c80b 100644 --- a/squashfs/verity_test.go +++ b/pkg/squashfs/verity_test.go @@ -9,50 +9,9 @@ import ( "testing" "github.com/stretchr/testify/assert" + "machinerun.io/atomfs/pkg/verity" ) -type uidmapTestcase struct { - uidmap string - expected bool -} - -var uidmapTests = []uidmapTestcase{ - { - uidmap: ` 0 0 4294967295`, - expected: true, - }, - { - uidmap: ` 0 0 1000 -2000 2000 1`, - expected: false, - }, - { - uidmap: ` 0 0 1000`, - expected: false, - }, - { - uidmap: ` 10 0 4294967295`, - expected: false, - }, - { - uidmap: ` 0 10 4294967295`, - expected: false, - }, - { - uidmap: ` 0 0 1`, - expected: false, - }, -} - -func TestAmHostRoot(t *testing.T) { - t.Parallel() - assert := assert.New(t) - for _, testcase := range uidmapTests { - v := uidmapIsHost(testcase.uidmap) - assert.Equal(v, testcase.expected) - } -} - func TestVerityMetadata(t *testing.T) { assert := assert.New(t) @@ -67,8 +26,8 @@ func TestVerityMetadata(t *testing.T) { err = os.WriteFile(path.Join(rootfs, "foo"), []byte("bar"), 0644) assert.NoError(err) - reader, _, rootHash, err := MakeSquashfs(tempdir, rootfs, nil, VerityMetadataPresent) - if err == cryptsetupTooOld { + reader, _, rootHash, err := MakeSquashfs(tempdir, rootfs, nil, verity.VerityMetadataPresent) + if err == verity.CryptsetupTooOld { t.Skip("libcryptsetup too old") } assert.NoError(err) diff --git a/pkg/verity/metadata.go b/pkg/verity/metadata.go new file mode 100644 index 0000000..6bfabbb --- /dev/null +++ b/pkg/verity/metadata.go @@ -0,0 +1,16 @@ +package verity + +import "strings" + +type VerityMetadata bool + +const ( + VeritySuffix = "verity" + + VerityMetadataPresent VerityMetadata = true + VerityMetadataMissing VerityMetadata = false +) + +func HasVerityMetadata(mediaType string) VerityMetadata { + return VerityMetadata(strings.HasSuffix(mediaType, VeritySuffix)) +} diff --git a/squashfs/verity.go b/pkg/verity/verity.go similarity index 66% rename from squashfs/verity.go rename to pkg/verity/verity.go index ea1bf7b..0b200fe 100644 --- a/squashfs/verity.go +++ b/pkg/verity/verity.go @@ -1,4 +1,4 @@ -package squashfs +package verity // #cgo pkg-config: libcryptsetup devmapper --static // #include @@ -87,11 +87,9 @@ import ( "github.com/martinjungblut/go-cryptsetup" "github.com/pkg/errors" "golang.org/x/sys/unix" - - "machinerun.io/atomfs/mount" ) -const VerityRootHashAnnotation = "io.stackeroci.stacker.squashfs_verity_root_hash" +const VerityRootHashAnnotation = "io.stackeroci.stacker.squashfs_verity_root_hash" // FIXME: s/squashfs/atomfs type verityDeviceType struct { Flags uint @@ -139,9 +137,9 @@ func isCryptsetupEINVAL(err error) bool { return ok && cse.Code() == -22 } -var cryptsetupTooOld = errors.Errorf("libcryptsetup not new enough, need >= 2.3.0") +var CryptsetupTooOld = errors.Errorf("libcryptsetup not new enough, need >= 2.3.0") -func appendVerityData(file string) (string, error) { +func AppendVerityData(file string) (string, error) { fi, err := os.Lstat(file) if err != nil { return "", errors.WithStack(err) @@ -149,7 +147,7 @@ func appendVerityData(file string) (string, error) { verityOffset := fi.Size() - // we expect mksquashfs to have padded the file to the nearest 4k + // we expect make fs to have padded the file to the nearest 4k // (dm-verity requires device block size, which is 512 for loopback, // which is a multiple of 4k), let's check that here if verityOffset%512 != 0 { @@ -182,7 +180,7 @@ func appendVerityData(file string) (string, error) { // render a special error message. rootHash, _, err := verityDevice.VolumeKeyGet(cryptsetup.CRYPT_ANY_SLOT, "") if isCryptsetupEINVAL(err) { - return "", cryptsetupTooOld + return "", CryptsetupTooOld } else if err != nil { return "", err } @@ -190,138 +188,27 @@ func appendVerityData(file string) (string, error) { return fmt.Sprintf("%x", rootHash), errors.WithStack(err) } -func verityDataLocation(sblock *superblock) (uint64, error) { - squashLen := sblock.size - - // squashfs is padded out to the nearest 4k - if squashLen%4096 != 0 { - squashLen = squashLen + (4096 - squashLen%4096) - } - - return squashLen, nil -} - func verityName(p string) string { - return fmt.Sprintf("%s-%s", p, veritySuffix) -} - -func fileChanged(a os.FileInfo, path string) bool { - b, err := os.Lstat(path) - if err != nil { - return true - } - return !os.SameFile(a, b) -} - -// Mount a filesystem as container root, without host root -// privileges. We do this using squashfuse. -func GuestMount(squashFile string, mountpoint string) error { - if isMountpoint(mountpoint) { - return errors.Errorf("%s is already mounted", mountpoint) - } - - abs, err := filepath.Abs(squashFile) - if err != nil { - return errors.Errorf("Failed to get absolute path for %s: %v", squashFile, err) - } - squashFile = abs - - abs, err = filepath.Abs(mountpoint) - if err != nil { - return errors.Errorf("Failed to get absolute path for %s: %v", mountpoint, err) - } - mountpoint = abs - - cmd, err := squashFuse(squashFile, mountpoint) - if err != nil { - return err - } - if err := cmd.Process.Release(); err != nil { - return errors.Errorf("Failed to release process after guestmount %s: %v", squashFile, err) - } - return nil -} - -func isMountpoint(dest string) bool { - mounted, err := mount.IsMountpoint(dest) - return err == nil && mounted -} - -// Takes /proc/self/uid_map contents as one string -// Returns true if this is a uidmap representing the whole host -// uid range. -func uidmapIsHost(oneline string) bool { - oneline = strings.TrimSuffix(oneline, "\n") - if len(oneline) == 0 { - return false - } - lines := strings.Split(oneline, "\n") - if len(lines) != 1 { - return false - } - words := strings.Fields(lines[0]) - if len(words) != 3 || words[0] != "0" || words[1] != "0" || words[2] != "4294967295" { - return false - } - - return true -} - -func AmHostRoot() bool { - // if not uid 0, not host root - if os.Geteuid() != 0 { - return false - } - // if uid_map doesn't map 0 to 0, not host root - bytes, err := os.ReadFile("/proc/self/uid_map") - if err != nil { - return false - } - return uidmapIsHost(string(bytes)) + return fmt.Sprintf("%s-%s", p, VeritySuffix) } -func Mount(squashfs, mountpoint, rootHash string) error { - if !AmHostRoot() { - return GuestMount(squashfs, mountpoint) - } - err := HostMount(squashfs, mountpoint, rootHash) - if err == nil || rootHash != "" { - return err - } - return GuestMount(squashfs, mountpoint) -} - -func HostMount(squashfs string, mountpoint string, rootHash string) error { - fi, err := os.Stat(squashfs) - if err != nil { - return errors.WithStack(err) - } - - sblock, err := readSuperblock(squashfs) - if err != nil { - return err - } - - verityOffset, err := verityDataLocation(sblock) - if err != nil { - return err - } - - if verityOffset == uint64(fi.Size()) && rootHash != "" { +func VerityHostMount(fsImgFile string, fsType string, mountpoint string, rootHash string, veritySize int64, verityOffset uint64) error { + if verityOffset == uint64(veritySize) && rootHash != "" { return errors.Errorf("asked for verity but no data present") } - if rootHash == "" && verityOffset != uint64(fi.Size()) { + if rootHash == "" && verityOffset != uint64(veritySize) { return errors.Errorf("verity data present but no root hash specified") } mountSourcePath := "" var verityDevice *cryptsetup.Device - name := verityName(path.Base(squashfs)) + name := verityName(path.Base(fsImgFile)) loopDevNeedsClosedOnErr := false var loopDev losetup.Device + var err error // set up the verity device if necessary if rootHash != "" { @@ -333,7 +220,7 @@ func HostMount(squashfs string, mountpoint string, rootHash string) error { return errors.WithStack(err) } - loopDev, err = losetup.Attach(squashfs, 0, true) + loopDev, err = losetup.Attach(fsImgFile, 0, true) if err != nil { return errors.WithStack(err) } @@ -392,7 +279,7 @@ func HostMount(squashfs string, mountpoint string, rootHash string) error { } } else { - loopDev, err = losetup.Attach(squashfs, 0, true) + loopDev, err = losetup.Attach(fsImgFile, 0, true) if err != nil { return errors.WithStack(err) } @@ -401,7 +288,7 @@ func HostMount(squashfs string, mountpoint string, rootHash string) error { } - err = errors.WithStack(unix.Mount(mountSourcePath, mountpoint, "squashfs", unix.MS_RDONLY, "")) + err = errors.WithStack(unix.Mount(mountSourcePath, mountpoint, fsType, unix.MS_RDONLY, "")) if err != nil { if verityDevice != nil { _ = verityDevice.Deactivate(name) @@ -449,61 +336,33 @@ func findLoopBackingVerity(device string) (int64, error) { return deviceNo, nil } -func Umount(mountpoint string) error { - mounts, err := mount.ParseMounts("/proc/self/mountinfo") +func VerityUnmount(mountPath string) error { + // find the loop device that backs the verity device + deviceNo, err := findLoopBackingVerity(mountPath) if err != nil { return err } - // first, find the verity device that backs the mount - theMount, found := mounts.FindMount(mountpoint) - if !found { - return errors.Errorf("%s is not a mountpoint", mountpoint) - } - - err = unix.Unmount(mountpoint, 0) + loopDev := losetup.New(uint64(deviceNo), 0) + // here, we don't have the loopback device any more (we detached it + // above). the cryptsetup API allows us to pass NULL for the crypt + // device, but go-cryptsetup doesn't have a way to initialize a NULL + // crypt device short of making the struct by hand like this. + err = (&cryptsetup.Device{}).Deactivate(mountPath) if err != nil { - return errors.Wrapf(err, "failed unmounting %v", mountpoint) - } - - if _, err := os.Stat(theMount.Source); err != nil { - if os.IsNotExist(err) { - return nil - } return errors.WithStack(err) } - // was this a verity mount or a regular loopback mount? (if it's a - // regular loopback mount, we detached it above, so need to do anything - // special here; verity doesn't play as nicely) - if strings.HasSuffix(theMount.Source, veritySuffix) { - // find the loop device that backs the verity device - deviceNo, err := findLoopBackingVerity(theMount.Source) - if err != nil { - return err - } - - loopDev := losetup.New(uint64(deviceNo), 0) - // here, we don't have the loopback device any more (we detached it - // above). the cryptsetup API allows us to pass NULL for the crypt - // device, but go-cryptsetup doesn't have a way to initialize a NULL - // crypt device short of making the struct by hand like this. - err = (&cryptsetup.Device{}).Deactivate(theMount.Source) - if err != nil { - return errors.WithStack(err) - } - - // finally, kill the loop dev - err = loopDev.Detach() - if err != nil { - return errors.Wrapf(err, "failed to detach loop dev for %v", theMount.Source) - } + // finally, kill the loop dev + err = loopDev.Detach() + if err != nil { + return errors.Wrapf(err, "failed to detach loop dev for %v", mountPath) } return nil } -// If we are using squashfuse, then we will be unable to get verity has from +// If we are using fuse, then we will be unable to get verity has from // the mount device. This is not a safe thing, we we only allow it when the // device was mounted originally with AllowMissingVerityData. diff --git a/squashfs/verity_static.go b/pkg/verity/verity_static.go similarity index 94% rename from squashfs/verity_static.go rename to pkg/verity/verity_static.go index feac991..d95456c 100644 --- a/squashfs/verity_static.go +++ b/pkg/verity/verity_static.go @@ -1,7 +1,7 @@ //go:build static_build // +build static_build -package squashfs +package verity // cryptsetup's pkgconfig is broken (it does not set Requires.private or // Libs.private at all), so we do the LDLIBS for it by hand. diff --git a/test/lxc.conf b/test/lxc.conf new file mode 100644 index 0000000..7db3c06 --- /dev/null +++ b/test/lxc.conf @@ -0,0 +1,46 @@ +# Template used to create this container: /usr/share/lxc/templates/lxc-download +# Parameters passed to the template: --dist alpine --release 3.19 --arch amd64 +# For additional config options, please look at lxc.container.conf(5) + +# Uncomment the following line to support nesting containers: +#lxc.include = /usr/share/lxc/config/nesting.conf +# (Be aware this has security implications) + + +# Distribution configuration +lxc.include = /usr/share/lxc/config/common.conf +lxc.include = /usr/share/lxc/config/userns.conf +lxc.arch = linux64 + +# Container specific configuration +lxc.idmap = u 0 165536 65536 +lxc.idmap = g 0 165536 65536 +lxc.rootfs.path = dir://rootfs +lxc.uts.name = mycontainer + +# Network configuration +lxc.net.0.type = veth +lxc.net.0.flags = up +lxc.net.0.link = lxcbr0 + +# mounts +lxc.mount.auto = proc:mixed sys:mixed cgroup:mixed +lxc.mount.entry = tmpfs tmp tmpfs size=1M 0 0 +lxc.mount.entry = /dev/null dev/null none bind,optional,create=file 0 0 +lxc.mount.entry = /dev/urandom dev/urandom none bind,optional,create=file 0 0 +lxc.mount.entry = /dev/random dev/random none bind,optional,create=file 0 0 +#lxc.mount.entry = /bin bin none ro,bind 0 0 +#lxc.mount.entry = /lib lib none ro,rbind 0 0 +#lxc.mount.entry = /sbin sbin none ro,bind 0 0 +#lxc.mount.entry = /usr/bin usr/bin none ro,bind 0 0 +#lxc.mount.entry = /usr/lib usr/lib none ro,bind 0 0 +#lxc.mount.entry = /usr/sbin usr/sbin none ro,bind 0 0 +#lxc.mount.entry = /etc/alternatives etc/alternatives none ro,bind 0 0 +#lxc.mount.entry = /sys sys none ro,bind 0 0 +lxc.mount.entry = /etc/resolv.conf etc/resolv.conf none bind,ro,optional,create=file 0 0 + +lxc.console.path = none + +lxc.environment = PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin + +#lxc.cap.keep = setuid setgid