From 367ab5610ec4d48dd853b74814ad994d8b7121b1 Mon Sep 17 00:00:00 2001 From: Itxaka <itxaka.garcia@spectrocloud.com> Date: Wed, 9 Apr 2025 11:21:22 +0200 Subject: [PATCH] Implement generic sysext management (#459) --- internal/constants/constants.go | 4 +- pkg/dag/dag_normal_boot.go | 3 ++ pkg/dag/dag_uki_boot.go | 2 +- pkg/state/steps_shared.go | 82 +++++++++++++++++++++++++++++++++ pkg/state/steps_uki.go | 49 -------------------- 5 files changed, 88 insertions(+), 52 deletions(-) diff --git a/internal/constants/constants.go b/internal/constants/constants.go index f6b23f4..50a4d3d 100644 --- a/internal/constants/constants.go +++ b/internal/constants/constants.go @@ -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\"" diff --git a/pkg/dag/dag_normal_boot.go b/pkg/dag/dag_normal_boot.go index 6effa7d..697c753 100644 --- a/pkg/dag/dag_normal_boot.go +++ b/pkg/dag/dag_normal_boot.go @@ -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), diff --git a/pkg/dag/dag_uki_boot.go b/pkg/dag/dag_uki_boot.go index cb6ea1a..d72eca4 100644 --- a/pkg/dag/dag_uki_boot.go +++ b/pkg/dag/dag_uki_boot.go @@ -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") diff --git a/pkg/state/steps_shared.go b/pkg/state/steps_shared.go index 1a59fd1..16844f7 100644 --- a/pkg/state/steps_shared.go +++ b/pkg/state/steps_shared.go @@ -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 { diff --git a/pkg/state/steps_uki.go b/pkg/state/steps_uki.go index a092822..dbe4578 100644 --- a/pkg/state/steps_uki.go +++ b/pkg/state/steps_uki.go @@ -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 - }))...) -}