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
-	}))...)
-}