Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
42 changes: 42 additions & 0 deletions kernel/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,51 @@

package kernel

import (
"os"

"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/testutil"
)

// MockEnsureInterval sets the overlord ensure interval for tests.
func MockOsSymlink(newSymlink func(string, string) error) (restore func()) {
old := osSymlink
osSymlink = newSymlink
return func() { osSymlink = old }
}

// MockAtomicWriteFile mocks the osutil.AtomicWriteFile wrapper used by
// writeDriversTreeMeta, so tests can simulate a marker-write failure.
func MockAtomicWriteFile(f func(string, []byte, os.FileMode, osutil.AtomicWriteFlags) error) (restore func()) {
return testutil.Mock(&atomicWriteFile, f)
}

// WriteDriversTreeMeta is exported for testing.
func WriteDriversTreeMeta(destDir string) error {
Comment thread
Mohit-Chachada marked this conversation as resolved.
Outdated
return writeDriversTreeMeta(destDir)
}

// ReadDriversTreeGeneratorVersion is exported for testing.
Comment thread
Mohit-Chachada marked this conversation as resolved.
Outdated
func ReadDriversTreeGeneratorVersion(destDir string) (int, error) {
meta, err := readDriversTreeGeneratorMeta(destDir)
if err != nil {
return 0, err
}
return meta.GeneratorVersion, nil
}

// KernelDriversTreeGeneratorVersion returns the current generator version
// constant, exported for testing.
func KernelDriversTreeGeneratorVersion() int {
Comment thread
Mohit-Chachada marked this conversation as resolved.
return kernelDriversTreeGeneratorVersion
}

// MockKernelDriversTreeGeneratorVersion overrides the generator version
// constant for testing (e.g. to simulate a revert scenario where the
// on-disk marker records a newer version than the running code).
func MockKernelDriversTreeGeneratorVersion(v int) (restore func()) {
old := kernelDriversTreeGeneratorVersion
Comment thread
Mohit-Chachada marked this conversation as resolved.
Outdated
kernelDriversTreeGeneratorVersion = v
return func() { kernelDriversTreeGeneratorVersion = old }
}
90 changes: 90 additions & 0 deletions kernel/kernel_drivers.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
package kernel

import (
"encoding/json"
"errors"
"fmt"
"io/fs"
Expand All @@ -40,6 +41,85 @@ import (
// For testing purposes
var osSymlink = os.Symlink

// atomicWriteFile is a mockable wrapper around osutil.AtomicWriteFile, used
// by writeDriversTreeMeta, so tests can simulate a marker-write failure
// (e.g. ENOSPC) without needing to actually exhaust disk space.
var atomicWriteFile = osutil.AtomicWriteFile

// kernelDriversTreeGeneratorVersion identifies the logic that produced a
// kernel drivers tree (the on-disk symlinks/files under
// <destDir>/lib/{modules,firmware}).
//
// IMPORTANT: bump this whenever there is a change to the layout or organization of the
// kernel drivers or firmware trees.
var kernelDriversTreeGeneratorVersion = 1

// driversTreeMeta is the content of the <destDir>/kernel.json marker file
// written after every successful kernel drivers tree build.
type driversTreeMeta struct {
GeneratorVersion int `json:"generator-version"`
}

func driversTreeMetaPath(destDir string) string {
return filepath.Join(destDir, "kernel.json")
}

// writeDriversTreeMeta records the generator version that produced destDir.
func writeDriversTreeMeta(destDir string) error {
meta := driversTreeMeta{GeneratorVersion: kernelDriversTreeGeneratorVersion}
data, err := json.Marshal(&meta)
if err != nil {
return err
}
return atomicWriteFile(driversTreeMetaPath(destDir), data, 0644, 0)
}

var (
errGeneratorMetaCorrupted = errors.New("metadata file is corrupted")
Comment thread
Mohit-Chachada marked this conversation as resolved.
Outdated
)

// readDriversTreeGeneratorMeta returns the generator metadata recorded for
// destDir. If no marker value is present a default zero value with
// GeneratorVersion set to 0 is returned and no error.
func readDriversTreeGeneratorMeta(destDir string) (driversTreeMeta, error) {
Comment thread
Mohit-Chachada marked this conversation as resolved.
Outdated
data, err := os.ReadFile(driversTreeMetaPath(destDir))
if errors.Is(err, fs.ErrNotExist) {
return driversTreeMeta{
// Explicit zero value so that there are no misconceptions
// of what it means
GeneratorVersion: 0,
}, nil
}
if err != nil {
return driversTreeMeta{}, err
}
var meta driversTreeMeta
if err := json.Unmarshal(data, &meta); err != nil {
// The marker could be corrupted, which means that the tree likely needs a rebuild.
return driversTreeMeta{}, errGeneratorMetaCorrupted
}
return meta, nil
}

// DriversTreeOutdated returns true when the kernel modules & firmware tree at
// destDir was built by an older version of the generator code, indicating it
// may need to be checked or rebuilt.
func DriversTreeOutdated(destDir string) (bool, error) {
v, err := readDriversTreeGeneratorMeta(destDir)
if err != nil {
if errors.Is(err, errGeneratorMetaCorrupted) {
// Corrupted metadata file warrants a rebuild.
return true, nil
}
return false, err
Comment thread
Mohit-Chachada marked this conversation as resolved.
}
logger.Debugf("checking kernel tree generator version, current %v, on disk %v",
Comment thread
pedronis marked this conversation as resolved.
kernelDriversTreeGeneratorVersion, v.GeneratorVersion)
// Only care about older (lower) versions. The tree may have been built by a
// newer snapd.
Comment thread
Mohit-Chachada marked this conversation as resolved.
Outdated
return kernelDriversTreeGeneratorVersion > v.GeneratorVersion, nil
}

// We expect as a minimum something that starts with three numbers
// separated by dots for the kernel version.
var utsRelease = regexp.MustCompile(`^([0-9]+\.){2}[0-9]+`)
Expand Down Expand Up @@ -366,6 +446,8 @@ func EnsureKernelDriversTree(kMntPts MountPoints, compsMntPts []ModulesCompMount
if exists && isDir {
logger.Debugf("device tree %q already created on installation, not re-creating",
targetDir)
// Nothing was built here, so the existing marker (if any) is
// left untouched.
return nil
}
}
Expand Down Expand Up @@ -452,6 +534,14 @@ func EnsureKernelDriversTree(kMntPts MountPoints, compsMntPts []ModulesCompMount
syscall.Sync()
}

if opts.KernelInstall {
// Record the version of the layout used for the firmware and modules
// tree.
if err := writeDriversTreeMeta(targetDir); err != nil {
return err
}
}

return nil
}

Expand Down
80 changes: 80 additions & 0 deletions kernel/kernel_drivers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -923,3 +923,83 @@ func (s *kernelDriversTestSuite) TestNeedsKernelDriversTreeClassicWithWrongBase(
c.Assert(kernel.NeedsKernelDriversTree(uc22model), Equals, tc.result)
}
}

