Skip to content

Commit

Permalink
snap/squashfs: enforce a minimum snap size to eliminate some kernel l…
Browse files Browse the repository at this point in the history
…og noise (#13191)

* squashfs: enforce a minimum snap size to eliminate kernel log noise

* integrity: update tests to account for new minimum snap size

* pack: update tests to account for new minimum snap size

* squashfs: rename truncate functions for clarity

* squashfs: make comment explaining growing a snap more clear

Co-authored-by: Miguel Pires <miguelpires94@gmail.com>

* squashfs: add better context to truncation error

Co-authored-by: Miguel Pires <miguelpires94@gmail.com>

* squashfs: split error if check onto multiple lines

* snap/pack: make test data consistent with new snap size

* snap/pack: update comment to be consistent with new snap size

* snap/squashfs: use more suited check comparator

* snap/integrity: add comment explaining calculation of verityHashSize

* pack: add comment explaining verityHashSize

* s/squashfs: log random snap data in test failure to enable reproduction

* s/squashfs: reduce mocking needed by calling actual mksquashfs command

---------

Co-authored-by: Miguel Pires <miguelpires94@gmail.com>
  • Loading branch information
andrewphelpsj and MiguelPires authored Sep 26, 2023
1 parent 402fdde commit 5bae3c1
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 27 deletions.
15 changes: 10 additions & 5 deletions snap/integrity/integrity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,11 @@ func (s *IntegrityTestSuite) TestGenerateAndAppendSuccess(c *C) {

snapPath, _ := snaptest.MakeTestSnapInfoWithFiles(c, "name: foo\nversion: 1.0", nil, nil)

// 8192 is the hash size that is created when running 'veritysetup format'
// on a minimally sized snap. there is not an easy way to calculate this
// value dynamically.
const verityHashSize = 8192

// mock the verity-setup command, what it does is make a copy of the snap
// and then returns pre-calculated output
vscmd := testutil.MockCommand(c, "veritysetup", fmt.Sprintf(`
Expand All @@ -227,19 +232,19 @@ case "$1" in
exit 0
;;
format)
cp %[1]s %[1]s.verity
echo "VERITY header information for %[1]s.verity"
truncate -s %[1]d %[2]s.verity
echo "VERITY header information for %[2]s.verity"
echo "UUID: f8b4f201-fe4e-41a2-9f1d-4908d3c76632"
echo "Hash type: 1"
echo "Data blocks: 1"
echo "Data blocks: 4"
echo "Data block size: 4096"
echo "Hash block size: 4096"
echo "Hash algorithm: sha256"
echo "Salt: f1a7f87b88692b388f47dbda4a3bdf790f5adc3104b325f8772aee593488bf15"
echo "Root hash: e2926364a8b1242d92fb1b56081e1ddb86eba35411961252a103a1c083c2be6d"
;;
esac
`, snapPath))
`, verityHashSize, snapPath))
defer vscmd.Restore()

snapFileInfo, err := os.Stat(snapPath)
Expand All @@ -266,7 +271,7 @@ esac
err = integrityDataHeader.Decode(header)
c.Check(err, IsNil)
c.Check(integrityDataHeader.Type, Equals, "integrity")
c.Check(integrityDataHeader.Size, Equals, uint64(2*4096))
c.Check(integrityDataHeader.Size, Equals, uint64(verityHashSize+integrity.HeaderSize))
c.Check(integrityDataHeader.DmVerity.RootHash, HasLen, 64)

c.Assert(vscmd.Calls(), HasLen, 2)
Expand Down
24 changes: 15 additions & 9 deletions snap/pack/pack_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import (
// for SanitizePlugsSlots
_ "github.com/snapcore/snapd/interfaces/builtin"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/snap/integrity"
"github.com/snapcore/snapd/snap/pack"
"github.com/snapcore/snapd/snap/squashfs"
"github.com/snapcore/snapd/testutil"
Expand Down Expand Up @@ -465,6 +466,11 @@ func (s *packSuite) TestPackWithIntegrity(c *C) {
sourceDir := makeExampleSnapSourceDir(c, "{name: hello, version: 0}")
targetDir := c.MkDir()

// 8192 is the hash size that is created when running 'veritysetup format'
// on a minimally sized snap. there is not an easy way to calculate this
// value dynamically.
const verityHashSize = 8192

// mock the verity-setup command, what it does is make a copy of the snap
// and then returns pre-calculated output
vscmd := testutil.MockCommand(c, "veritysetup", fmt.Sprintf(`
Expand All @@ -474,19 +480,19 @@ case "$1" in
exit 0
;;
format)
cp %[1]s/hello_0_all.snap %[1]s/hello_0_all.snap.verity
echo "VERITY header information for %[1]s/hello_0_all.snap.verity"
truncate -s %[1]d %[2]s/hello_0_all.snap.verity
echo "VERITY header information for %[2]s/hello_0_all.snap.verity"
echo "UUID: 606d10a2-24d8-4c6b-90cf-68207aa7c850"
echo "Hash type: 1"
echo "Data blocks: 1"
echo "Data blocks: 4"
echo "Data block size: 4096"
echo "Hash block size: 4096"
echo "Hash algorithm: sha256"
echo "Salt: eba61f2091bb6122226aef83b0d6c1623f095fc1fda5712d652a8b34a02024ea"
echo "Root hash: 3fbfef5f1f0214d727d03eebc4723b8ef5a34740fd8f1359783cff1ef9c3f334"
;;
esac
`, targetDir))
`, verityHashSize, targetDir))
defer vscmd.Restore()

