Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
16 changes: 15 additions & 1 deletion overlord/devicestate/devicemgr.go
Original file line number Diff line number Diff line change
Expand Up @@ -1266,6 +1266,20 @@ func (m *DeviceManager) ensureFDE() error {

logger.Trace("ensure", "manager", "DeviceManager", "func", "ensureFDE")

model, err := m.Model()
if err != nil {
if errors.Is(err, state.ErrNoState) {
logger.Debugf("no model is available, skipping ensureFDE")
return nil
}
return err
}

runPostInstallChecks, err := install.CheckHybridQuestingRelease(model)
if err != nil {
return err
}

// FIXME: we should rename to something like "reset lockout"
lockoutResetErr := secbootMarkSuccessful()

Expand All @@ -1276,7 +1290,7 @@ func (m *DeviceManager) ensureFDE() error {
// FIXME: we need to check that a try kernel was attempted here and not attempt
// repair in that case.

if err := fdestateAttemptAutoRepairIfNeeded(m.state, lockoutResetErr); err != nil {
if err := fdestateAttemptAutoRepairIfNeeded(m.state, lockoutResetErr, runPostInstallChecks); err != nil {
return err
}

Expand Down
2 changes: 1 addition & 1 deletion overlord/devicestate/devicestate_cloudinit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func (s *cloudInitBaseSuite) SetUpTest(c *C) {
r := release.MockOnClassic(false)
defer r()

s.AddCleanup(devicestate.MockFdestateAttemptAutoRepairIfNeeded(func(st *state.State, lockoutResetErr error) error {
s.AddCleanup(devicestate.MockFdestateAttemptAutoRepairIfNeeded(func(st *state.State, lockoutResetErr error, runPostInstallChecks bool) error {
return nil
}))

Expand Down
4 changes: 2 additions & 2 deletions overlord/devicestate/devicestate_systems_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3445,9 +3445,9 @@ func mockHelperForEncryptionAvailabilityCheck(s suiteWithAddCleanup, c *C, isSup
s.DeviceManager().SetEncryptionSupportInfoInCacheUnlocked(cacheLabel, encInfo)
}

restore := install.MockSecbootPreinstallCheck(func(ctx context.Context, bootImagePaths []string) (*secboot.PreinstallCheckContext, []secboot.PreinstallErrorDetails, error) {
restore := install.MockSecbootPreinstallCheck(func(ctx context.Context, bootImageFiles []bootloader.BootFile) (*secboot.PreinstallCheckContext, []secboot.PreinstallErrorDetails, error) {
callCnt.checkCnt++
c.Assert(bootImagePaths, HasLen, 3)
c.Assert(bootImageFiles, HasLen, 3)
c.Assert(isSupportedUbuntuHybrid, Equals, true)
if hasTPM {
return preinstallCheckContext, nil, nil
Expand Down
54 changes: 52 additions & 2 deletions overlord/devicestate/devicestate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3797,12 +3797,62 @@ func (s *deviceMgrSuite) TestDeviceManagerStartupCallbacks(c *C) {
}

func (s *deviceMgrSuite) TestDeviceManagerEnsureFDE(c *C) {
s.setHybridModelInState(c)
defer release.MockReleaseInfo(&release.OS{ID: "ubuntu", VersionID: "26.04"})()
defer release.MockOnClassic(true)()

defer devicestate.MockSecbootMarkSuccessful(func() error {
return fmt.Errorf("MarkSuccessful did not work")
})()

called := 0
defer devicestate.MockFdestateAttemptAutoRepairIfNeeded(func(st *state.State, lockoutResetErr error, runPostInstallChecks bool) error {
c.Check(runPostInstallChecks, Equals, true)
c.Check(lockoutResetErr, ErrorMatches, `MarkSuccessful did not work`)
called++
return nil
})()

devicestate.SetSystemMode(s.mgr, "run")
err := devicestate.EnsureFDE(s.mgr)
c.Assert(err, IsNil)
c.Check(called, Equals, 1)
}

func (s *deviceMgrSuite) TestDeviceManagerEnsureFDEClassicNoPostInstallChecks(c *C) {
s.setHybridModelInState(c)
defer release.MockReleaseInfo(&release.OS{ID: "ubuntu", VersionID: "25.04"})()
defer release.MockOnClassic(true)()

defer devicestate.MockSecbootMarkSuccessful(func() error {
return fmt.Errorf("MarkSuccessful did not work")
})()

called := 0
defer devicestate.MockFdestateAttemptAutoRepairIfNeeded(func(st *state.State, lockoutResetErr error, runPostInstallChecks bool) error {
c.Check(runPostInstallChecks, Equals, false)
c.Check(lockoutResetErr, ErrorMatches, `MarkSuccessful did not work`)
called++
return nil
})()

devicestate.SetSystemMode(s.mgr, "run")
err := devicestate.EnsureFDE(s.mgr)
c.Assert(err, IsNil)
c.Check(called, Equals, 1)
}

func (s *deviceMgrSuite) TestDeviceManagerEnsureFDENoPostInstallChecks(c *C) {
s.setUC20PCModelInState(c)
defer release.MockOnClassic(false)()

defer devicestate.MockSecbootMarkSuccessful(func() error {
return fmt.Errorf("MarkSuccessful did not work")
})()

called := 0
defer devicestate.MockFdestateAttemptAutoRepairIfNeeded(func(st *state.State, lockoutResetErr error) error {
defer devicestate.MockFdestateAttemptAutoRepairIfNeeded(func(st *state.State, lockoutResetErr error, runPostInstallChecks bool) error {
c.Check(runPostInstallChecks, Equals, false)
c.Check(lockoutResetErr, ErrorMatches, `MarkSuccessful did not work`)
called++
return nil
Expand All @@ -3820,7 +3870,7 @@ func (s *deviceMgrSuite) TestDeviceManagerEnsureFDEInstall(c *C) {
return fmt.Errorf("unexpected call")
})()

defer devicestate.MockFdestateAttemptAutoRepairIfNeeded(func(st *state.State, lockoutResetErr error) error {
defer devicestate.MockFdestateAttemptAutoRepairIfNeeded(func(st *state.State, lockoutResetErr error, runPostInstallChecks bool) error {
c.Errorf("unexpected call")
return fmt.Errorf("unexpected call")
})()
Expand Down
2 changes: 1 addition & 1 deletion overlord/devicestate/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -751,6 +751,6 @@ func MockOsutilBootID(bootID string) (restore func()) {
return testutil.Mock(&osutilBootID, func() (string, error) { return bootID, nil })
}

func MockFdestateAttemptAutoRepairIfNeeded(f func(st *state.State, locktoutResetErr error) error) (restore func()) {
func MockFdestateAttemptAutoRepairIfNeeded(f func(st *state.State, locktoutResetErr error, runPostInstallChecks bool) error) (restore func()) {
return testutil.Mock(&fdestateAttemptAutoRepairIfNeeded, f)
}
146 changes: 141 additions & 5 deletions overlord/fdestate/autorepair.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,16 @@
package fdestate

import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"time"

"github.com/snapcore/snapd/boot"
"github.com/snapcore/snapd/bootloader"
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/gadget/device"
"github.com/snapcore/snapd/logger"
Expand All @@ -34,8 +39,13 @@ import (
)

var (
bootloaderFind = bootloader.Find

bootReadModeenv = boot.ReadModeenv

secbootProvisionTPM = secboot.ProvisionTPM
secbootShouldAttemptRepair = secboot.ShouldAttemptRepair
secbootPostinstallCheck = secboot.PostinstallCheck

osutilBootID = osutil.BootID
)
Expand All @@ -51,6 +61,10 @@ const (
AutoRepairSuccess AutoRepairResult = "success"
)

const (
postInstallCheckTimeout = 2 * time.Minute
)

type repairState struct {
Result AutoRepairResult `json:"result"`
}
Expand Down Expand Up @@ -97,7 +111,109 @@ func getRepairAttemptResult(st *state.State) (*repairState, error) {
return rs.State, nil
}

func autoRepair(st *state.State) (AutoRepairResult, error) {
func getRunBootChain() ([]bootloader.BootFile, error) {
modeenv, err := bootReadModeenv(dirs.GlobalRootDir)
if err != nil {
return nil, fmt.Errorf("cannot read modeenv: %w", err)
}

rbl, err := bootloaderFind(boot.InitramfsUbuntuSeedDir, &bootloader.Options{
Role: bootloader.RoleRecovery,
})
if err != nil {
return nil, fmt.Errorf("cannot find recovery bootloader: %w", err)
}

tbl, ok := rbl.(bootloader.TrustedAssetsBootloader)
if !ok {
return nil, fmt.Errorf("internal error: recovery bootloader does not support trusted assets")
}

bl, err := bootloaderFind(boot.InitramfsUbuntuBootDir, &bootloader.Options{
Role: bootloader.RoleRunMode,
NoSlashBoot: true,
})
if err != nil {
return nil, fmt.Errorf("cannot find run bootloader: %w", err)
}

ebl, ok := bl.(bootloader.ExtractedRunKernelImageBootloader)
if !ok {
return nil, fmt.Errorf("internal error: run bootloader does not support kernel extraction")
}

info, err := ebl.TryKernel()
if err != nil {
if err == bootloader.ErrNoTryKernelRef {
info, err = ebl.Kernel()
}
if err != nil {
return nil, err
}
}

trustedAssets, err := tbl.TrustedAssets()
if err != nil {
return nil, err
}

kernelPath := info.MountFile()

runModeBootChains, err := tbl.BootChains(bl, kernelPath)
if err != nil {
return nil, err
}

// runModeBootChains is all possible run boot chains, but only one should exist (there
// are legacy boot chains before we registered UEFI boot entries).
// The "BootFile"s for the gadget part points to identifier names instead of real path, so we
// need to resolve those. To resolve those we need to cross check with the modeenv, and then
// find the file in the cache. The last one, is the kernel and should be pointing to the right place.
for _, runModeBootChain := range runModeBootChains {

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.

This loop warrants some comments

var chain []bootloader.BootFile

if len(runModeBootChain) == 0 {
// That is not possible for a boot chain to be size 0, because that would mean there is no
// kernel. We should not ignore this, there are bigger problems.
return nil, fmt.Errorf("internal error: no file in boot chain")
}
Comment thread
valentindavid marked this conversation as resolved.

ignoreChain := false
for _, bf := range runModeBootChain[:len(runModeBootChain)-1] {
path := bf.Path
name, ok := trustedAssets[path]
if !ok {
return nil, fmt.Errorf("internal error: unknown trusted asset %s from boot chain", path)
}
var hashes []string
if bf.Role == bootloader.RoleRecovery {
hashes, ok = modeenv.CurrentTrustedRecoveryBootAssets[name]
} else {
hashes, ok = modeenv.CurrentTrustedBootAssets[name]
}
if !ok {
ignoreChain = true
break
}

// In theory we should only have one hash here. Multiple would be when we are trying

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.

Perhaps test and warn if somehow this is not the case?

// a boot chain, and this should have been cleaned. It should be safe to take the last one (newest).
if len(hashes) > 1 {
logger.Noticef("WARNING: multiple hashes for a trusted boot file were found.")
}
hash := hashes[len(hashes)-1]
p := filepath.Join(dirs.SnapBootAssetsDir, bl.Name(), fmt.Sprintf("%s-%s", name, hash))
chain = append(chain, bootloader.NewBootFile("", p, bf.Role))
}
if !ignoreChain {
return append(chain, runModeBootChain[len(runModeBootChain)-1]), nil
}
}

return nil, fmt.Errorf("cannot find the active boot chain")
}

func autoRepair(st *state.State, runPostInstallChecks bool) (AutoRepairResult, error) {
method, err := device.SealedKeysMethod(dirs.GlobalRootDir)
if err != nil {
return AutoRepairNotAttempted, err
Expand All @@ -106,8 +222,28 @@ func autoRepair(st *state.State) (AutoRepairResult, error) {
switch method {
case device.SealingMethodFDESetupHook:
case device.SealingMethodTPM, device.SealingMethodLegacyTPM:
// FIXME: re-run platform checks (post install checks?)
// Then maybe return AutoRepairFailedEncryptionSupport
if runPostInstallChecks {
images, err := getRunBootChain()
if err != nil {
return AutoRepairNotAttempted, err
}

ctx, cancel := context.WithTimeout(context.Background(), postInstallCheckTimeout)
defer cancel()

if _, details, err := secbootPostinstallCheck(ctx, images); len(details) > 0 || err != nil {
if err != nil {
logger.Noticef("WARNING: could not auto repair keyslots due to failed platform initialization: %v", err)
} else {
var messages []string
for _, detail := range details {
messages = append(messages, fmt.Sprintf("- %s", detail.Message))
}
logger.Noticef("WARNING: could not auto repair keyslots due to failed platform initialization:\n%s", strings.Join(messages, "\n"))
}
return AutoRepairFailedPlatformInit, nil
}
}

lockoutAuthFile := device.TpmLockoutAuthUnder(boot.InstallHostFDESaveDir)
if err := secbootProvisionTPM(secboot.TPMPartialReprovision, lockoutAuthFile); err != nil {
Expand Down Expand Up @@ -143,7 +279,7 @@ func autoRepair(st *state.State) (AutoRepairResult, error) {
// of lockout reset and may attempt to repair keyslots. If the
// auto-repair attempted has already occurred during the current boot,
// this will do nothing.
func AttemptAutoRepairIfNeeded(st *state.State, lockoutResetErr error) error {
func AttemptAutoRepairIfNeeded(st *state.State, lockoutResetErr error, runPostInstallChecks bool) error {
if lockoutResetErr != nil {
// FIXME: we need to either try repair in some cases and save the
// error for the status API
Expand Down Expand Up @@ -188,7 +324,7 @@ func AttemptAutoRepairIfNeeded(st *state.State, lockoutResetErr error) error {
}
}

result, err := autoRepair(st)
result, err := autoRepair(st, runPostInstallChecks)
if err != nil {
return err
}
Expand Down
Loading
Loading