func (s *kernelDriversTestSuite) TestDriversTreeMetaRoundTrip(c *C) {
destDir := c.MkDir()

c.Assert(kernel.WriteDriversTreeMeta(destDir), IsNil)

v, err := kernel.ReadDriversTreeGeneratorVersion(destDir)
c.Assert(err, IsNil)
c.Assert(v, Equals, kernel.KernelDriversTreeGeneratorVersion())

// The marker file lives inside destDir, so it is cleaned up by
// RemoveKernelDriversTree's existing os.RemoveAll.
c.Assert(osutil.FileExists(filepath.Join(destDir, "kernel.json")), Equals, true)
}

func (s *kernelDriversTestSuite) TestDriversTreeOutdatedMissingMarker(c *C) {
destDir := c.MkDir()

needsCheck, err := kernel.DriversTreeOutdated(destDir)
c.Assert(err, IsNil)
c.Assert(needsCheck, Equals, true)
}

func (s *kernelDriversTestSuite) TestDriversTreeOutdatedUpToDate(c *C) {
destDir := c.MkDir()

c.Assert(kernel.WriteDriversTreeMeta(destDir), IsNil)

needsCheck, err := kernel.DriversTreeOutdated(destDir)
c.Assert(err, IsNil)
c.Assert(needsCheck, Equals, false)
}

func (s *kernelDriversTestSuite) TestDriversTreeOutdatedForwardOnly(c *C) {
destDir := c.MkDir()

// Simulate a tree built by a newer generator than what is currently
// running (e.g. after a snapd revert): the marker records a version
// higher than the current constant.
func() {
restore := kernel.MockKernelDriversTreeGeneratorVersion(100)
defer restore()
c.Assert(kernel.WriteDriversTreeMeta(destDir), IsNil)
}()

needsCheck, err := kernel.DriversTreeOutdated(destDir)
c.Assert(err, IsNil)
c.Assert(needsCheck, Equals, false)
}

func (s *kernelDriversTestSuite) TestDriversTreeOutdatedCorruptMarker(c *C) {
destDir := c.MkDir()

c.Assert(os.WriteFile(filepath.Join(destDir, "kernel.json"), []byte("not json"), 0644), IsNil)

needsCheck, err := kernel.DriversTreeOutdated(destDir)
c.Assert(err, IsNil)
c.Assert(needsCheck, Equals, true)
}

func (s *kernelDriversTestSuite) TestKernelInstallMarkerWriteFailureDiscardsTree(c *C) {
kversion := "5.15.0-78-generic"
mountDir := filepath.Join(dirs.SnapMountDir, "pc-kernel/1")
createKernelSnapFiles(c, kversion, mountDir, createKernelSnapFilesOpts{})

destDir := kernel.DriversTreeDir(dirs.GlobalRootDir, "pc-kernel", snap.R(1))
kMntPts := kernel.MountPoints{Current: mountDir, Target: mountDir}

boom := errors.New("boom: no space left on device")
restore := kernel.MockAtomicWriteFile(func(string, []byte, os.FileMode, osutil.AtomicWriteFlags) error {
return boom
})
defer restore()

err := kernel.EnsureKernelDriversTree(kMntPts, nil, destDir,
&kernel.KernelDriversTreeOptions{KernelInstall: true})
c.Assert(err, Equals, boom)

c.Check(osutil.FileExists(destDir), Equals, false)
}
4 changes: 4 additions & 0 deletions tests/core/basic20plus/task.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,10 @@ execute: |
stat /run/systemd/system/sysroot-usr-lib-modules.mount
stat /run/systemd/system/"sysroot-writable-system\x2ddata-snap-$device\x2dkernel-"*

# Check the generator version
gojq '.["generator-version"]' /var/lib/snapd/kernel/"$device"-kernel/x1/kernel.json | \

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

x1? I'm prob missing something but I don't see where this test installs a local kernel.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the core suite, so running on UC and we repack the kernel in prepare, so it's always unasserted AFAIU

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

garden:ubuntu-core-26-64 .../tests/core/basic20plus# snap list pc-kernel
Name       Version        Rev  Tracking  Publisher  Notes
pc-kernel  7.0.0-38.38r1  x1   -         -          kernel,components[13]

MATCH '^1$'

not mountpoint /run/mnt/base
loop="$(findmnt -o source / -n)"
# Might be in /run/mnt/ubuntu-seed/ on first 26+ boot or in /run/mnt/data/
Expand Down
Loading