snapPath, err := pack.Snap(sourceDir, &pack.Options{
Expand All @@ -505,11 +511,11 @@ esac
c.Assert(err, IsNil)
defer snapFile.Close()

// example snap has a size of 4096 (1 block)
_, err = snapFile.Seek(4096, io.SeekStart)
// example snap has a size of 16384 (4 blocks)
_, err = snapFile.Seek(squashfs.MinimumSnapSize, io.SeekStart)
c.Assert(err, IsNil)

integrityHdr := make([]byte, 4096)
integrityHdr := make([]byte, integrity.HeaderSize)
_, err = snapFile.Read(integrityHdr)
c.Assert(err, IsNil)

Expand All @@ -526,9 +532,9 @@ esac
c.Assert(ok, Equals, true)
hdrSize, err := strconv.ParseUint(hdrSizeStr, 10, 64)
c.Assert(err, IsNil)
c.Check(hdrSize, Equals, uint64(2*4096))
c.Check(hdrSize, Equals, uint64(integrity.HeaderSize+verityHashSize))

fi, err := snapFile.Stat()
c.Assert(err, IsNil)
c.Check(fi.Size(), Equals, int64(3*4096))
c.Check(fi.Size(), Equals, int64(squashfs.MinimumSnapSize+(integrity.HeaderSize+verityHashSize)))
}
31 changes: 30 additions & 1 deletion snap/squashfs/squashfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,12 @@ type BuildOpts struct {
ExcludeFiles []string
}

// MinimumSnapSize is the smallest size a snap can be. The kernel attempts to read a
// partition table from the snap when a loopback device is created from it. If the snap
// is smaller than this size, some versions of the kernel will print error logs while
// scanning the loopback device for partitions.
const MinimumSnapSize int64 = 16384

// Build builds the snap.
func (s *Snap) Build(sourceDir string, opts *BuildOpts) error {
if opts == nil {
Expand Down Expand Up @@ -537,6 +543,7 @@ func (s *Snap) Build(sourceDir string, opts *BuildOpts) error {
"-no-fragments",
"-no-progress",
)

if len(opts.ExcludeFiles) > 0 {
cmd.Args = append(cmd.Args, "-wildcards")
for _, excludeFile := range opts.ExcludeFiles {
Expand All @@ -548,21 +555,43 @@ func (s *Snap) Build(sourceDir string, opts *BuildOpts) error {
cmd.Args = append(cmd.Args, "-all-root", "-no-xattrs")
}

return osutil.ChDir(sourceDir, func() error {
err = osutil.ChDir(sourceDir, func() error {
output, err := cmd.CombinedOutput()
if err != nil {
return MksquashfsError{fmt.Sprintf("mksquashfs call failed: %s", osutil.OutputErr(output, err))}
}

return nil
})
if err != nil {
return err
}

// Grow the snap if it is smaller than the minimum snap size. See
// MinimumSnapSize for more details.
return s.growSnapToMinSize(MinimumSnapSize)
}

// BuildDate returns the "Creation or last append time" as reported by unsquashfs.
func (s *Snap) BuildDate() time.Time {
return BuildDate(s.path)
}

func (s *Snap) growSnapToMinSize(minSize int64) error {
size, err := s.Size()
if err != nil {
return fmt.Errorf("cannot get size of snap: %w", err)
}
if size >= minSize {
return nil
}
if err := os.Truncate(s.path, minSize); err != nil {
return fmt.Errorf("cannot grow snap to minimum size: %w", err)
}

return nil
}

// BuildDate returns the "Creation or last append time" as reported by unsquashfs.
func BuildDate(path string) time.Time {
var t0 time.Time
Expand Down
57 changes: 45 additions & 12 deletions snap/squashfs/squashfs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import (

"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/randutil"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/snap/snapdir"
"github.com/snapcore/snapd/snap/squashfs"
Expand All @@ -47,13 +48,6 @@ import (
// Hook up check.v1 into the "go test" runner
func Test(t *testing.T) { TestingT(t) }

type SquashfsTestSuite struct {
oldStdout, oldStderr, outf *os.File
testutil.BaseTest
}

var _ = Suite(&SquashfsTestSuite{})

func makeSnap(c *C, manifest, data string) *squashfs.Snap {
cur, _ := os.Getwd()
return makeSnapInDir(c, cur, manifest, data)
Expand Down Expand Up @@ -107,9 +101,18 @@ func makeSnapInDir(c *C, dir, manifest, data string) *squashfs.Snap {
return sn
}

type SquashfsTestSuite struct {
testutil.BaseTest

oldStdout, oldStderr, outf *os.File
}

var _ = Suite(&SquashfsTestSuite{})

func (s *SquashfsTestSuite) SetUpTest(c *C) {
d := c.MkDir()
dirs.SetRootDir(d)
s.AddCleanup(func() { dirs.SetRootDir("") })
err := os.Chdir(d)
c.Assert(err, IsNil)

Expand Down Expand Up @@ -668,12 +671,17 @@ func (s *SquashfsTestSuite) TestBuildSupportsMultipleExcludesWithOnlyOneWildcard
c.Check(cmd, Equals, "/usr/bin/mksquashfs")
return nil, errors.New("bzzt")
})()
mksq := testutil.MockCommand(c, "mksquashfs", "")
mksq := testutil.MockCommand(c, "mksquashfs", `/usr/bin/mksquashfs "$@"`)
defer mksq.Restore()

fakeSourcedir := c.MkDir()
for _, n := range []string{"exclude1", "exclude2", "exclude3"} {
err := os.WriteFile(filepath.Join(fakeSourcedir, n), nil, 0644)
c.Assert(err, IsNil)
}
snapPath := filepath.Join(c.MkDir(), "foo.snap")
sn := squashfs.New(snapPath)
err := sn.Build(c.MkDir(), &squashfs.BuildOpts{
err := sn.Build(fakeSourcedir, &squashfs.BuildOpts{
SnapType: "core",
ExcludeFiles: []string{"exclude1", "exclude2", "exclude3"},
})
Expand All @@ -693,7 +701,8 @@ func (s *SquashfsTestSuite) TestBuildUsesMksquashfsFromCoreIfAvailable(c *C) {
defer squashfs.MockCommandFromSystemSnap(func(cmd string, args ...string) (*exec.Cmd, error) {
usedFromCore = true
c.Check(cmd, Equals, "/usr/bin/mksquashfs")
return &exec.Cmd{Path: "/bin/true"}, nil
fakeCmd := exec.Cmd{Path: "/usr/bin/mksquashfs", Args: []string{"/usr/bin/mksquashfs"}}
return &fakeCmd, nil
})()
mksq := testutil.MockCommand(c, "mksquashfs", "exit 1")
defer mksq.Restore()
Expand All @@ -714,7 +723,7 @@ func (s *SquashfsTestSuite) TestBuildUsesMksquashfsFromClassicIfCoreUnavailable(
c.Check(cmd, Equals, "/usr/bin/mksquashfs")
return nil, errors.New("bzzt")
})()
mksq := testutil.MockCommand(c, "mksquashfs", "")
mksq := testutil.MockCommand(c, "mksquashfs", `/usr/bin/mksquashfs "$@"`)
defer mksq.Restore()

buildDir := c.MkDir()
Expand Down Expand Up @@ -749,7 +758,7 @@ func (s *SquashfsTestSuite) TestBuildVariesArgsByType(c *C) {
defer squashfs.MockCommandFromSystemSnap(func(cmd string, args ...string) (*exec.Cmd, error) {
return nil, errors.New("bzzt")
})()
mksq := testutil.MockCommand(c, "mksquashfs", "")
mksq := testutil.MockCommand(c, "mksquashfs", `/usr/bin/mksquashfs "$@"`)
defer mksq.Restore()

buildDir := c.MkDir()
Expand Down Expand Up @@ -962,3 +971,27 @@ func (s *SquashfsTestSuite) TestBuildWithCompressionUnhappy(c *C) {
})
c.Assert(err, ErrorMatches, "(?m)^mksquashfs call failed: ")
}

func (s *SquashfsTestSuite) TestBuildBelowMinimumSize(c *C) {
// this snap is empty. without truncating it to be larger, it should be smaller than
// the minimum snap size
sn := squashfs.New(filepath.Join(c.MkDir(), "truncate_me.snap"))
sn.Build(c.MkDir(), nil)

size, err := sn.Size()
c.Assert(err, IsNil)

c.Assert(size, Equals, squashfs.MinimumSnapSize)
}

func (s *SquashfsTestSuite) TestBuildAboveMinimumSize(c *C) {
// fill a snap with random data that will not compress well. it should be forced
// to be bigger than the minimum threshold
randomData := randutil.RandomString(int(squashfs.MinimumSnapSize * 2))
sn := makeSnapInDir(c, c.MkDir(), "name: do_not_truncate_me", randomData)

size, err := sn.Size()
c.Assert(err, IsNil)

c.Assert(int(size), testutil.IntGreaterThan, int(squashfs.MinimumSnapSize), Commentf("random snap data: %s", randomData))
}

0 comments on commit 5bae3c1

Please sign in to comment.