From 9712748dff8f339b3df1a891906c0ae7a03367cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alfonso=20S=C3=A1nchez-Beato?= Date: Tue, 13 Feb 2024 15:15:58 +0000 Subject: [PATCH] kernel,overlord: modify EnsureKernelDriversTree so it can handle kernel-modules components. --- kernel/kernel_drivers.go | 204 ++++++++++++++++++----- kernel/kernel_drivers_test.go | 249 +++++++++++++++++++++++----- osutil/rename.go | 27 +++ osutil/rename_darwin.go | 25 +++ osutil/rename_linux.go | 28 ++++ osutil/rename_linux_test.go | 50 ++++++ overlord/snapstate/backend/setup.go | 4 +- 7 files changed, 507 insertions(+), 80 deletions(-) create mode 100644 osutil/rename.go create mode 100644 osutil/rename_darwin.go create mode 100644 osutil/rename_linux.go create mode 100644 osutil/rename_linux_test.go diff --git a/kernel/kernel_drivers.go b/kernel/kernel_drivers.go index 3d633f52bc8..faa54992b6a 100644 --- a/kernel/kernel_drivers.go +++ b/kernel/kernel_drivers.go @@ -26,6 +26,7 @@ import ( "os" "path/filepath" "regexp" + "syscall" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/logger" @@ -40,6 +41,15 @@ var osSymlink = os.Symlink // separated by dots for the kernel version. var utsRelease = regexp.MustCompile(`^([0-9]+\.){2}[0-9]+`) +const earlyKernelsDir = "/run/mnt/kernel-snaps" + +// EarlyKernelModsComponentMountDir returns the path where components are mounted on +// early boot. +func EarlyKernelModsComponentMountDir(compName string, compRev snap.Revision, ksnapName string, snapRev snap.Revision) string { + return filepath.Join(dirs.GlobalRootDir, earlyKernelsDir, + ksnapName, "components", snapRev.String(), compName, compRev.String()) +} + // KernelVersionFromModulesDir returns the kernel version for a mounted kernel // snap (this would be the output if "uname -r" for a running kernel). It // assumes that there is a folder named modules/$(uname -r) inside the snap. @@ -70,9 +80,8 @@ func KernelVersionFromModulesDir(mountPoint string) (string, error) { return kversion, nil } -func createFirmwareSymlinks(orig, dest string) error { - fwOrig := filepath.Join(orig, "firmware") - fwDest := filepath.Join(dest, "lib", "firmware") +func createFirmwareSymlinks(fwMount, fwDest string) error { + fwOrig := filepath.Join(fwMount, "firmware") if err := os.MkdirAll(fwDest, 0755); err != nil { return err } @@ -122,27 +131,18 @@ func createFirmwareSymlinks(orig, dest string) error { return nil } -func createModulesSubtree(origin, dest string) error { - kversion, err := KernelVersionFromModulesDir(origin) - if err != nil { - // Bit of a corner case, but maybe possible. Log anyway. - // TODO detect this issue in snap pack, should be enforced - // if the snap declares kernel-modules components. - logger.Noticef("no modules found in %q", origin) - return nil - } - +func createModulesSubtree(kernelMount, kernelTree, kversion, kname string, krev snap.Revision, compInfos []*snap.ComponentSideInfo) error { // Although empty we need "lib" because "depmod" always appends // "/lib/modules/" to the directory passed with option // "-b". - modsRoot := filepath.Join(dest, "lib", "modules", kversion) + modsRoot := filepath.Join(kernelTree, "lib", "modules", kversion) if err := os.MkdirAll(modsRoot, 0755); err != nil { return err } // Copy modinfo files from the snap (these might be overwritten if // kernel-modules components are installed). - modsGlob := filepath.Join(origin, "modules", kversion, "modules.*") + modsGlob := filepath.Join(kernelMount, "modules", kversion, "modules.*") modFiles, err := filepath.Glob(modsGlob) if err != nil { // Should not really happen (only possible error is ErrBadPattern) @@ -156,7 +156,7 @@ func createModulesSubtree(origin, dest string) error { } // Symbolic links to early mount kernel snap - earlyMntDir := filepath.Join(origin, "modules", kversion) + earlyMntDir := filepath.Join(kernelMount, "modules", kversion) for _, d := range []string{"kernel", "vdso"} { lname := filepath.Join(modsRoot, d) to := filepath.Join(earlyMntDir, d) @@ -165,48 +165,174 @@ func createModulesSubtree(origin, dest string) error { } } + // If necessary, add modules from components and run depmod + return setupModsFromComp(kernelTree, kversion, kname, krev, compInfos) +} + +func setupModsFromComp(kernelTree, kversion, kname string, krev snap.Revision, compInfos []*snap.ComponentSideInfo) error { + // This folder needs to exist always to allow for directory swapping + // in the future, even if right now we don't have components. + compsRoot := filepath.Join(kernelTree, "lib", "modules", kversion, "updates") + if err := os.MkdirAll(compsRoot, 0755); err != nil { + return err + } + + if len(compInfos) == 0 { + return nil + } + + // Symbolic links to components + for _, ci := range compInfos { + compMntDir := filepath.Join(EarlyKernelModsComponentMountDir( + ci.Component.ComponentName, ci.Revision, kname, krev)) + lname := filepath.Join(compsRoot, ci.Component.ComponentName) + to := filepath.Join(compMntDir, "modules", kversion) + if err := osSymlink(to, lname); err != nil { + return err + } + } + + // Run depmod + stdout, stderr, err := osutil.RunSplitOutput("depmod", "-b", kernelTree, kversion) + if err != nil { + return osutil.OutputErrCombine(stdout, stderr, err) + } + logger.Noticef("depmod output:\n%s\n", string(osutil.CombineStdOutErr(stdout, stderr))) + return nil } -func driversTreeDir(ksnapName string, rev snap.Revision) string { - return filepath.Join(dirs.SnapdStateDir(dirs.GlobalRootDir), "kernel", ksnapName, rev.String()) +func driversTreeDir(kernelSubdir string, rev snap.Revision) string { + return filepath.Join(dirs.SnapdStateDir(dirs.GlobalRootDir), "kernel", kernelSubdir, rev.String()) } -// RemoveKernelDriversTree cleans-up the writable kernel tree for a given snap -// and revision. -func RemoveKernelDriversTree(ksnapName string, rev snap.Revision) (err error) { - treeRoot := driversTreeDir(ksnapName, rev) +// RemoveKernelDriversTree cleans-up the writable kernel tree in snapd data +// folder, under kernelSubdir/ (kernelSubdir is usually the snap name). +func RemoveKernelDriversTree(kernelSubdir string, rev snap.Revision) (err error) { + treeRoot := driversTreeDir(kernelSubdir, rev) return os.RemoveAll(treeRoot) } -// EnsureKernelDriversTree creates a tree in -// /var/lib/snapd/kernel// with subfolders can be bind-mounted -// on boot to /usr/lib/{modules,firmware}. This tree contains files from the -// kernel snap mounted on kernelMount, as well as symlinks to it. -// TODO this will be extended to consider kernel-modules components. -func EnsureKernelDriversTree(ksnapName string, rev snap.Revision, kernelMount string) (err error) { +type KernelDriversTreeOptions struct { + // Set if we are building the tree for a kernel we are installing right now + KernelInstall bool +} + +// EnsureKernelDriversTree creates a drivers tree that can include modules/fw +// from kernel-modules components. opts.KernelInstall tells the function if +// this is a kernel install (which might be installing components at the same +// time) or an only components install. +// +// For kernel installs, this function creates a tree in +// /var/lib/snapd/kernel//, which is bind-mounted after a +// reboot to /usr/lib/{modules,firmware} (the currently active kernel is using +// a different path as it has a different revision). This tree contains files +// from the kernel snap mounted on kernelMount, as well as symlinks to it. +// +// For components-only install, we want the components to be available without +// rebooting. For this, we work on a temporary tree, and after finishing it we +// swap atomically the affected modules/firmware folders with those of the +// currently active kernel drivers tree. +func EnsureKernelDriversTree(ksnapName string, rev snap.Revision, kernelMount string, kmodsInfos []*snap.ComponentSideInfo, opts *KernelDriversTreeOptions) (err error) { + // The temporal dir when installing only components can be fixed as a + // task installing/updating a kernel-modules component must conflict + // with changes containing this same task. This helps with clean-ups if + // something goes wrong. Note that this folder needs to be in the same + // filesystem as the final one so we can atomically switch the folders. + ksnapDir := ksnapName + "_tmp" + if opts.KernelInstall { + ksnapDir = ksnapName + } // Initial clean-up to make the function idempotent - if rmErr := RemoveKernelDriversTree(ksnapName, rev); rmErr != nil && + if rmErr := RemoveKernelDriversTree(ksnapDir, rev); rmErr != nil && !errors.Is(err, fs.ErrNotExist) { logger.Noticef("while removing old kernel tree: %v", rmErr) } - // Remove tree in case something goes wrong defer func() { - if err != nil { - if rmErr := RemoveKernelDriversTree(ksnapName, rev); rmErr != nil && - !errors.Is(err, fs.ErrNotExist) { - logger.Noticef("while cleaning up kernel tree: %v", rmErr) - } + // Remove on return if error or if temporary tree + if err == nil && opts.KernelInstall { + return + } + if rmErr := RemoveKernelDriversTree(ksnapDir, rev); rmErr != nil && + !errors.Is(err, fs.ErrNotExist) { + logger.Noticef("while cleaning up kernel tree: %v", rmErr) } }() - // Root of our kernel tree - treeRoot := driversTreeDir(ksnapName, rev) + treeRoot := driversTreeDir(ksnapDir, rev) + + // Create drivers tree + kversion, err := KernelVersionFromModulesDir(kernelMount) + if err == nil { + if err := createModulesSubtree(kernelMount, treeRoot, + kversion, ksnapName, rev, kmodsInfos); err != nil { + return err + } + } else { + // Bit of a corner case, but maybe possible. Log anyway. + // TODO detect this issue in snap pack, should be enforced + // if the snap declares kernel-modules components. + logger.Noticef("no modules found in %q", kernelMount) + } - if err := createModulesSubtree(kernelMount, treeRoot); err != nil { + fwDir := filepath.Join(treeRoot, "lib", "firmware") + if opts.KernelInstall { + // symlinks in /lib/firmware are not affected by components + if err := createFirmwareSymlinks(kernelMount, fwDir); err != nil { + return err + } + } + updateFwDir := filepath.Join(fwDir, "updates") + // This folder needs to exist always to allow for directory swapping + // in the future, even if right now we don't have components. + if err := os.MkdirAll(updateFwDir, 0755); err != nil { return err } + for _, kmi := range kmodsInfos { + compMntDir := filepath.Join(EarlyKernelModsComponentMountDir( + kmi.Component.ComponentName, + kmi.Revision, ksnapName, rev)) + if err := createFirmwareSymlinks(compMntDir, updateFwDir); err != nil { + return err + } + } + + // Sync before returning successfully (install kernel case) and also + // for swapping case so we have consistent content before swapping + // folder. + syscall.Sync() + + if !opts.KernelInstall { + // There is a (very small) chance of a poweroff/reboot while + // having swapped only one of these two folders. If that + // happens, snapd will re-run the task on the next boot, but + // with mismatching modules/fw for the installed components. As + // modules shipped by components should not be that critical, + // in principle the system should recover. + + // Swap modules directories + oldRoot := driversTreeDir(ksnapName, rev) + + // Swap updates directory inside firmware dir + oldFwUpdates := filepath.Join(oldRoot, "lib", "firmware", "updates") + if err := osutil.SwapDirs(oldFwUpdates, updateFwDir); err != nil { + return fmt.Errorf("while swapping %q <-> %q: %w", oldFwUpdates, updateFwDir, err) + } - return createFirmwareSymlinks(kernelMount, treeRoot) + newMods := filepath.Join(treeRoot, "lib", "modules", kversion) + oldMods := filepath.Join(oldRoot, "lib", "modules", kversion) + if err := osutil.SwapDirs(oldMods, newMods); err != nil { + // Undo firmware swap + if err := osutil.SwapDirs(oldFwUpdates, updateFwDir); err != nil { + logger.Noticef("while reverting modules swap: %v", err) + } + return fmt.Errorf("while swapping %q <-> %q: %w", newMods, oldMods, err) + } + + // Make sure that changes are written + syscall.Sync() + } + + return nil } diff --git a/kernel/kernel_drivers_test.go b/kernel/kernel_drivers_test.go index f70b5c551da..b818d05ce63 100644 --- a/kernel/kernel_drivers_test.go +++ b/kernel/kernel_drivers_test.go @@ -33,6 +33,7 @@ import ( "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/naming" "github.com/snapcore/snapd/testutil" ) @@ -135,68 +136,74 @@ func (s *kernelDriversTestSuite) TestBuildKernelDriversTree(c *C) { c.Assert(osutil.FileExists(treeRoot), Equals, false) } +type expectInode struct { + file string + fType fs.FileMode + linkTarget string +} + +func doDirChecks(c *C, dir string, expected []expectInode) { + entries, err := os.ReadDir(dir) + c.Assert(err, IsNil) + c.Assert(len(entries), Equals, len(expected)) + for i, ent := range entries { + c.Check(ent.Name(), Equals, expected[i].file) + c.Check(ent.Type(), Equals, expected[i].fType) + if ent.Type() == fs.ModeSymlink { + dest, err := os.Readlink(filepath.Join(dir, ent.Name())) + c.Assert(err, IsNil) + c.Check(dest, Equals, expected[i].linkTarget) + } + } +} + func testBuildKernelDriversTree(c *C) { mountDir := filepath.Join(dirs.RunDir, "mnt/pc-kernel") kversion := "5.15.0-78-generic" createKernelSnapFiles(c, kversion, mountDir) // Now build the tree - c.Assert(kernel.EnsureKernelDriversTree("pc-kernel", snap.R(1), mountDir), IsNil) + c.Assert(kernel.EnsureKernelDriversTree("pc-kernel", snap.R(1), mountDir, nil, + &kernel.KernelDriversTreeOptions{KernelInstall: true}), IsNil) // Check content is as expected modsRoot := filepath.Join(dirs.SnapdStateDir(dirs.GlobalRootDir), "kernel", "pc-kernel", "1", "lib", "modules", kversion) - - entries, err := os.ReadDir(modsRoot) - c.Assert(err, IsNil) - type expectInode struct { - file string - fType fs.FileMode - linkTarget string - } modsMntDir := filepath.Join(mountDir, "modules", kversion) expected := []expectInode{ {"kernel", fs.ModeSymlink, filepath.Join(modsMntDir, "kernel")}, {"modules.builtin.alias.bin", 0, ""}, {"modules.dep.bin", 0, ""}, {"modules.symbols", 0, ""}, + {"updates", fs.ModeDir, ""}, {"vdso", fs.ModeSymlink, filepath.Join(modsMntDir, "vdso")}, } - c.Assert(len(entries), Equals, len(expected)) - for i, ent := range entries { - c.Check(ent.Name(), Equals, expected[i].file) - c.Check(ent.Type(), Equals, expected[i].fType) - if ent.Type() == fs.ModeSymlink { - dest, err := os.Readlink(filepath.Join(modsRoot, ent.Name())) - c.Assert(err, IsNil) - c.Check(dest, Equals, expected[i].linkTarget) - } - } + doDirChecks(c, modsRoot, expected) // Check firmware entries fwRoot := filepath.Join(dirs.SnapdStateDir(dirs.GlobalRootDir), "kernel", "pc-kernel", "1", "lib", "firmware") fwMntDir := filepath.Join(mountDir, "firmware") - entries, err = os.ReadDir(fwRoot) - c.Assert(err, IsNil) expected = []expectInode{ {"blob1", fs.ModeSymlink, filepath.Join(fwMntDir, "blob1")}, {"blob2", fs.ModeSymlink, filepath.Join(fwMntDir, "blob2")}, {"ln_to_blob3", fs.ModeSymlink, "subdir/blob3"}, {"subdir", fs.ModeSymlink, filepath.Join(fwMntDir, "subdir")}, + {"updates", fs.ModeDir, ""}, } - for i, ent := range entries { - c.Check(ent.Name(), Equals, expected[i].file) - c.Check(ent.Type(), Equals, expected[i].fType) - // Must all be symlinks - dest, err := os.Readlink(filepath.Join(fwRoot, ent.Name())) + doDirChecks(c, fwRoot, expected) + + // Check symlinks to files point to real files + for _, ln := range []string{ + filepath.Join(fwRoot, "blob1"), + filepath.Join(fwRoot, "blob2"), + filepath.Join(fwRoot, "ln_to_blob3"), + filepath.Join(fwRoot, "subdir/blob3"), + } { + path, err := filepath.EvalSymlinks(ln) c.Assert(err, IsNil) - c.Check(dest, Equals, expected[i].linkTarget) - if ent.Name() == "subdir" { - // File can be accessed from symlink to directory - exists, isReg, err := osutil.RegularFileExists(filepath.Join(fwRoot, ent.Name(), "blob3")) - c.Assert(err, IsNil) - c.Check(exists, Equals, true) - c.Check(isReg, Equals, true) - } + exists, isReg, err := osutil.RegularFileExists(path) + c.Assert(err, IsNil) + c.Check(exists, Equals, true) + c.Check(isReg, Equals, true) } } @@ -208,7 +215,8 @@ func (s *kernelDriversTestSuite) TestBuildKernelDriversNoModsOrFw(c *C) { c.Assert(os.MkdirAll(mountDir, 0755), IsNil) // Build the tree should not fail - err := kernel.EnsureKernelDriversTree("pc-kernel", snap.R(1), mountDir) + err := kernel.EnsureKernelDriversTree("pc-kernel", snap.R(1), mountDir, + nil, &kernel.KernelDriversTreeOptions{KernelInstall: true}) c.Assert(err, IsNil) // but log should warn about this @@ -234,7 +242,8 @@ func (s *kernelDriversTestSuite) TestBuildKernelDriversOnlyMods(c *C) { createKernelSnapFilesOnlyModules(c, kversion, mountDir) // Build the tree should not fail - err := kernel.EnsureKernelDriversTree("pc-kernel", snap.R(1), mountDir) + err := kernel.EnsureKernelDriversTree("pc-kernel", snap.R(1), mountDir, nil, + &kernel.KernelDriversTreeOptions{KernelInstall: true}) c.Assert(err, IsNil) // check created file @@ -265,7 +274,8 @@ func (s *kernelDriversTestSuite) TestBuildKernelDriversOnlyFw(c *C) { createKernelSnapFilesOnlyFw(c, mountDir) // Build the tree should not fail - err := kernel.EnsureKernelDriversTree("pc-kernel", snap.R(1), mountDir) + err := kernel.EnsureKernelDriversTree("pc-kernel", snap.R(1), mountDir, + nil, &kernel.KernelDriversTreeOptions{KernelInstall: true}) c.Assert(err, IsNil) // check link @@ -286,7 +296,8 @@ func (s *kernelDriversTestSuite) TestBuildKernelDriversAbsFwSymlink(c *C) { os.Symlink("/absdir/blob3", filepath.Join(fwDir, "ln_to_abs")) // Fails on the absolute path in the link - err := kernel.EnsureKernelDriversTree("pc-kernel", snap.R(1), mountDir) + err := kernel.EnsureKernelDriversTree("pc-kernel", snap.R(1), mountDir, + nil, &kernel.KernelDriversTreeOptions{KernelInstall: true}) c.Assert(err, ErrorMatches, `symlink \".*lib/firmware/ln_to_abs\" points to absolute path \"/absdir/blob3\"`) // Make sure the tree has been deleted @@ -305,7 +316,8 @@ func (s *kernelDriversTestSuite) TestBuildKernelDriversTreeCleanup(c *C) { defer restore() // Now build the tree - err := kernel.EnsureKernelDriversTree("pc-kernel", snap.R(1), mountDir) + err := kernel.EnsureKernelDriversTree("pc-kernel", snap.R(1), mountDir, + nil, &kernel.KernelDriversTreeOptions{KernelInstall: true}) c.Assert(err, ErrorMatches, "mocked symlink error") // Make sure the tree has been deleted @@ -323,10 +335,167 @@ func (s *kernelDriversTestSuite) TestBuildKernelDriversBadFileType(c *C) { c.Assert(syscall.Mkfifo(filepath.Join(fwDir, "fifo"), 0666), IsNil) // Now build the tree - err := kernel.EnsureKernelDriversTree("pc-kernel", snap.R(1), mountDir) + err := kernel.EnsureKernelDriversTree("pc-kernel", snap.R(1), mountDir, + nil, &kernel.KernelDriversTreeOptions{KernelInstall: true}) c.Assert(err, ErrorMatches, `"fifo" has unexpected file type: p---------`) // Make sure the tree has been deleted treeRoot := filepath.Join(dirs.SnapdStateDir(dirs.GlobalRootDir), "kernel", "pc-kernel", "1") c.Assert(osutil.FileExists(treeRoot), Equals, false) } + +func createKernelModulesCompFiles(c *C, kversion, compdir, filePrefix string) { + c.Assert(os.MkdirAll(compdir, 0755), IsNil) + + // Create some kernel module file + modDir := filepath.Join(compdir, "modules", kversion, "kernel/foo") + c.Assert(os.MkdirAll(modDir, 0755), IsNil) + c.Assert(os.WriteFile(filepath.Join(modDir, filePrefix+".ko.zst"), []byte{}, 0644), IsNil) + + // and some fw + fwDir := filepath.Join(compdir, "firmware") + c.Assert(os.MkdirAll(fwDir, 0755), IsNil) + c.Assert(os.WriteFile(filepath.Join(fwDir, filePrefix+".bin"), []byte{}, 0644), IsNil) +} + +func (s *kernelDriversTestSuite) TestBuildKernelDriversTreeWithKernelAndComps(c *C) { + // Build twice to make sure the function is idempotent + opts := &kernel.KernelDriversTreeOptions{KernelInstall: true} + testBuildKernelDriversTreeWithComps(c, opts) + testBuildKernelDriversTreeWithComps(c, opts) + + // Now remove and check + kernel.RemoveKernelDriversTree("pc-kernel", snap.R(1)) + treeRoot := filepath.Join(dirs.SnapdStateDir(dirs.GlobalRootDir), "kernel", "pc-kernel", "1") + c.Assert(osutil.FileExists(treeRoot), Equals, false) +} + +func (s *kernelDriversTestSuite) TestBuildKernelDriversTreeCompsNoKernelInstall(c *C) { + // Kernel needs to have been installed first + testBuildKernelDriversTree(c) + // Build twice to make sure the function is idempotent + opts := &kernel.KernelDriversTreeOptions{KernelInstall: false} + testBuildKernelDriversTreeWithComps(c, opts) + testBuildKernelDriversTreeWithComps(c, opts) + + // Now remove and check + kernel.RemoveKernelDriversTree("pc-kernel", snap.R(1)) + treeRoot := filepath.Join(dirs.SnapdStateDir(dirs.GlobalRootDir), "kernel", "pc-kernel", "1") + c.Assert(osutil.FileExists(treeRoot), Equals, false) + + // No _tmp folder should be around + treeRoot = filepath.Join(dirs.SnapdStateDir(dirs.GlobalRootDir), "kernel", "pc-kernel_tmp", "1") + c.Assert(osutil.FileExists(treeRoot), Equals, false) +} + +func (s *kernelDriversTestSuite) TestBuildKernelDriversTreeCompsNoKernel(c *C) { + mockCmd := testutil.MockCommand(c, "depmod", "") + defer mockCmd.Restore() + + mountDir := filepath.Join(dirs.RunDir, "mnt/pc-kernel") + kversion := "5.15.0-78-generic" + createKernelSnapFiles(c, kversion, mountDir) + + compMntDir1 := filepath.Join(dirs.RunDir, "mnt/kernel-snaps/comp1") + compMntDir2 := filepath.Join(dirs.RunDir, "mnt/kernel-snaps/comp2") + createKernelModulesCompFiles(c, kversion, compMntDir1, "comp1") + createKernelModulesCompFiles(c, kversion, compMntDir2, "comp2") + kmods := []*snap.ComponentSideInfo{ + snap.NewComponentSideInfo(naming.NewComponentRef("pc-kernel", "comp1"), snap.R(11)), + snap.NewComponentSideInfo(naming.NewComponentRef("pc-kernel", "comp2"), snap.R(22)), + } + + // Now build the tree, will fail as no kernel was installed previously + err := kernel.EnsureKernelDriversTree("pc-kernel", snap.R(1), mountDir, kmods, &kernel.KernelDriversTreeOptions{KernelInstall: false}) + c.Assert(err, ErrorMatches, `while swapping .*: no such file or directory`) +} + +func testBuildKernelDriversTreeWithComps(c *C, opts *kernel.KernelDriversTreeOptions) { + mockCmd := testutil.MockCommand(c, "depmod", "") + defer mockCmd.Restore() + + mountDir := filepath.Join(dirs.RunDir, "mnt/pc-kernel") + kversion := "5.15.0-78-generic" + createKernelSnapFiles(c, kversion, mountDir) + + compMntDir1 := filepath.Join(dirs.RunDir, "mnt/kernel-snaps/pc-kernel/components/1/comp1/11") + compMntDir2 := filepath.Join(dirs.RunDir, "mnt/kernel-snaps/pc-kernel/components/1/comp2/22") + createKernelModulesCompFiles(c, kversion, compMntDir1, "comp1") + createKernelModulesCompFiles(c, kversion, compMntDir2, "comp2") + kmods := []*snap.ComponentSideInfo{ + snap.NewComponentSideInfo(naming.NewComponentRef("pc-kernel", "comp1"), snap.R(11)), + snap.NewComponentSideInfo(naming.NewComponentRef("pc-kernel", "comp2"), snap.R(22)), + } + + // Now build the tree + c.Assert(kernel.EnsureKernelDriversTree("pc-kernel", snap.R(1), mountDir, kmods, opts), IsNil) + + ksubdir := "pc-kernel_tmp" + if opts.KernelInstall { + ksubdir = "pc-kernel" + } + treeRoot := filepath.Join(dirs.SnapdStateDir(dirs.GlobalRootDir), "kernel", ksubdir, "1") + c.Assert(mockCmd.Calls(), DeepEquals, [][]string{ + {"depmod", "-b", treeRoot, kversion}, + }) + + // Check modules root dir is as expected + modsRoot := filepath.Join(dirs.SnapdStateDir(dirs.GlobalRootDir), "kernel", "pc-kernel", "1", "lib", "modules", kversion) + modsMntDir := filepath.Join(mountDir, "modules", kversion) + expected := []expectInode{ + {"kernel", fs.ModeSymlink, filepath.Join(modsMntDir, "kernel")}, + {"modules.builtin.alias.bin", 0, ""}, + {"modules.dep.bin", 0, ""}, + {"modules.symbols", 0, ""}, + {"updates", fs.ModeDir, ""}, + {"vdso", fs.ModeSymlink, filepath.Join(modsMntDir, "vdso")}, + } + doDirChecks(c, modsRoot, expected) + + // Check links for modules shipped in components + updatesDir := filepath.Join(modsRoot, "updates") + expected = []expectInode{ + {"comp1", fs.ModeSymlink, filepath.Join(compMntDir1, "modules", kversion)}, + {"comp2", fs.ModeSymlink, filepath.Join(compMntDir2, "modules", kversion)}, + } + doDirChecks(c, updatesDir, expected) + + // Check firmware entries from snap + fwRoot := filepath.Join(dirs.SnapdStateDir(dirs.GlobalRootDir), "kernel", "pc-kernel", "1", "lib", "firmware") + fwMntDir := filepath.Join(mountDir, "firmware") + expected = []expectInode{ + {"blob1", fs.ModeSymlink, filepath.Join(fwMntDir, "blob1")}, + {"blob2", fs.ModeSymlink, filepath.Join(fwMntDir, "blob2")}, + {"ln_to_blob3", fs.ModeSymlink, "subdir/blob3"}, + {"subdir", fs.ModeSymlink, filepath.Join(fwMntDir, "subdir")}, + {"updates", fs.ModeDir, ""}, + } + doDirChecks(c, fwRoot, expected) + + // Check firmware entries from components + fwUpdates := filepath.Join(fwRoot, "updates") + expected = []expectInode{ + {"comp1.bin", fs.ModeSymlink, filepath.Join(compMntDir1, "firmware/comp1.bin")}, + {"comp2.bin", fs.ModeSymlink, filepath.Join(compMntDir2, "firmware/comp2.bin")}, + } + doDirChecks(c, fwUpdates, expected) + + // Check symlinks to files point to real files + for _, ln := range []string{ + filepath.Join(updatesDir, "comp1/kernel/foo/comp1.ko.zst"), + filepath.Join(updatesDir, "comp2/kernel/foo/comp2.ko.zst"), + filepath.Join(fwRoot, "blob1"), + filepath.Join(fwRoot, "blob2"), + filepath.Join(fwRoot, "ln_to_blob3"), + filepath.Join(fwRoot, "subdir/blob3"), + filepath.Join(fwUpdates, "comp1.bin"), + filepath.Join(fwUpdates, "comp2.bin"), + } { + path, err := filepath.EvalSymlinks(ln) + c.Assert(err, IsNil) + exists, isReg, err := osutil.RegularFileExists(path) + c.Assert(err, IsNil) + c.Check(exists, Equals, true) + c.Check(isReg, Equals, true) + } +} diff --git a/osutil/rename.go b/osutil/rename.go new file mode 100644 index 00000000000..37cede42735 --- /dev/null +++ b/osutil/rename.go @@ -0,0 +1,27 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2024 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package osutil + +// SwapDirs swaps oldpath with newpath in an atomic way from the point +// of view of the running system. If it is needed to make the change +// persistent, users should call sync when appropriate. +func SwapDirs(oldpath string, newpath string) (err error) { + return swapDirs(oldpath, newpath) +} diff --git a/osutil/rename_darwin.go b/osutil/rename_darwin.go new file mode 100644 index 00000000000..441ebac0b84 --- /dev/null +++ b/osutil/rename_darwin.go @@ -0,0 +1,25 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2024 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package osutil + +// swapDirsAtomically is not implemented on darwin +func swapDirs(oldpath string, newpath string) (err error) { + return ErrDarwin +} diff --git a/osutil/rename_linux.go b/osutil/rename_linux.go new file mode 100644 index 00000000000..642276b634d --- /dev/null +++ b/osutil/rename_linux.go @@ -0,0 +1,28 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2024 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package osutil + +import "golang.org/x/sys/unix" + +// swapDirs swaps atomically (for the running system) two directories by using +// renameat2 syscall. The directories must be absolute. +func swapDirs(oldpath string, newpath string) (err error) { + return unix.Renameat2(-1, oldpath, -1, newpath, unix.RENAME_EXCHANGE) +} diff --git a/osutil/rename_linux_test.go b/osutil/rename_linux_test.go new file mode 100644 index 00000000000..ecf3bc2d56c --- /dev/null +++ b/osutil/rename_linux_test.go @@ -0,0 +1,50 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2024 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package osutil_test + +import ( + "os" + "path/filepath" + + "github.com/snapcore/snapd/osutil" + "gopkg.in/check.v1" +) + +type renameSuite struct{} + +var _ = check.Suite(renameSuite{}) + +func (renameSuite) TestSwapDirs(c *check.C) { + dir1 := c.MkDir() + dir2 := c.MkDir() + file1 := "file1" + file2 := "file2" + c.Assert(os.WriteFile(filepath.Join(dir1, file1), nil, 0644), check.IsNil) + c.Assert(os.WriteFile(filepath.Join(dir2, file2), nil, 0644), check.IsNil) + + osutil.SwapDirs(dir1, dir2) + + for _, path := range []string{filepath.Join(dir1, file2), filepath.Join(dir2, file1)} { + exists, isreg, err := osutil.RegularFileExists(path) + c.Check(exists, check.Equals, true) + c.Check(isreg, check.Equals, true) + c.Check(err, check.IsNil) + } +} diff --git a/overlord/snapstate/backend/setup.go b/overlord/snapstate/backend/setup.go index 268d4b25061..32a5dc6cd24 100644 --- a/overlord/snapstate/backend/setup.go +++ b/overlord/snapstate/backend/setup.go @@ -156,7 +156,9 @@ func (b Backend) SetupKernelSnap(instanceName string, rev snap.Revision, meter p }() // Build kernel tree that will be mounted from initramfs - return kernel.EnsureKernelDriversTree(instanceName, rev, filepath.Join(dirs.GlobalRootDir, earlyMountDir)) + return kernel.EnsureKernelDriversTree(instanceName, rev, + filepath.Join(dirs.GlobalRootDir, earlyMountDir), + nil, &kernel.KernelDriversTreeOptions{KernelInstall: true}) } func (b Backend) RemoveKernelSnapSetup(instanceName string, rev snap.Revision, meter progress.Meter) error {