Skip to content
Open
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
134 changes: 67 additions & 67 deletions docs/imagecustomizer/api/distribution-support.md

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion toolkit/tools/imagecustomizerapi/previewfeaturetype.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ const (

// PreviewFeatureCreate enables the create command for building new images from scratch.
PreviewFeatureCreate PreviewFeature = "create"

// PreviewFeatureAzureContainerLinux3 enables support for Azure Container Linux 3 images.
PreviewFeatureAzureContainerLinux3 PreviewFeature = "azure-container-linux-3"
)

func (pf PreviewFeature) IsValid() error {
Expand All @@ -57,7 +60,7 @@ func (pf PreviewFeature) IsValid() error {
PreviewFeaturePackageSnapshotTime, PreviewFeatureKdumpBootFiles, PreviewFeatureFedora42,
PreviewFeatureUbuntu2204, PreviewFeatureUbuntu2404, PreviewFeatureBaseConfigs,
PreviewFeatureInputImageOci, PreviewFeatureOutputSelinuxPolicy,
PreviewFeatureBtrfs, PreviewFeatureCreate:
PreviewFeatureBtrfs, PreviewFeatureCreate, PreviewFeatureAzureContainerLinux3:
return nil
default:
return fmt.Errorf("invalid preview feature: %s", pf)
Expand Down
15 changes: 9 additions & 6 deletions toolkit/tools/imagegen/installutils/installutils.go
Original file line number Diff line number Diff line change
Expand Up @@ -717,7 +717,9 @@ func ConfigureUserStartupCommand(installChroot safechroot.ChrootInterface, usern
return
}

func SELinuxUpdateConfig(selinuxMode configuration.SELinux, installChroot safechroot.ChrootInterface) (err error) {
func SELinuxUpdateConfig(selinuxMode configuration.SELinux, installChroot safechroot.ChrootInterface,
selinuxConfigFile string,
) (err error) {
const (
selinuxPattern = "^SELINUX=.*"
)
Expand All @@ -732,17 +734,18 @@ func SELinuxUpdateConfig(selinuxMode configuration.SELinux, installChroot safech
mode = SELinuxConfigDisabled
}

selinuxConfigPath := filepath.Join(installChroot.RootDir(), SELinuxConfigFile)
selinuxConfigPath := filepath.Join(installChroot.RootDir(), selinuxConfigFile)
selinuxProperty := fmt.Sprintf("SELINUX=%s", mode)
err = sed(selinuxPattern, selinuxProperty, "`", selinuxConfigPath)
return
}

func SELinuxRelabelFiles(installChroot safechroot.ChrootInterface, mountPointToFsTypeMap map[string]string, isRootFS bool,
selinuxConfigFile string,
) (err error) {
const (
fileContextBasePath = "etc/selinux/%s/contexts/files/file_contexts"
)
// Derive the file_contexts path from the SELinux config file path.
selinuxDir := filepath.Dir(selinuxConfigFile)
fileContextBasePath := filepath.Join(selinuxDir, "%s", "contexts", "files", "file_contexts")
var listOfMountsToLabel []string

if isRootFS {
Expand All @@ -765,7 +768,7 @@ func SELinuxRelabelFiles(installChroot safechroot.ChrootInterface, mountPointToF
}

// Find the type of policy we want to label with
selinuxConfigPath := filepath.Join(installChroot.RootDir(), SELinuxConfigFile)
selinuxConfigPath := filepath.Join(installChroot.RootDir(), selinuxConfigFile)
stdout, stderr, err := shell.Execute("sed", "-n", "s/^SELINUXTYPE=\\(.*\\)$/\\1/p", selinuxConfigPath)
if err != nil {
err = fmt.Errorf("failed to find an SELINUXTYPE in (%s):\n%w\n%v", selinuxConfigPath, err, stderr)
Expand Down
4 changes: 2 additions & 2 deletions toolkit/tools/pkg/imagecustomizerlib/bootcustomizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,8 +215,8 @@ func (b *BootCustomizer) GetSELinuxMode(buildDir string, imageChroot safechroot.
return selinuxMode, nil
}

// Fallback: Get the SELinux mode from the /etc/selinux/config file.
selinuxMode, err = getSELinuxModeFromConfigFile(imageChroot)
// Fallback: Get the SELinux mode from the SELinux config file.
selinuxMode, err = getSELinuxModeFromConfigFile(imageChroot, b.distroHandler)
if err != nil {
return imagecustomizerapi.SELinuxModeDefault, err
}
Expand Down
2 changes: 1 addition & 1 deletion toolkit/tools/pkg/imagecustomizerlib/customizeos.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ func doOsCustomizations(ctx context.Context, rc *ResolvedConfig, imageConnection
return err
}

err = selinuxSetFiles(ctx, selinuxMode, imageChroot)
err = selinuxSetFiles(ctx, selinuxMode, imageChroot, distroHandler.GetSELinuxConfigFile())
if err != nil {
return err
}
Expand Down
38 changes: 27 additions & 11 deletions toolkit/tools/pkg/imagecustomizerlib/customizeselinux.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ import (
"golang.org/x/sys/unix"
)

const (
// selinuxConfigFileDefault is the path to the SELinux configuration file for most distros.
selinuxConfigFileDefault = "etc/selinux/config"
)

var (
// SELinux-related errors
ErrSELinuxGetCurrentMode = NewImageCustomizerError("SELinux:GetCurrentMode", "failed to get current SELinux mode")
Expand Down Expand Up @@ -81,37 +86,46 @@ func handleSELinux(ctx context.Context, buildDir string, selinuxMode imagecustom
}
}

err = UpdateSELinuxModeInConfigFile(selinuxMode, imageChroot)
if err != nil {
return imagecustomizerapi.SELinuxModeDefault, err
selinuxConfigFile := distroHandler.GetSELinuxConfigFile()
if distroHandler.IsSELinuxConfigFileReadOnly() {
// The SELinux config file is on a read-only partition (e.g. dm-verity /usr on ACL).
// The mode has already been applied via the kernel command line above; skip the file update.
logger.Log.Debugf("Skipping SELinux config file update: %s is on a read-only mount", selinuxConfigFile)
} else {
err = UpdateSELinuxModeInConfigFile(selinuxMode, imageChroot, selinuxConfigFile)
if err != nil {
return imagecustomizerapi.SELinuxModeDefault, err
}
}

return selinuxMode, nil
}

func UpdateSELinuxModeInConfigFile(selinuxMode imagecustomizerapi.SELinuxMode, imageChroot safechroot.ChrootInterface) error {
func UpdateSELinuxModeInConfigFile(selinuxMode imagecustomizerapi.SELinuxMode, imageChroot safechroot.ChrootInterface,
selinuxConfigFile string,
) error {
imagerSELinuxMode, err := selinuxModeToImager(selinuxMode)
if err != nil {
return err
}

selinuxConfigFileFullPath := filepath.Join(imageChroot.RootDir(), installutils.SELinuxConfigFile)
selinuxConfigFileFullPath := filepath.Join(imageChroot.RootDir(), selinuxConfigFile)
selinuxConfigFileExists, err := file.PathExists(selinuxConfigFileFullPath)
if err != nil {
return fmt.Errorf("%w (file='%s'):\n%w", ErrSELinuxConfigFileCheck, installutils.SELinuxConfigFile, err)
return fmt.Errorf("%w (file='/%s'):\n%w", ErrSELinuxConfigFileCheck, selinuxConfigFile, err)
}

// Ensure an SELinux policy has been installed.
// Typically, this is provided by the 'selinux-policy' package.
if selinuxMode != imagecustomizerapi.SELinuxModeDisabled && !selinuxConfigFileExists {
return fmt.Errorf("%w (file='%s'):\n"+
return fmt.Errorf("%w (file='/%s'):\n"+
"please ensure an SELinux policy is installed:\n"+
"the '%s' package provides the default policy",
ErrSELinuxPolicyMissing, installutils.SELinuxConfigFile, configuration.SELinuxPolicyDefault)
ErrSELinuxPolicyMissing, selinuxConfigFile, configuration.SELinuxPolicyDefault)
}

if selinuxConfigFileExists {
err = installutils.SELinuxUpdateConfig(imagerSELinuxMode, imageChroot)
err = installutils.SELinuxUpdateConfig(imagerSELinuxMode, imageChroot, selinuxConfigFile)
if err != nil {
return fmt.Errorf("%w:\n%w", ErrSELinuxConfigUpdate, err)
}
Expand All @@ -120,7 +134,9 @@ func UpdateSELinuxModeInConfigFile(selinuxMode imagecustomizerapi.SELinuxMode, i
return nil
}

func selinuxSetFiles(ctx context.Context, selinuxMode imagecustomizerapi.SELinuxMode, imageChroot *safechroot.Chroot) error {
func selinuxSetFiles(ctx context.Context, selinuxMode imagecustomizerapi.SELinuxMode, imageChroot *safechroot.Chroot,
selinuxConfigFile string,
) error {
if selinuxMode == imagecustomizerapi.SELinuxModeDisabled {
// SELinux is disabled in the kernel command line.
// So, no need to call setfiles.
Expand All @@ -144,7 +160,7 @@ func selinuxSetFiles(ctx context.Context, selinuxMode imagecustomizerapi.SELinux
}

// Set the SELinux config file and relabel all the files.
err := installutils.SELinuxRelabelFiles(imageChroot, mountPointToFsTypeMap, false)
err := installutils.SELinuxRelabelFiles(imageChroot, mountPointToFsTypeMap, false, selinuxConfigFile)
if err != nil {
return fmt.Errorf("%w:\n%w", ErrSELinuxRelabelFiles, err)
}
Expand Down
131 changes: 106 additions & 25 deletions toolkit/tools/pkg/imagecustomizerlib/customizeuki.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,13 +192,22 @@ func extractAndSaveUkiCmdline(buildDir string, imageChroot *safechroot.Chroot, d
addonDirPath := filepath.Join(filepath.Dir(ukiFile), fmt.Sprintf("%s.extra.d", ukiFileName))
addonFilePath := filepath.Join(addonDirPath, fmt.Sprintf("%s.addon.efi", kernelName))

if _, err := os.Stat(addonFilePath); os.IsNotExist(err) {
return fmt.Errorf("addon file does not exist: %s", addonFilePath)
}

cmdline, err := extractCmdlineFromSinglePE(addonFilePath, buildDir)
if err != nil {
return fmt.Errorf("failed to extract cmdline from addon (%s):\n%w", addonFilePath, err)
var cmdline string
_, statErr := os.Stat(addonFilePath)
if statErr == nil {
Comment thread
liulanze marked this conversation as resolved.
cmdline, err = extractCmdlineFromSinglePE(addonFilePath, buildDir)
if err != nil {
return fmt.Errorf("failed to extract cmdline from addon (%s):\n%w", addonFilePath, err)
}
} else if os.IsNotExist(statErr) {
if !distroHandler.AllowsMissingUkiAddon() {
return fmt.Errorf("IC addon not found at (%s): expected addon file is missing", addonFilePath)
}
// No IC-managed addon yet (e.g., ACL has only oem/firstboot addons on first run).
// Start with empty cmdline; modifyUkiAddon will create the addon.
logger.Log.Infof("No IC addon found at (%s); a new addon will be created with user-specified args", addonFilePath)
} else {
return fmt.Errorf("failed to stat addon file (%s):\n%w", addonFilePath, statErr)
}

// In modify mode, we don't have initramfs info, so leave it empty
Expand Down Expand Up @@ -318,13 +327,31 @@ func prepareUkiHelper(ctx context.Context, buildDir string, uki *imagecustomizer
return fmt.Errorf("%w:\n%w", ErrUKICleanBootDir, err)
}

// Combine kernel-to-initramfs mapping and kernel command line arguments into a single structure.
// Combine kernel-to-initramfs mapping and kernel command line arguments.
// Prefer existing uki-kernel-info.json (may have been modified by handleBootLoader/handleSELinux).
cmdlineFilePath := filepath.Join(buildDir, UkiBuildDir, UkiKernelInfoJson)
existingKernelInfo, existErr := readUkiKernelInfoFile(cmdlineFilePath)

kernelInfo := make(map[string]UkiKernelInfo)

for kernel, initramfs := range kernelToInitramfs {
cmdline, exists := kernelToArgs[kernel]
if !exists {
return fmt.Errorf("no command line arguments found for kernel (%s)", kernel)
var cmdline string
if existErr == nil {
if existingInfo, ok := existingKernelInfo[kernel]; ok {
cmdline = existingInfo.Cmdline
} else {
var exists bool
cmdline, exists = kernelToArgs[kernel]
if !exists {
return fmt.Errorf("no command line arguments found for kernel (%s)", kernel)
}
}
} else {
var exists bool
cmdline, exists = kernelToArgs[kernel]
if !exists {
return fmt.Errorf("no command line arguments found for kernel (%s)", kernel)
}
}

kernelInfo[kernel] = UkiKernelInfo{
Expand All @@ -334,7 +361,6 @@ func prepareUkiHelper(ctx context.Context, buildDir string, uki *imagecustomizer
}

// Dump kernel information to a file in buildDir.
cmdlineFilePath := filepath.Join(buildDir, UkiBuildDir, UkiKernelInfoJson)
err = writeUkiKernelInfoFile(cmdlineFilePath, kernelInfo)
if err != nil {
return fmt.Errorf("%w (path='%s'):\n%w", ErrUKICmdlineFileWrite, cmdlineFilePath, err)
Expand Down Expand Up @@ -382,15 +408,33 @@ func createUkiDirectories(buildDir string, imageChroot *safechroot.Chroot) error
func copyUkiFiles(buildDir string, kernelToInitramfs map[string]string, imageChroot *safechroot.Chroot,
bootConfig BootFilesArchConfig, uki *imagecustomizerapi.Uki,
) error {
// Resolve the EFI stub path
efiStubSrc := filepath.Join(imageChroot.RootDir(), bootConfig.ukiEfiStubBinaryPath)
if _, err := os.Stat(efiStubSrc); err != nil {
return fmt.Errorf("UKI EFI stub not found at %s:\n%w", bootConfig.ukiEfiStubBinaryPath, err)
}

// Resolve the addon stub path, falling back to the main EFI stub
addonStubSrc := filepath.Join(imageChroot.RootDir(), bootConfig.ukiAddonStubBinaryPath)
if _, err := os.Stat(addonStubSrc); err != nil {
logger.Log.Infof("UKI addon stub not found at %s, using main EFI stub as fallback", bootConfig.ukiAddonStubBinaryPath)
addonStubSrc = efiStubSrc
}

// Both create and modify modes need the stub files
filesToCopy := map[string]string{
filepath.Join(imageChroot.RootDir(), bootConfig.ukiEfiStubBinaryPath): filepath.Join(buildDir, UkiBuildDir, bootConfig.ukiEfiStubBinary),
filepath.Join(imageChroot.RootDir(), bootConfig.ukiAddonStubBinaryPath): filepath.Join(buildDir, UkiBuildDir, bootConfig.ukiAddonStubBinary),
efiStubSrc: filepath.Join(buildDir, UkiBuildDir, bootConfig.ukiEfiStubBinary),
addonStubSrc: filepath.Join(buildDir, UkiBuildDir, bootConfig.ukiAddonStubBinary),
}

// Create mode needs additional files (os-release, kernels, initramfs)
if uki == nil || uki.Mode != imagecustomizerapi.UkiModeModify {
filesToCopy[filepath.Join(imageChroot.RootDir(), "/etc/os-release")] = filepath.Join(buildDir, UkiBuildDir, "os-release")
// Try /etc/os-release first, then fall back to /usr/lib/os-release.
osReleaseSrc := filepath.Join(imageChroot.RootDir(), "/etc/os-release")
if _, err := os.Stat(osReleaseSrc); err != nil {
osReleaseSrc = filepath.Join(imageChroot.RootDir(), "/usr/lib/os-release")
}
filesToCopy[osReleaseSrc] = filepath.Join(buildDir, UkiBuildDir, "os-release")

for kernel, initramfs := range kernelToInitramfs {
kernelSource := filepath.Join(imageChroot.RootDir(), BootDir, kernel)
Expand Down Expand Up @@ -554,6 +598,11 @@ func modifyUkiAddon(ukiFilePath string, stubPath string, rc *ResolvedConfig) err
addonDirPath := filepath.Join(filepath.Dir(ukiFilePath), fmt.Sprintf("%s.extra.d", ukiFileName))
addonFullPath := filepath.Join(addonDirPath, fmt.Sprintf("%s.addon.efi", kernelName))

err = os.MkdirAll(addonDirPath, os.ModePerm)
if err != nil {
return fmt.Errorf("failed to create addon directory (%s):\n%w", addonDirPath, err)
}

ukifyCmd := []string{
"build",
fmt.Sprintf("--cmdline=%s", modifiedCmdline),
Expand Down Expand Up @@ -628,6 +677,11 @@ func createUki(ctx context.Context, rc *ResolvedConfig) error {
return fmt.Errorf("%w:\n%w", ErrUKICleanOldFiles, err)
}

err = os.MkdirAll(ukiOutputFullPath, os.ModePerm)
if err != nil {
return fmt.Errorf("failed to create UKI output directory (%s):\n%w", ukiOutputFullPath, err)
}

stubPath := filepath.Join(rc.BuildDirAbs, UkiBuildDir, bootConfig.ukiEfiStubBinary)
addonStubPath := filepath.Join(rc.BuildDirAbs, UkiBuildDir, bootConfig.ukiAddonStubBinary)
osSubreleaseFullPath := filepath.Join(rc.BuildDirAbs, UkiBuildDir, "os-release")
Expand Down Expand Up @@ -1068,19 +1122,29 @@ func extractKernelAndInitramfsFromUkisHelper(ctx context.Context, imageChroot *s
logger.Log.Infof("Successfully extracted kernel and initramfs for version (%s)", kernelVersion)
}

// Regenerate grub.cfg now that kernels are in /boot
logger.Log.Infof("Regenerating grub.cfg after kernel extraction")

// Ensure /boot/grub2 directory exists
grubDir := filepath.Join(imageChroot.RootDir(), filepath.Dir(installutils.FedoraGrubCfgFile))
err = os.MkdirAll(grubDir, 0o755)
// Regenerate grub.cfg now that kernels are in /boot.
// Only needed for GRUB-based distros; systemd-boot distros skip this.
bootloaderType, err := distroHandler.DetectBootloaderType(imageChroot)
if err != nil {
return fmt.Errorf("failed to create grub directory (%s):\n%w", grubDir, err)
return fmt.Errorf("failed to detect bootloader type:\n%w", err)
}

err = installutils.CallGrubMkconfig(imageChroot)
if err != nil {
return fmt.Errorf("failed to regenerate grub.cfg after kernel extraction:\n%w", err)
if bootloaderType == BootloaderTypeGrub {
logger.Log.Infof("Regenerating grub.cfg after kernel extraction")

// Ensure /boot/grub2 directory exists
grubDir := filepath.Join(imageChroot.RootDir(), filepath.Dir(installutils.FedoraGrubCfgFile))
err = os.MkdirAll(grubDir, 0o755)
if err != nil {
return fmt.Errorf("failed to create grub directory (%s):\n%w", grubDir, err)
}

err = installutils.CallGrubMkconfig(imageChroot)
if err != nil {
return fmt.Errorf("failed to regenerate grub.cfg after kernel extraction:\n%w", err)
}
} else {
logger.Log.Infof("Skipping grub.cfg regeneration (non-GRUB bootloader)")
}

return nil
Expand Down Expand Up @@ -1141,6 +1205,23 @@ func cleanBootDirectory(imageChroot *safechroot.Chroot, distroHandler DistroHand
continue
}

// Some distros (e.g. ACL) mount the ESP directly at /boot, so /boot IS
// the ESP. In that case, preserve all directories and non-kernel files —
// only delete standalone kernel images and initramfs archives that would
// normally live directly in /boot on traditional distros.
if distroHandler.PreserveBootDirLayout() {
if entry.IsDir() {
continue
}
name := entry.Name()
isKernelOrInitramfs := strings.HasPrefix(name, "vmlinuz-") ||
strings.HasPrefix(name, "initramfs-") ||
strings.HasPrefix(name, "initrd-")
if !isKernelOrInitramfs {
continue
}
}

err := os.RemoveAll(entryPath)
if err != nil {
return fmt.Errorf("failed to remove (%s):\n%w", entryPath, err)
Expand Down
19 changes: 19 additions & 0 deletions toolkit/tools/pkg/imagecustomizerlib/distrohandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,25 @@ type DistroHandler interface {
// returns the UUID of the partition that contains the grub.cfg.
FindBootPartitionUuidFromEsp(espMountDir string) (string, error)

// GetSELinuxConfigFile returns the path to the SELinux configuration
// file relative to the image root.
GetSELinuxConfigFile() string

// IsSELinuxConfigFileReadOnly reports whether the SELinux config file lives
// on a read-only partition and cannot be written during image customization.
// When true, SELinux mode must be applied via the kernel command line only.
IsSELinuxConfigFileReadOnly() bool

// AllowsMissingUkiAddon reports whether it is valid for the IC-managed UKI
// addon to be absent on the first customization run. When true, a missing
// addon results in an empty base cmdline and the addon is created from scratch.
// When false, a missing addon is an error (invalid image state).
AllowsMissingUkiAddon() bool

// PreserveBootDirLayout reports whether /boot is the ESP itself.
// When true, cleanBootDirectory only removes kernel/initramfs files and preserves all directories.
PreserveBootDirLayout() bool

// Reports whether SELinux configuration is supported by the tool for this distro.
SELinuxSupported() bool

Expand Down
Loading
Loading