diff --git a/cmd/snap-bootstrap/cmd_initramfs_mounts.go b/cmd/snap-bootstrap/cmd_initramfs_mounts.go
index edadde3fb86..cdbbdb6730a 100644
--- a/cmd/snap-bootstrap/cmd_initramfs_mounts.go
+++ b/cmd/snap-bootstrap/cmd_initramfs_mounts.go
@@ -95,7 +95,6 @@ var (
snap.TypeSnapd: "snapd",
}
- secbootProvisionForCVM func(initramfsUbuntuSeedDir string) error
secbootMeasureSnapSystemEpochWhenPossible func() error
secbootMeasureSnapModelWhenPossible func(findModel func() (*asserts.Model, error)) error
secbootUnlockVolumeUsingSealedKeyIfEncrypted func(disk disks.Disk, name string, encryptionKeyFile string, opts *secboot.UnlockVolumeUsingSealedKeyOptions) (secboot.UnlockResult, error)
@@ -1905,80 +1904,6 @@ func maybeMountSave(disk disks.Disk, rootdir string, encrypted bool, mountOpts *
return true, nil
}
-// XXX: workaround for the lack of model in CVM systems
-type genericCVMModel struct{}
-
-func (*genericCVMModel) Classic() bool {
- return true
-}
-
-func (*genericCVMModel) Grade() asserts.ModelGrade {
- return "signed"
-}
-
-func generateMountsModeRunCVM(mst *initramfsMountsState) error {
- // Mount ESP as UbuntuSeedDir which has UEFI label
- if err := mountNonDataPartitionMatchingKernelDisk(boot.InitramfsUbuntuSeedDir, "UEFI"); err != nil {
- return err
- }
-
- // get the disk that we mounted the ESP from as a reference
- // point for future mounts
- disk, err := disks.DiskFromMountPoint(boot.InitramfsUbuntuSeedDir, nil)
- if err != nil {
- return err
- }
-
- // Mount rootfs
- if err := secbootProvisionForCVM(boot.InitramfsUbuntuSeedDir); err != nil {
- return err
- }
- runModeCVMKey := filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "cloudimg-rootfs.sealed-key")
- opts := &secboot.UnlockVolumeUsingSealedKeyOptions{
- AllowRecoveryKey: true,
- }
- unlockRes, err := secbootUnlockVolumeUsingSealedKeyIfEncrypted(disk, "cloudimg-rootfs", runModeCVMKey, opts)
- if err != nil {
- return err
- }
- fsckSystemdOpts := &systemdMountOptions{
- NeedsFsck: true,
- Ephemeral: true,
- }
- if err := doSystemdMount(unlockRes.FsDevice, boot.InitramfsDataDir, fsckSystemdOpts); err != nil {
- return err
- }
-
- // Verify that cloudimg-rootfs comes from where we expect it to
- diskOpts := &disks.Options{}
- if unlockRes.IsEncrypted {
- // then we need to specify that the data mountpoint is
- // expected to be a decrypted device
- diskOpts.IsDecryptedDevice = true
- }
-
- matches, err := disk.MountPointIsFromDisk(boot.InitramfsDataDir, diskOpts)
- if err != nil {
- return err
- }
- if !matches {
- // failed to verify that cloudimg-rootfs mountpoint
- // comes from the same disk as ESP
- return fmt.Errorf("cannot validate boot: cloudimg-rootfs mountpoint is expected to be from disk %s but is not", disk.Dev())
- }
-
- // Unmount ESP because otherwise unmounting is racy and results in booted systems without ESP
- if err := doSystemdMount("", boot.InitramfsUbuntuSeedDir, &systemdMountOptions{Umount: true, Ephemeral: true}); err != nil {
- return err
- }
-
- // There is no real model on a CVM device but minimal model
- // information is required by the later code
- mst.SetVerifiedBootModel(&genericCVMModel{})
-
- return nil
-}
-
func generateMountsModeRun(mst *initramfsMountsState) error {
// 1. mount ubuntu-boot
if err := mountNonDataPartitionMatchingKernelDisk(boot.InitramfsUbuntuBootDir, "ubuntu-boot"); err != nil {
diff --git a/cmd/snap-bootstrap/cmd_initramfs_mounts_cvm.go b/cmd/snap-bootstrap/cmd_initramfs_mounts_cvm.go
new file mode 100644
index 00000000000..b7a0c6ca09e
--- /dev/null
+++ b/cmd/snap-bootstrap/cmd_initramfs_mounts_cvm.go
@@ -0,0 +1,111 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019-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 main
+
+import (
+ "fmt"
+ "path/filepath"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/boot"
+ "github.com/snapcore/snapd/osutil/disks"
+ "github.com/snapcore/snapd/secboot"
+)
+
+var (
+ secbootProvisionForCVM func(initramfsUbuntuSeedDir string) error
+)
+
+// XXX: workaround for the lack of model in CVM systems
+type genericCVMModel struct{}
+
+func (*genericCVMModel) Classic() bool {
+ return true
+}
+
+func (*genericCVMModel) Grade() asserts.ModelGrade {
+ return "signed"
+}
+
+// generateMountsModeRunCVM is used to generate mounts for the special "cloudimg-rootfs" mode which
+// mounts the rootfs from a partition on the disk rather than a base snap. It supports TPM-backed FDE
+// for the rootfs partition using a sealed key from the seed partition.
+func generateMountsModeRunCVM(mst *initramfsMountsState) error {
+ // Mount ESP as UbuntuSeedDir which has UEFI label
+ if err := mountNonDataPartitionMatchingKernelDisk(boot.InitramfsUbuntuSeedDir, "UEFI"); err != nil {
+ return err
+ }
+
+ // get the disk that we mounted the ESP from as a reference
+ // point for future mounts
+ disk, err := disks.DiskFromMountPoint(boot.InitramfsUbuntuSeedDir, nil)
+ if err != nil {
+ return err
+ }
+
+ // Mount rootfs
+ if err := secbootProvisionForCVM(boot.InitramfsUbuntuSeedDir); err != nil {
+ return err
+ }
+ runModeCVMKey := filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "cloudimg-rootfs.sealed-key")
+ opts := &secboot.UnlockVolumeUsingSealedKeyOptions{
+ AllowRecoveryKey: true,
+ }
+ unlockRes, err := secbootUnlockVolumeUsingSealedKeyIfEncrypted(disk, "cloudimg-rootfs", runModeCVMKey, opts)
+ if err != nil {
+ return err
+ }
+ fsckSystemdOpts := &systemdMountOptions{
+ NeedsFsck: true,
+ Ephemeral: true,
+ }
+ if err := doSystemdMount(unlockRes.FsDevice, boot.InitramfsDataDir, fsckSystemdOpts); err != nil {
+ return err
+ }
+
+ // Verify that cloudimg-rootfs comes from where we expect it to
+ diskOpts := &disks.Options{}
+ if unlockRes.IsEncrypted {
+ // then we need to specify that the data mountpoint is
+ // expected to be a decrypted device
+ diskOpts.IsDecryptedDevice = true
+ }
+
+ matches, err := disk.MountPointIsFromDisk(boot.InitramfsDataDir, diskOpts)
+ if err != nil {
+ return err
+ }
+ if !matches {
+ // failed to verify that cloudimg-rootfs mountpoint
+ // comes from the same disk as ESP
+ return fmt.Errorf("cannot validate boot: cloudimg-rootfs mountpoint is expected to be from disk %s but is not", disk.Dev())
+ }
+
+ // Unmount ESP because otherwise unmounting is racy and results in booted systems without ESP
+ if err := doSystemdMount("", boot.InitramfsUbuntuSeedDir, &systemdMountOptions{Umount: true, Ephemeral: true}); err != nil {
+ return err
+ }
+
+ // There is no real model on a CVM device but minimal model
+ // information is required by the later code
+ mst.SetVerifiedBootModel(&genericCVMModel{})
+
+ return nil
+}
diff --git a/cmd/snap-bootstrap/cmd_initramfs_mounts_cvm_test.go b/cmd/snap-bootstrap/cmd_initramfs_mounts_cvm_test.go
new file mode 100644
index 00000000000..09ea09cb171
--- /dev/null
+++ b/cmd/snap-bootstrap/cmd_initramfs_mounts_cvm_test.go
@@ -0,0 +1,173 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019-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 main_test
+
+import (
+ "fmt"
+ "path/filepath"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/boot"
+ main "github.com/snapcore/snapd/cmd/snap-bootstrap"
+ "github.com/snapcore/snapd/osutil/disks"
+ "github.com/snapcore/snapd/secboot"
+ "github.com/snapcore/snapd/testutil"
+)
+
+var (
+ cvmEncPart = disks.Partition{
+ FilesystemLabel: "cloudimg-rootfs-enc",
+ PartitionUUID: "cloudimg-rootfs-enc-partuuid",
+ KernelDeviceNode: "/dev/sda1",
+ }
+
+ defaultCVMDisk = &disks.MockDiskMapping{
+ Structure: []disks.Partition{
+ seedPart,
+ cvmEncPart,
+ },
+ DiskHasPartitions: true,
+ DevNum: "defaultCVMDev",
+ }
+)
+
+type initramfsCVMMountsSuite struct {
+ baseInitramfsMountsSuite
+}
+
+var _ = Suite(&initramfsCVMMountsSuite{})
+
+func (s *initramfsCVMMountsSuite) SetUpTest(c *C) {
+ s.baseInitramfsMountsSuite.SetUpTest(c)
+ s.AddCleanup(main.MockSecbootProvisionForCVM(func(_ string) error {
+ return nil
+ }))
+}
+
+func (s *initramfsCVMMountsSuite) TestInitramfsMountsRunCVMModeHappy(c *C) {
+ s.mockProcCmdlineContent(c, "snapd_recovery_mode=cloudimg-rootfs")
+
+ restore := main.MockPartitionUUIDForBootedKernelDisk("specific-ubuntu-seed-partuuid")
+ defer restore()
+
+ restore = disks.MockMountPointDisksToPartitionMapping(
+ map[disks.Mountpoint]*disks.MockDiskMapping{
+ {Mountpoint: boot.InitramfsUbuntuSeedDir}: defaultCVMDisk,
+ {Mountpoint: boot.InitramfsDataDir, IsDecryptedDevice: true}: defaultCVMDisk,
+ },
+ )
+ defer restore()
+
+ // don't do anything from systemd-mount, we verify the arguments passed at
+ // the end with cmd.Calls
+ cmd := testutil.MockCommand(c, "systemd-mount", ``)
+ defer cmd.Restore()
+
+ // mock that in turn, /run/mnt/ubuntu-boot, /run/mnt/ubuntu-seed, etc. are
+ // mounted
+ n := 0
+ restore = main.MockOsutilIsMounted(func(where string) (bool, error) {
+ n++
+ switch n {
+ // first call for each mount returns false, then returns true, this
+ // tests in the case where systemd is racy / inconsistent and things
+ // aren't mounted by the time systemd-mount returns
+ case 1, 2:
+ c.Assert(where, Equals, boot.InitramfsUbuntuSeedDir)
+ case 3, 4:
+ c.Assert(where, Equals, boot.InitramfsDataDir)
+ case 5, 6:
+ c.Assert(where, Equals, boot.InitramfsUbuntuSeedDir)
+ default:
+ c.Errorf("unexpected IsMounted check on %s", where)
+ return false, fmt.Errorf("unexpected IsMounted check on %s", where)
+ }
+ return n%2 == 0, nil
+ })
+ defer restore()
+
+ // Mock the call to TPMCVM, to ensure that TPM provisioning is
+ // done before unlock attempt
+ provisionTPMCVMCalled := false
+ restore = main.MockSecbootProvisionForCVM(func(_ string) error {
+ // Ensure this function is only called once
+ c.Assert(provisionTPMCVMCalled, Equals, false)
+ provisionTPMCVMCalled = true
+ return nil
+ })
+ defer restore()
+
+ cloudimgActivated := false
+ restore = main.MockSecbootUnlockVolumeUsingSealedKeyIfEncrypted(func(disk disks.Disk, name string, sealedEncryptionKeyFile string, opts *secboot.UnlockVolumeUsingSealedKeyOptions) (secboot.UnlockResult, error) {
+ c.Assert(provisionTPMCVMCalled, Equals, true)
+ c.Assert(name, Equals, "cloudimg-rootfs")
+ c.Assert(sealedEncryptionKeyFile, Equals, filepath.Join(s.tmpDir, "run/mnt/ubuntu-seed/device/fde/cloudimg-rootfs.sealed-key"))
+ c.Assert(opts.AllowRecoveryKey, Equals, true)
+ c.Assert(opts.WhichModel, IsNil)
+
+ cloudimgActivated = true
+ // return true because we are using an encrypted device
+ return happyUnlocked("cloudimg-rootfs", secboot.UnlockedWithSealedKey), nil
+ })
+ defer restore()
+
+ _, err := main.Parser().ParseArgs([]string{"initramfs-mounts"})
+ c.Assert(err, IsNil)
+ c.Check(s.Stdout.String(), Equals, "")
+
+ // 2 per mountpoint + 1 more for cross check
+ c.Assert(n, Equals, 5)
+
+ // failed to use mockSystemdMountSequence way of asserting this
+ // note that other test cases also mix & match using
+ // mockSystemdMountSequence & DeepEquals
+ c.Assert(cmd.Calls(), DeepEquals, [][]string{
+ {
+ "systemd-mount",
+ "/dev/disk/by-partuuid/specific-ubuntu-seed-partuuid",
+ boot.InitramfsUbuntuSeedDir,
+ "--no-pager",
+ "--no-ask-password",
+ "--fsck=yes",
+ "--options=private",
+ "--property=Before=initrd-fs.target",
+ },
+ {
+ "systemd-mount",
+ "/dev/mapper/cloudimg-rootfs-random",
+ boot.InitramfsDataDir,
+ "--no-pager",
+ "--no-ask-password",
+ "--fsck=yes",
+ },
+ {
+ "systemd-mount",
+ boot.InitramfsUbuntuSeedDir,
+ "--umount",
+ "--no-pager",
+ "--no-ask-password",
+ "--fsck=no",
+ },
+ })
+
+ c.Check(provisionTPMCVMCalled, Equals, true)
+ c.Check(cloudimgActivated, Equals, true)
+}
diff --git a/cmd/snap-bootstrap/cmd_initramfs_mounts_test.go b/cmd/snap-bootstrap/cmd_initramfs_mounts_test.go
index 91fa6daef6b..ee771833176 100644
--- a/cmd/snap-bootstrap/cmd_initramfs_mounts_test.go
+++ b/cmd/snap-bootstrap/cmd_initramfs_mounts_test.go
@@ -177,12 +177,6 @@ var (
KernelDeviceNode: "/dev/sda5",
}
- cvmEncPart = disks.Partition{
- FilesystemLabel: "cloudimg-rootfs-enc",
- PartitionUUID: "cloudimg-rootfs-enc-partuuid",
- KernelDeviceNode: "/dev/sda1",
- }
-
// a boot disk without ubuntu-save
defaultBootDisk = &disks.MockDiskMapping{
Structure: []disks.Partition{
@@ -227,15 +221,6 @@ var (
DevNum: "defaultEncDev",
}
- defaultCVMDisk = &disks.MockDiskMapping{
- Structure: []disks.Partition{
- seedPart,
- cvmEncPart,
- },
- DiskHasPartitions: true,
- DevNum: "defaultCVMDev",
- }
-
// a boot disk without ubuntu-seed, which can happen for classic
defaultNoSeedWithSaveDisk = &disks.MockDiskMapping{
Structure: []disks.Partition{
@@ -387,9 +372,6 @@ func (s *baseInitramfsMountsSuite) SetUpTest(c *C) {
// by default mock that we don't have UEFI vars, etc. to get the booted
// kernel partition partition uuid
s.AddCleanup(main.MockPartitionUUIDForBootedKernelDisk(""))
- s.AddCleanup(main.MockSecbootProvisionForCVM(func(_ string) error {
- return nil
- }))
s.AddCleanup(main.MockSecbootMeasureSnapSystemEpochWhenPossible(func() error {
return nil
}))
@@ -2447,116 +2429,6 @@ func (s *initramfsMountsSuite) TestInitramfsMountsRunModeEncryptedDataHappy(c *C
c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, "run-model-measured"), testutil.FilePresent)
}
-func (s *initramfsMountsSuite) TestInitramfsMountsRunCVMModeHappy(c *C) {
- s.mockProcCmdlineContent(c, "snapd_recovery_mode=cloudimg-rootfs")
-
- restore := main.MockPartitionUUIDForBootedKernelDisk("specific-ubuntu-seed-partuuid")
- defer restore()
-
- restore = disks.MockMountPointDisksToPartitionMapping(
- map[disks.Mountpoint]*disks.MockDiskMapping{
- {Mountpoint: boot.InitramfsUbuntuSeedDir}: defaultCVMDisk,
- {Mountpoint: boot.InitramfsDataDir, IsDecryptedDevice: true}: defaultCVMDisk,
- },
- )
- defer restore()
-
- // don't do anything from systemd-mount, we verify the arguments passed at
- // the end with cmd.Calls
- cmd := testutil.MockCommand(c, "systemd-mount", ``)
- defer cmd.Restore()
-
- // mock that in turn, /run/mnt/ubuntu-boot, /run/mnt/ubuntu-seed, etc. are
- // mounted
- n := 0
- restore = main.MockOsutilIsMounted(func(where string) (bool, error) {
- n++
- switch n {
- // first call for each mount returns false, then returns true, this
- // tests in the case where systemd is racy / inconsistent and things
- // aren't mounted by the time systemd-mount returns
- case 1, 2:
- c.Assert(where, Equals, boot.InitramfsUbuntuSeedDir)
- case 3, 4:
- c.Assert(where, Equals, boot.InitramfsDataDir)
- case 5, 6:
- c.Assert(where, Equals, boot.InitramfsUbuntuSeedDir)
- default:
- c.Errorf("unexpected IsMounted check on %s", where)
- return false, fmt.Errorf("unexpected IsMounted check on %s", where)
- }
- return n%2 == 0, nil
- })
- defer restore()
-
- // Mock the call to TPMCVM, to ensure that TPM provisioning is
- // done before unlock attempt
- provisionTPMCVMCalled := false
- restore = main.MockSecbootProvisionForCVM(func(_ string) error {
- // Ensure this function is only called once
- c.Assert(provisionTPMCVMCalled, Equals, false)
- provisionTPMCVMCalled = true
- return nil
- })
- defer restore()
-
- cloudimgActivated := false
- restore = main.MockSecbootUnlockVolumeUsingSealedKeyIfEncrypted(func(disk disks.Disk, name string, sealedEncryptionKeyFile string, opts *secboot.UnlockVolumeUsingSealedKeyOptions) (secboot.UnlockResult, error) {
- c.Assert(provisionTPMCVMCalled, Equals, true)
- c.Assert(name, Equals, "cloudimg-rootfs")
- c.Assert(sealedEncryptionKeyFile, Equals, filepath.Join(s.tmpDir, "run/mnt/ubuntu-seed/device/fde/cloudimg-rootfs.sealed-key"))
- c.Assert(opts.AllowRecoveryKey, Equals, true)
- c.Assert(opts.WhichModel, IsNil)
-
- cloudimgActivated = true
- // return true because we are using an encrypted device
- return happyUnlocked("cloudimg-rootfs", secboot.UnlockedWithSealedKey), nil
- })
- defer restore()
-
- _, err := main.Parser().ParseArgs([]string{"initramfs-mounts"})
- c.Assert(err, IsNil)
- c.Check(s.Stdout.String(), Equals, "")
-
- // 2 per mountpoint + 1 more for cross check
- c.Assert(n, Equals, 5)
-
- // failed to use mockSystemdMountSequence way of asserting this
- // note that other test cases also mix & match using
- // mockSystemdMountSequence & DeepEquals
- c.Assert(cmd.Calls(), DeepEquals, [][]string{
- {
- "systemd-mount",
- "/dev/disk/by-partuuid/specific-ubuntu-seed-partuuid",
- boot.InitramfsUbuntuSeedDir,
- "--no-pager",
- "--no-ask-password",
- "--fsck=yes",
- "--options=private",
- "--property=Before=initrd-fs.target",
- },
- {
- "systemd-mount",
- "/dev/mapper/cloudimg-rootfs-random",
- boot.InitramfsDataDir,
- "--no-pager",
- "--no-ask-password",
- "--fsck=yes",
- },
- {
- "systemd-mount",
- boot.InitramfsUbuntuSeedDir,
- "--umount",
- "--no-pager",
- "--no-ask-password",
- "--fsck=no",
- },
- })
-
- c.Check(provisionTPMCVMCalled, Equals, true)
- c.Check(cloudimgActivated, Equals, true)
-}
-
func (s *initramfsMountsSuite) TestInitramfsMountsRunModeEncryptedDataUnhappyNoSave(c *C) {
s.mockProcCmdlineContent(c, "snapd_recovery_mode=run")