Files
snapd/boot/model.go
Samuele Pedroni d0cfb81163 boot,o/devicestate: protect with a mutex modifying modeeenv and sealing/releasing (#13185)
this should allow to release the global state lock while doing resealing/sealing proper as those are slow operations in fact

* boot: use loadModeenv a bit more consistently and some XXXs

* boot: take a lock around read modeenv/modify(/reaseal) etc

* boot: do not seal without the modeenv associated lock

* boot: do not reseal without the modeenv associated lock

* boot,o/devicestate: introduce Unlocker to unlock global state

have boot.DeviceChange make use of it for a start

* boot: explain a bit more modeenvMu

* overlord: introduce state.Unlocker convenience method

* boot: check that the lock is taken also in bootStateUpdate20.commit
2023-09-20 18:26:12 +02:00

161 lines
5.1 KiB
Go

// -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>.
*
*/
package boot
import (
"fmt"
"path/filepath"
"github.com/snapcore/snapd/asserts"
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/snap"
)
// DeviceChange handles a change of the underlying device. Specifically it can
// be used during remodel when a new device is associated with a new model. The
// encryption keys will be resealed for both models. The device model file which
// is measured during boot will be updated. The recovery systems that belong to
// the old model will no longer be usable.
func DeviceChange(from snap.Device, to snap.Device, unlocker Unlocker) error {
if !to.HasModeenv() {
// nothing useful happens on a non-UC20 system here
return nil
}
modeenvLock()
defer modeenvUnlock()
m, err := loadModeenv()
if err != nil {
return err
}
newModel := to.Model()
oldModel := from.Model()
modified := false
if modelUniqueID(m.TryModelForSealing()) != modelUniqueID(newModel) {
// we either haven't been here yet, or a reboot occurred after
// try model was cleared and modeenv was rewritten
m.setTryModel(newModel)
modified = true
}
if modelUniqueID(m.ModelForSealing()) != modelUniqueID(oldModel) {
// a modeenv with new model was already written, restore
// the 'expected' original state, the model file on disk
// will match one of the models
m.setModel(oldModel)
modified = true
}
if modified {
if err := m.Write(); err != nil {
return err
}
}
// reseal with both models now, such that we'd still be able to boot
// even if there is a reboot before the device/model file is updated, or
// before the final reseal with one model
const expectReseal = true
if err := resealKeyToModeenv(dirs.GlobalRootDir, m, expectReseal, unlocker); err != nil {
// best effort clear the modeenv's try model
m.clearTryModel()
if mErr := m.Write(); mErr != nil {
return fmt.Errorf("%v (restoring modeenv failed: %v)", err, mErr)
}
return err
}
// update the device model file in boot (we may be overwriting the same
// model file if we reached this place before a reboot has occurred)
if err := writeModelToUbuntuBoot(to.Model()); err != nil {
err = fmt.Errorf("cannot write new model file: %v", err)
// the file has not been modified, so just clear the try model
m.clearTryModel()
if mErr := m.Write(); mErr != nil {
return fmt.Errorf("%v (restoring modeenv failed: %v)", err, mErr)
}
return err
}
// now we can update the model to the new one
m.setModel(newModel)
// and clear the try model
m.clearTryModel()
if err := m.Write(); err != nil {
// modeenv has not been written and still contains both the old
// and a new model, but the model file has been modified,
// restore the original model file
if restoreErr := writeModelToUbuntuBoot(from.Model()); restoreErr != nil {
return fmt.Errorf("%v (restoring model failed: %v)", err, restoreErr)
}
// however writing modeenv failed, so trying to clear the model
// and write it again could be pointless, let the failure
// percolate up the stack
return err
}
// past a successful reseal, the old recovery systems become unusable and will
// not be able to access the data anymore
if err := resealKeyToModeenv(dirs.GlobalRootDir, m, expectReseal, unlocker); err != nil {
// resealing failed, but modeenv and the file have been modified
// first restore the modeenv in case we reboot, such that if the
// post reboot code reseals, it will allow both models (in case
// even more reboots occur)
m.setModel(from.Model())
m.setTryModel(newModel)
if mErr := m.Write(); mErr != nil {
return fmt.Errorf("%v (writing modeenv failed: %v)", err, mErr)
}
// restore the original model file (we have resealed for both
// models previously)
if restoreErr := writeModelToUbuntuBoot(from.Model()); restoreErr != nil {
return fmt.Errorf("%v (restoring model failed: %v)", err, restoreErr)
}
// drop the tried model
m.clearTryModel()
if mErr := m.Write(); mErr != nil {
return fmt.Errorf("%v (restoring modeenv failed: %v)", err, mErr)
}
// resealing failed, so no point in trying it again
return err
}
return nil
}
var writeModelToUbuntuBoot = writeModelToUbuntuBootImpl
func writeModelToUbuntuBootImpl(model *asserts.Model) error {
modelPath := filepath.Join(InitramfsUbuntuBootDir, "device/model")
f, err := osutil.NewAtomicFile(modelPath, 0644, 0, osutil.NoChown, osutil.NoChown)
if err != nil {
return err
}
defer f.Cancel()
if err := asserts.NewEncoder(f).Encode(model); err != nil {
return err
}
return f.Commit()
}