Implement generic sysext management (#459)

This commit is contained in:
Itxaka 2025-04-09 11:21:22 +02:00 committed by GitHub
parent 6eca30162d
commit 367ab5610e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 88 additions and 52 deletions

View File

@ -102,7 +102,7 @@ const (
OpUkiKcrypt = "uki-unlock"
OpUkiMountLivecd = "mount-livecd"
OpUkiExtractCerts = "extract-certs"
OpUkiCopySysExtensions = "copy-sysextensions"
OpUkiCopySysExtensions = "enable-sysextensions"
UkiLivecdMountPoint = "/run/initramfs/live"
UkiIsoBaseTree = "/run/rootfsbase"
UkiIsoBootImage = "efiboot.img"
@ -116,7 +116,7 @@ const (
PathAppend = "/usr/bin:/usr/sbin:/bin:/sbin"
PATH = "PATH"
DefaultPCR = 11
SourceSysExtDir = "/.extra/sysext/"
SourceSysExtDir = "/var/lib/kairos/extensions/"
DestSysExtDir = "/run/extensions"
VerityCertDir = "/run/verity.d/"
SysextDefaultPolicy = "--image-policy=\"root=verity+signed+absent:usr=verity+signed+absent\""

View File

@ -57,6 +57,9 @@ func RegisterNormalBoot(s *state.State, g *herd.Graph) error {
// Depends on mount binds as that usually mounts COS_PERSISTENT
s.LogIfError(s.MountCustomBindsDagStep(g), "custom binds mount")
//
s.LogIfError(s.EnableSysExtensions(g, herd.WithWeakDeps(cnst.OpMountBind)), "enable sysextensions")
// Write fstab file
s.LogIfError(s.WriteFstabDagStep(g,
herd.WithDeps(cnst.OpMountRoot, cnst.OpDiscoverState, cnst.OpLoadConfig),

View File

@ -66,7 +66,7 @@ func RegisterUKI(s *state.State, g *herd.Graph) error {
// Copy any sysextensions found under cnst.SourceSysExtDir into cnst.DestSysExtDir so its loaded by systemd automatically on start
// always after cnst.OpMountBind stage so we have a persistent cnst.DestSysExtDir
// Note that the loading of the extensions is done by systemd with the systemd-sysext service
s.LogIfError(s.CopySysExtensionsDagStep(g, herd.WithDeps(cnst.OpMountBind)), "copy sysextensions")
s.LogIfError(s.EnableSysExtensions(g, herd.WithWeakDeps(cnst.OpMountBind)), "enable sysextensions")
// run initramfs stage
s.LogIfError(s.InitramfsStageDagStep(g, herd.WeakDeps, herd.WithDeps(cnst.OpMountBind, cnst.OpUkiCopySysExtensions)), "uki initramfs")

View File

@ -397,6 +397,88 @@ func (s *State) MountCustomBindsDagStep(g *herd.Graph, opts ...herd.OpOption) er
)...)
}
// EnableSysExtensions softlinks extensions for the running state from /var/lib/kairos/extensions/$STATE to /run/extensions.
// So when initramfs stage runs and enables systemd-sysext it can load the extensions for a given bootentry.
func (s *State) EnableSysExtensions(g *herd.Graph, opts ...herd.OpOption) error {
return g.Add(cnst.OpUkiCopySysExtensions, append(opts, herd.WithCallback(func(_ context.Context) error {
// If uki and we are not booting from install media then do nothing
if internalUtils.IsUKI() {
if !state.EfiBootFromInstall(internalUtils.Log) {
internalUtils.Log.Debug().Msg("Not copying sysextensions as we think we are booting from removable media")
return nil
}
}
// Not that while we are using the source the /sysroot by using s.path
// the destination dir is actually /run/extensions without any sysroot path appended
// This is because after initramfs finishes it will be moved into the final sysroot automatically
// and the one under /sysroot/run will be shadowed
// create the /run/extensions dir if it does not exist
if _, err := os.Stat(cnst.DestSysExtDir); os.IsNotExist(err) {
err = os.MkdirAll(cnst.DestSysExtDir, 0755)
if err != nil {
internalUtils.Log.Err(err).Msg("Creating sysext dir")
return err
}
}
// At this point the extensions dir should be available
r, err := state.NewRuntimeWithLogger(internalUtils.Log)
if err != nil {
return err
}
var dir string
switch r.BootState {
case state.Active:
dir = fmt.Sprintf("%s/%s", cnst.SourceSysExtDir, "active")
case state.Passive:
dir = fmt.Sprintf("%s/%s", cnst.SourceSysExtDir, "passive")
case state.Recovery:
dir = fmt.Sprintf("%s/%s", cnst.SourceSysExtDir, "recovery")
default:
internalUtils.Log.Debug().Str("state", string(r.BootState)).Msg("Not copying sysextensions as we are not in a state that we know off")
return nil
}
// move to use dir with the full path from here so its simpler
entries, err := os.ReadDir(s.path(dir))
// We don't care if the dir does not exist
if err != nil && !os.IsNotExist(err) {
return nil
}
// If we wanted to use a common dir for extensions used for both entries, here we would do something like:
// commonEntries, _ := os.ReadDir(s.path(fmt.Sprintf("%s/%s", cnst.SourceSysExtDir, "common")))
// entries = append(entries, commonEntries...)
for _, entry := range entries {
if !entry.IsDir() && filepath.Ext(entry.Name()) == ".raw" {
// If the file is a raw file, lets softlink it
if internalUtils.IsUKI() {
// Verify the signature
output, err := internalUtils.CommandWithPath(fmt.Sprintf("systemd-dissect --validate %s %s", cnst.SysextDefaultPolicy, s.path(filepath.Join(dir, entry.Name()))))
if err != nil {
// If the file didn't pass the validation, we don't copy it
internalUtils.Log.Warn().Str("src", s.path(filepath.Join(dir, entry.Name()))).Msg("Sysextension does not pass validation")
internalUtils.Log.Debug().Err(err).Str("src", s.path(filepath.Join(dir, entry.Name()))).Str("output", output).Msg("Validating sysextension")
continue
}
}
// it has to link to the final dir after initramfs, so we avoid setting s.path here for the target
err = os.Symlink(filepath.Join(dir, entry.Name()), filepath.Join(cnst.DestSysExtDir, entry.Name()))
if err != nil {
internalUtils.Log.Err(err).Msg("Creating symlink")
return err
}
internalUtils.Log.Debug().Str("what", entry.Name()).Msg("Enabled sysextension")
}
}
return nil
}))...)
}
// WriteFstabDagStep will add writing the final fstab file with all the mounts
// Depends on everything but weak, so it will still try to write.
func (s *State) WriteFstabDagStep(g *herd.Graph, opts ...herd.OpOption) error {

View File

@ -5,7 +5,6 @@ import (
"encoding/json"
"encoding/pem"
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
@ -612,51 +611,3 @@ func (s *State) ExtractCerts(g *herd.Graph, opts ...herd.OpOption) error {
return nil
}))...)
}
// CopySysExtensionsDagStep Copies extensions from the EFI partitions to the persistent one so they can be started.
func (s *State) CopySysExtensionsDagStep(g *herd.Graph, opts ...herd.OpOption) error {
return g.Add(cnst.OpUkiCopySysExtensions, append(opts, herd.WithCallback(func(_ context.Context) error {
if !state.EfiBootFromInstall(internalUtils.Log) {
internalUtils.Log.Debug().Msg("Not copying sysextensions as we think we are booting from removable media")
return nil
}
// Copy the sys extensions to the rootfs
// Remember that we use s.path for the destination as it adds the future /sysroot prefix
// But for source, we are in initramfs so it should be without the prefix
// return if the source or dest dir is not there
if _, err := os.Stat(cnst.SourceSysExtDir); os.IsNotExist(err) {
internalUtils.Log.Debug().Str("dir", cnst.SourceSysExtDir).Msg("No sysextensions found")
return nil
}
if _, err := os.Stat(s.path(cnst.DestSysExtDir)); os.IsNotExist(err) {
_ = os.MkdirAll(s.path(cnst.DestSysExtDir), 0755)
}
err := filepath.WalkDir(s.path(cnst.SourceSysExtDir), func(_ string, d fs.DirEntry, err error) error {
if d.IsDir() {
return nil
}
src := filepath.Join(cnst.SourceSysExtDir, d.Name())
dest := s.path(filepath.Join(cnst.DestSysExtDir, d.Name()))
// TODO: Use the policy from the system config if exists, otherwise drop to default?
// This is to make it work also in non-uki envs where we might have a relaxed policy
output, err2 := internalUtils.CommandWithPath(fmt.Sprintf("systemd-dissect --validate %s %s", cnst.SysextDefaultPolicy, src))
if err2 != nil {
// If the file didn't pass the validation, we don't copy it
internalUtils.Log.Warn().Str("src", src).Msg("Sysextension does not pass validation")
internalUtils.Log.Debug().Err(err2).Str("src", src).Str("output", output).Msg("Validating sysextension")
return nil
}
// Copy the file to the sys-extensions directory
err2 = internalUtils.Copy(src, dest)
if err != nil {
internalUtils.Log.Err(err2).Str("src", src).Str("dest", dest).Msg("Copying sysextension")
}
internalUtils.Log.Debug().Str("src", src).Str("dest", dest).Msg("Copied sysextension")
return err2
})
return err
}))...)
}