From e6f35e63d33ff97243d2bc1048779167c40c7f8e Mon Sep 17 00:00:00 2001 From: lanzeliu Date: Mon, 27 Apr 2026 14:31:16 -0700 Subject: [PATCH 1/3] Add UKI modify mode, SELinux, and kernel cmdline support for ACL --- .../api/distribution-support.md | 134 +++++++++--------- .../imagecustomizerapi/previewfeaturetype.go | 5 +- .../imagegen/installutils/installutils.go | 15 +- .../pkg/imagecustomizerlib/bootcustomizer.go | 4 +- .../pkg/imagecustomizerlib/customizeos.go | 2 +- .../imagecustomizerlib/customizeselinux.go | 41 ++++-- .../pkg/imagecustomizerlib/customizeuki.go | 128 +++++++++++++---- .../pkg/imagecustomizerlib/distrohandler.go | 8 ++ .../imagecustomizerlib/distrohandler_acl.go | 72 ++++++++-- .../distrohandler_azurelinux.go | 8 ++ .../distrohandler_fedora.go | 8 ++ .../distrohandler_ubuntu.go | 8 ++ .../pkg/imagecustomizerlib/grubcfgutils.go | 13 +- .../pkg/imagecustomizerlib/imagecustomizer.go | 41 +++--- .../pkg/imagecustomizerlib/partitionutils.go | 6 + .../tools/pkg/osmodifierlib/modifierutils.go | 17 ++- 16 files changed, 353 insertions(+), 157 deletions(-) diff --git a/docs/imagecustomizer/api/distribution-support.md b/docs/imagecustomizer/api/distribution-support.md index 7a58f89bf..4fabb88b6 100644 --- a/docs/imagecustomizer/api/distribution-support.md +++ b/docs/imagecustomizer/api/distribution-support.md @@ -10,76 +10,76 @@ The following tables show which APIs are supported for each distribution. APIs marked as **Preview** require the distribution's [previewFeatures](./configuration/config.md#previewfeatures-string) value to be set -(e.g. `ubuntu-22.04`, `ubuntu-24.04`). +(e.g. `ubuntu-22.04`, `ubuntu-24.04`, `azure-container-linux-3`). ## Command line -| Subcommand + Flag | Azure Linux 3.0 | Azure Linux 4.0 | Ubuntu 22.04, 24.04 | -|-----------------------------------------------------------------------------------------------------|:---------------:|:---------------:|:-------------------:| -| [create](./cli/create.md) | Yes | Yes | No | -| [convert](./cli/convert.md) | Yes | Yes | Yes | -| [customize](./cli/customize.md) | Yes | Yes | Preview | -|  [--build-dir](./cli/customize.md#--build-dirdirectory-path) | Yes | Yes | Preview | -|  [--image-file](./cli/customize.md#--image-filefile-path) | Yes | Yes | Preview | -|  [--image](./cli/customize.md#--image) | Yes | Yes | No | -|   `azureLinux:*` | Yes | Yes | N/A | -|   `oci:*` | Yes | Yes | Preview | -|  [--output-image-file](./cli/customize.md#--output-image-filefile-path) | Yes | Yes | Preview | -|  [--output-path](./cli/customize.md#--output-pathfile-path) | Yes | Yes | Preview | -|  [--output-image-format](./cli/customize.md#--output-image-formatformat) | Yes | Yes | Preview | -|   `baremetal-image` | Yes | No | Preview | -|   `cosi` | Yes | No | Preview | -|   `iso` | Yes | No | No | -|   `pxe-dir` | Yes | No | No | -|   `pxe-tar` | Yes | No | No | -|   `qcow2` | Yes | Yes | Preview | -|   `raw` | Yes | Yes | Preview | -|   `vhd-fixed` | Yes | Yes | Preview | -|   `vhd` | Yes | Yes | Preview | -|   `vhdx` | Yes | Yes | Preview | -|  [--cosi-compression-level](./cli/customize.md#--cosi-compression-levellevel) | Yes | No | No | -|  [--output-selinux-policy-path](./cli/customize.md#--output-selinux-policy-pathdirectory-path) | Yes | No | No | -|  [--config-file](./cli/customize.md#--config-filefile-path) | Yes | Yes | Preview | -|  [--rpm-source](./cli/customize.md#--rpm-sourcepath) | Yes | Yes | No | -|  [--disable-base-image-rpm-repos](./cli/customize.md#--disable-base-image-rpm-repos) | Yes | Yes | No | -|  [--package-snapshot-time](./cli/customize.md#--package-snapshot-time) | Yes | No | No | -|  [--image-cache-dir](./cli/customize.md#--image-cache-dir) | Yes | Yes | No | -| [inject-files](./cli/inject-files.md) | Yes | Yes | No | +| Subcommand + Flag | Azure Linux 3.0 | Azure Linux 4.0 | Ubuntu 22.04, 24.04 | Azure Container Linux 3.0 | +|-----------------------------------------------------------------------------------------------------|:---------------:|:---------------:|:-------------------:|:-------------------------:| +| [create](./cli/create.md) | Yes | Yes | No | No | +| [convert](./cli/convert.md) | Yes | Yes | Yes | No | +| [customize](./cli/customize.md) | Yes | Yes | Preview | Preview | +|  [--build-dir](./cli/customize.md#--build-dirdirectory-path) | Yes | Yes | Preview | Preview | +|  [--image-file](./cli/customize.md#--image-filefile-path) | Yes | Yes | Preview | Preview | +|  [--image](./cli/customize.md#--image) | Yes | Yes | No | No | +|   `azureLinux:*` | Yes | Yes | N/A | N/A | +|   `oci:*` | Yes | Yes | Preview | No | +|  [--output-image-file](./cli/customize.md#--output-image-filefile-path) | Yes | Yes | Preview | Preview | +|  [--output-path](./cli/customize.md#--output-pathfile-path) | Yes | Yes | Preview | Preview | +|  [--output-image-format](./cli/customize.md#--output-image-formatformat) | Yes | Yes | Preview | Preview | +|   `baremetal-image` | Yes | No | Preview | No | +|   `cosi` | Yes | No | Preview | No | +|   `iso` | Yes | No | No | No | +|   `pxe-dir` | Yes | No | No | No | +|   `pxe-tar` | Yes | No | No | No | +|   `qcow2` | Yes | Yes | Preview | Preview | +|   `raw` | Yes | Yes | Preview | Preview | +|   `vhd-fixed` | Yes | Yes | Preview | Preview | +|   `vhd` | Yes | Yes | Preview | Preview | +|   `vhdx` | Yes | Yes | Preview | Preview | +|  [--cosi-compression-level](./cli/customize.md#--cosi-compression-levellevel) | Yes | No | No | No | +|  [--output-selinux-policy-path](./cli/customize.md#--output-selinux-policy-pathdirectory-path) | Yes | No | No | No | +|  [--config-file](./cli/customize.md#--config-filefile-path) | Yes | Yes | Preview | Preview | +|  [--rpm-source](./cli/customize.md#--rpm-sourcepath) | Yes | Yes | No | No | +|  [--disable-base-image-rpm-repos](./cli/customize.md#--disable-base-image-rpm-repos) | Yes | Yes | No | No | +|  [--package-snapshot-time](./cli/customize.md#--package-snapshot-time) | Yes | No | No | No | +|  [--image-cache-dir](./cli/customize.md#--image-cache-dir) | Yes | Yes | No | No | +| [inject-files](./cli/inject-files.md) | Yes | Yes | No | No | ## Configuration -| API | Azure Linux 3.0 | Azure Linux 4.0 | Ubuntu 22.04, 24.04 | -|------------------------------------------------------------------------------------------|:---------------------:|:---------------------:|:-------------------:| -| [input.image.path](./configuration/inputImage.md#path-string) | Yes | Yes | Preview | -| [input.image.oci](./configuration/inputImage.md#oci-ociimage) | Yes | Yes | No | -| [input.image.azureLinux](./configuration/inputImage.md#azurelinux-azurelinuximage) | Yes | Yes | N/A | -| [storage](./configuration/config.md#storage-storage) | Yes | No | No | -| [iso](./configuration/config.md#iso-iso) | Yes | No | No | -| [pxe](./configuration/config.md#pxe-pxe) | Yes | No | No | -| [os.hostname](./configuration/os.md#hostname-string) | Yes | Yes | Preview | -| [os.kernelCommandLine](./configuration/os.md#kernelcommandline-kernelcommandline) | Yes | No | No | -| [os.packages](./configuration/os.md#packages-packages) | Yes | Yes | Preview | -|  [.updateExistingPackages](./configuration/packages.md#updateexistingpackages-bool) | Yes | Yes | Preview | -|  [.installLists](./configuration/packages.md#installlists-string) | Yes | Yes | Preview | -|  [.install](./configuration/packages.md#install-string) | Yes | Yes | Preview | -|  [.removeLists](./configuration/packages.md#removelists-string) | Yes | Yes | Preview | -|  [.remove](./configuration/packages.md#remove-string) | Yes | Yes | Preview | -|  [.updateLists](./configuration/packages.md#updatelists-string) | Yes | Yes | Preview | -|  [.update](./configuration/packages.md#update-string) | Yes | Yes | Preview | -|  [.snapshotTime](./configuration/packages.md#snapshottime-string) | Yes | No | No | -| [os.additionalFiles](./configuration/os.md#additionalfiles-additionalfile) | Yes | Yes | Preview | -| [os.additionalDirs](./configuration/os.md#additionaldirs-dirconfig) | Yes | Yes | Preview | -| [os.groups](./configuration/os.md#groups-group) | Yes | Yes | Preview | -| [os.users](./configuration/os.md#users-user) | Yes | Yes | Preview | -| [os.modules](./configuration/os.md#modules-module) | Yes | Yes | Preview | -| [os.services](./configuration/os.md#services-services) | Yes | Yes | Preview | -| [os.overlays](./configuration/os.md#overlays-overlay) | Yes | Yes | No | -| [os.bootloader](./configuration/os.md#bootloader-bootloader) | Yes | No | No | -| [os.uki](./configuration/os.md#uki-uki) | Yes | No | No | -| [os.selinux](./configuration/os.md#selinux-selinux) | Yes | No | No | -| [os.imageHistory](./configuration/os.md#imagehistory-string) | Yes | Yes | Preview | -| [scripts](./configuration/config.md#scripts-scripts) | Yes | Yes | Preview | -| [output.image](./configuration/output.md#image-outputimage) | Yes | Yes | Preview | -| [output.artifacts](./configuration/output.md#artifacts-outputartifacts) | Yes | No | No | -| [output.selinuxPolicyPath](./configuration/output.md#selinuxpolicypath-string) | Yes | No | No | -| [previewFeatures](./configuration/config.md#previewfeatures-string) | Yes | Yes | Yes | +| API | Azure Linux 3.0 | Azure Linux 4.0 | Ubuntu 22.04, 24.04 | Azure Container Linux 3.0 | +|------------------------------------------------------------------------------------------|:---------------------:|:---------------------:|:-------------------:|:-------------------------:| +| [input.image.path](./configuration/inputImage.md#path-string) | Yes | Yes | Preview | Preview | +| [input.image.oci](./configuration/inputImage.md#oci-ociimage) | Yes | Yes | No | No | +| [input.image.azureLinux](./configuration/inputImage.md#azurelinux-azurelinuximage) | Yes | Yes | N/A | N/A | +| [storage](./configuration/config.md#storage-storage) | Yes | No | No | No | +| [iso](./configuration/config.md#iso-iso) | Yes | No | No | No | +| [pxe](./configuration/config.md#pxe-pxe) | Yes | No | No | No | +| [os.hostname](./configuration/os.md#hostname-string) | Yes | Yes | Preview | Preview | +| [os.kernelCommandLine](./configuration/os.md#kernelcommandline-kernelcommandline) | Yes | No | No | Preview | +| [os.packages](./configuration/os.md#packages-packages) | Yes | Yes | Preview | No | +|  [.updateExistingPackages](./configuration/packages.md#updateexistingpackages-bool) | Yes | Yes | Preview | No | +|  [.installLists](./configuration/packages.md#installlists-string) | Yes | Yes | Preview | No | +|  [.install](./configuration/packages.md#install-string) | Yes | Yes | Preview | No | +|  [.removeLists](./configuration/packages.md#removelists-string) | Yes | Yes | Preview | No | +|  [.remove](./configuration/packages.md#remove-string) | Yes | Yes | Preview | No | +|  [.updateLists](./configuration/packages.md#updatelists-string) | Yes | Yes | Preview | No | +|  [.update](./configuration/packages.md#update-string) | Yes | Yes | Preview | No | +|  [.snapshotTime](./configuration/packages.md#snapshottime-string) | Yes | No | No | No | +| [os.additionalFiles](./configuration/os.md#additionalfiles-additionalfile) | Yes | Yes | Preview | Preview | +| [os.additionalDirs](./configuration/os.md#additionaldirs-dirconfig) | Yes | Yes | Preview | Preview | +| [os.groups](./configuration/os.md#groups-group) | Yes | Yes | Preview | Preview | +| [os.users](./configuration/os.md#users-user) | Yes | Yes | Preview | Preview | +| [os.modules](./configuration/os.md#modules-module) | Yes | Yes | Preview | Preview | +| [os.services](./configuration/os.md#services-services) | Yes | Yes | Preview | Preview | +| [os.overlays](./configuration/os.md#overlays-overlay) | Yes | Yes | No | No | +| [os.bootloader](./configuration/os.md#bootloader-bootloader) | Yes | No | No | No | +| [os.uki](./configuration/os.md#uki-uki) | Yes | No | No | Preview | +| [os.selinux](./configuration/os.md#selinux-selinux) | Yes | No | No | Preview | +| [os.imageHistory](./configuration/os.md#imagehistory-string) | Yes | Yes | Preview | Preview | +| [scripts](./configuration/config.md#scripts-scripts) | Yes | Yes | Preview | Preview | +| [output.image](./configuration/output.md#image-outputimage) | Yes | Yes | Preview | Preview | +| [output.artifacts](./configuration/output.md#artifacts-outputartifacts) | Yes | No | No | Preview | +| [output.selinuxPolicyPath](./configuration/output.md#selinuxpolicypath-string) | Yes | No | No | No | +| [previewFeatures](./configuration/config.md#previewfeatures-string) | Yes | Yes | Yes | Yes | diff --git a/toolkit/tools/imagecustomizerapi/previewfeaturetype.go b/toolkit/tools/imagecustomizerapi/previewfeaturetype.go index a993f993b..0e52c4ace 100644 --- a/toolkit/tools/imagecustomizerapi/previewfeaturetype.go +++ b/toolkit/tools/imagecustomizerapi/previewfeaturetype.go @@ -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 { @@ -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) diff --git a/toolkit/tools/imagegen/installutils/installutils.go b/toolkit/tools/imagegen/installutils/installutils.go index b3e949ab6..09dbdffa4 100644 --- a/toolkit/tools/imagegen/installutils/installutils.go +++ b/toolkit/tools/imagegen/installutils/installutils.go @@ -729,7 +729,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=.*" ) @@ -744,17 +746,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 { @@ -777,7 +780,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) diff --git a/toolkit/tools/pkg/imagecustomizerlib/bootcustomizer.go b/toolkit/tools/pkg/imagecustomizerlib/bootcustomizer.go index 6dd4acfd6..13a5990b7 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/bootcustomizer.go +++ b/toolkit/tools/pkg/imagecustomizerlib/bootcustomizer.go @@ -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 } diff --git a/toolkit/tools/pkg/imagecustomizerlib/customizeos.go b/toolkit/tools/pkg/imagecustomizerlib/customizeos.go index 14dd0ad13..8c25636a5 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/customizeos.go +++ b/toolkit/tools/pkg/imagecustomizerlib/customizeos.go @@ -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.GetSELinuxConfigDir()) if err != nil { return err } diff --git a/toolkit/tools/pkg/imagecustomizerlib/customizeselinux.go b/toolkit/tools/pkg/imagecustomizerlib/customizeselinux.go index 504b13aa1..1052c0ec7 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/customizeselinux.go +++ b/toolkit/tools/pkg/imagecustomizerlib/customizeselinux.go @@ -19,6 +19,11 @@ import ( "golang.org/x/sys/unix" ) +const ( + // selinuxConfigDirDefault is the path to the SELinux configuration directory for most distros. + selinuxConfigDirDefault = "etc/selinux" +) + var ( // SELinux-related errors ErrSELinuxGetCurrentMode = NewImageCustomizerError("SELinux:GetCurrentMode", "failed to get current SELinux mode") @@ -81,37 +86,47 @@ func handleSELinux(ctx context.Context, buildDir string, selinuxMode imagecustom } } - err = UpdateSELinuxModeInConfigFile(selinuxMode, imageChroot) - if err != nil { - return imagecustomizerapi.SELinuxModeDefault, err + selinuxConfigDir := distroHandler.GetSELinuxConfigDir() + if isPathOnReadOnlyMount("/"+selinuxConfigDir, imageChroot) { + // The SELinux config dir 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", selinuxConfigDir) + } else { + err = UpdateSELinuxModeInConfigFile(selinuxMode, imageChroot, selinuxConfigDir) + 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, + selinuxConfigDir string, +) error { imagerSELinuxMode, err := selinuxModeToImager(selinuxMode) if err != nil { return err } - selinuxConfigFileFullPath := filepath.Join(imageChroot.RootDir(), installutils.SELinuxConfigFile) + selinuxConfigFile := filepath.Join(selinuxConfigDir, "config") + 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) } @@ -120,7 +135,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, + selinuxConfigDir string, +) error { if selinuxMode == imagecustomizerapi.SELinuxModeDisabled { // SELinux is disabled in the kernel command line. // So, no need to call setfiles. @@ -143,8 +160,10 @@ func selinuxSetFiles(ctx context.Context, selinuxMode imagecustomizerapi.SELinux mountPointToFsTypeMap[mountPoint.GetTarget()] = mountPoint.GetFSType() } + selinuxConfigFile := filepath.Join(selinuxConfigDir, "config") + // 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) } diff --git a/toolkit/tools/pkg/imagecustomizerlib/customizeuki.go b/toolkit/tools/pkg/imagecustomizerlib/customizeuki.go index d0464f93b..f20ca545c 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/customizeuki.go +++ b/toolkit/tools/pkg/imagecustomizerlib/customizeuki.go @@ -192,13 +192,19 @@ 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 { + 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) { + // 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 @@ -318,13 +324,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{ @@ -334,7 +358,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) @@ -382,15 +405,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) @@ -554,6 +595,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), @@ -628,6 +674,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") @@ -1068,19 +1119,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 @@ -1141,6 +1202,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) diff --git a/toolkit/tools/pkg/imagecustomizerlib/distrohandler.go b/toolkit/tools/pkg/imagecustomizerlib/distrohandler.go index 98697ac34..a7b507015 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/distrohandler.go +++ b/toolkit/tools/pkg/imagecustomizerlib/distrohandler.go @@ -59,6 +59,14 @@ type DistroHandler interface { // returns the UUID of the partition that contains the grub.cfg. FindBootPartitionUuidFromEsp(espMountDir string) (string, error) + // GetSELinuxConfigDir returns the path to the SELinux configuration + // directory relative to the image root. + GetSELinuxConfigDir() string + + // 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 diff --git a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_acl.go b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_acl.go index 6e5001a06..5c6eb8a6f 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_acl.go +++ b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_acl.go @@ -7,11 +7,15 @@ import ( "context" "fmt" "io/fs" - "path/filepath" + "slices" + + "github.com/sirupsen/logrus" "github.com/microsoft/azure-linux-image-tools/toolkit/tools/imagecustomizerapi" "github.com/microsoft/azure-linux-image-tools/toolkit/tools/internal/imageconnection" + "github.com/microsoft/azure-linux-image-tools/toolkit/tools/internal/logger" "github.com/microsoft/azure-linux-image-tools/toolkit/tools/internal/safechroot" + "github.com/microsoft/azure-linux-image-tools/toolkit/tools/internal/shell" "github.com/microsoft/azure-linux-image-tools/toolkit/tools/internal/targetos" ) @@ -32,8 +36,9 @@ func (d *aclDistroHandler) GetTargetOs() targetos.TargetOs { } func (d *aclDistroHandler) ValidateConfig(rc *ResolvedConfig) error { - // ACL Phase 0: only mount/recognize/passthrough is supported. - // Block operations that would fail with confusing errors later. + if !slices.Contains(rc.PreviewFeatures, imagecustomizerapi.PreviewFeatureAzureContainerLinux3) { + return ErrAzureContainerLinux3PreviewFeatureRequired + } if rc.Storage.CustomizePartitions() { return fmt.Errorf("storage repartitioning is not yet supported for ACL") @@ -43,12 +48,23 @@ func (d *aclDistroHandler) ValidateConfig(rc *ResolvedConfig) error { return fmt.Errorf("bootloader hard-reset is not supported on ACL (ACL uses systemd-boot, not GRUB)") } - if rc.Uki != nil && rc.Uki.Mode != imagecustomizerapi.UkiModePassthrough { - return fmt.Errorf("only UKI passthrough mode is currently supported for ACL (got %q)", rc.Uki.Mode) - } - - if len(rc.OsKernelCommandLine.ExtraCommandLine) > 0 { - return fmt.Errorf("kernel command line modification is not yet supported for ACL") + for _, configWithBase := range rc.ConfigChain { + os := configWithBase.Config.OS + if os == nil { + continue + } + + pkgs := os.Packages + if len(pkgs.Install) > 0 || len(pkgs.InstallLists) > 0 || + len(pkgs.Remove) > 0 || len(pkgs.RemoveLists) > 0 || + len(pkgs.Update) > 0 || len(pkgs.UpdateLists) > 0 || + pkgs.UpdateExistingPackages { + return fmt.Errorf("package operations are not yet supported for ACL") + } + + if os.Overlays != nil { + return fmt.Errorf("overlays are not yet supported for ACL") + } } return nil @@ -80,7 +96,25 @@ func (d *aclDistroHandler) GetEspDir() string { } func (d *aclDistroHandler) FindBootPartitionUuidFromEsp(espMountDir string) (string, error) { - return readBootPartitionUuidFromGrubCfg(filepath.Join(espMountDir, espGrubCfgPathAzl3), bootPartitionRegexAzl3) + // ACL does not use GRUB and the EFI System Partition IS the boot partition. + // Return an empty UUID to signal that the ESP itself is the boot partition. + return "", nil +} + +func (d *aclDistroHandler) GetSELinuxConfigDir() string { + // ACL uses overlayfs for /etc. At runtime, /etc is composed from the + // immutable lowerdir and a writable upperdir on the ROOT ext4 partition. + // When IC mounts the partitions individually (no overlay), /etc/selinux/ + // does not exist on the bare rootfs — the actual SELinux config lives in + // the overlay lowerdir. + return "usr/share/distro/etc/selinux" +} + +func (d *aclDistroHandler) PreserveBootDirLayout() bool { + // ACL mounts the ESP directly at /boot, so /boot IS the ESP. + // cleanBootDirectory must not delete any directories or unrecognized files + // from /boot — only kernel/initramfs file patterns may be removed. + return true } func (d *aclDistroHandler) SELinuxSupported() bool { @@ -105,7 +139,21 @@ func (d *aclDistroHandler) WriteGrub2ConfigFile(grub2Config string, } func (d *aclDistroHandler) RegenerateInitramfs(ctx context.Context, imageChroot *safechroot.Chroot) error { - return fmt.Errorf("initramfs regeneration is not yet supported for ACL") + logger.Log.Infof("Regenerating initramfs for ACL") + + ctx, span := startRegenerateInitramfsSpan(ctx) + defer span.End() + + err := shell.NewExecBuilder("dracut", "--force", "--regenerate-all"). + LogLevel(logrus.DebugLevel, logrus.DebugLevel). + ErrorStderrLines(1). + Chroot(imageChroot.ChrootDir()). + Execute() + if err != nil { + return fmt.Errorf("failed to rebuild initramfs for ACL:\n%w", err) + } + + return nil } func (d *aclDistroHandler) ConfigureDiskBootLoader(imageConnection *imageconnection.ImageConnection, @@ -113,5 +161,5 @@ func (d *aclDistroHandler) ConfigureDiskBootLoader(imageConnection *imageconnect selinuxConfig imagecustomizerapi.SELinux, kernelCommandLine imagecustomizerapi.KernelCommandLine, currentSELinuxMode imagecustomizerapi.SELinuxMode, newImage bool, ) error { - return fmt.Errorf("bootloader configuration is not yet supported for ACL") + return fmt.Errorf("bootloader configuration is not supported on ACL (systemd-boot auto-discovers UKIs)") } diff --git a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_azurelinux.go b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_azurelinux.go index 59f51980e..e2c507b93 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_azurelinux.go +++ b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_azurelinux.go @@ -119,6 +119,14 @@ func (d *azureLinuxDistroHandler) FindBootPartitionUuidFromEsp(espMountDir strin return readBootPartitionUuidFromGrubCfg(filepath.Join(espMountDir, espGrubCfgPath), bootPartitionRegex) } +func (d *azureLinuxDistroHandler) GetSELinuxConfigDir() string { + return selinuxConfigDirDefault +} + +func (d *azureLinuxDistroHandler) PreserveBootDirLayout() bool { + return false +} + func (d *azureLinuxDistroHandler) SELinuxSupported() bool { return true } diff --git a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_fedora.go b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_fedora.go index 150a2a831..8fe2927e5 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_fedora.go +++ b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_fedora.go @@ -98,6 +98,14 @@ func (d *fedoraDistroHandler) FindBootPartitionUuidFromEsp(espMountDir string) ( return readBootPartitionUuidFromGrubCfg(filepath.Join(espMountDir, espGrubCfgPathAzl3), bootPartitionRegexAzl3) } +func (d *fedoraDistroHandler) GetSELinuxConfigDir() string { + return selinuxConfigDirDefault +} + +func (d *fedoraDistroHandler) PreserveBootDirLayout() bool { + return false +} + func (d *fedoraDistroHandler) SELinuxSupported() bool { return true } diff --git a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_ubuntu.go b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_ubuntu.go index 2b7cd4d4b..67c28aa1b 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_ubuntu.go +++ b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_ubuntu.go @@ -118,6 +118,14 @@ func (d *ubuntuDistroHandler) FindBootPartitionUuidFromEsp(espMountDir string) ( return readBootPartitionUuidFromGrubCfg(filepath.Join(espMountDir, espGrubCfgPathAzl3), bootPartitionRegexAzl3) } +func (d *ubuntuDistroHandler) GetSELinuxConfigDir() string { + return selinuxConfigDirDefault +} + +func (d *ubuntuDistroHandler) PreserveBootDirLayout() bool { + return false +} + func (d *ubuntuDistroHandler) SELinuxSupported() bool { return false } diff --git a/toolkit/tools/pkg/imagecustomizerlib/grubcfgutils.go b/toolkit/tools/pkg/imagecustomizerlib/grubcfgutils.go index 8fab312dc..2ed40e80c 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/grubcfgutils.go +++ b/toolkit/tools/pkg/imagecustomizerlib/grubcfgutils.go @@ -766,21 +766,22 @@ func getSELinuxModeFromLinuxArgsDeferIfMissing(args []grubConfigLinuxArg) (image return imagecustomizerapi.SELinuxModeDisabled, nil } -// Gets the SELinux mode set by the /etc/selinux/config file. -func getSELinuxModeFromConfigFile(imageChroot safechroot.ChrootInterface) (imagecustomizerapi.SELinuxMode, error) { - selinuxConfigFilePath := filepath.Join(imageChroot.RootDir(), installutils.SELinuxConfigFile) +// Gets the SELinux mode set by the SELinux config file. +func getSELinuxModeFromConfigFile(imageChroot safechroot.ChrootInterface, distroHandler DistroHandler) (imagecustomizerapi.SELinuxMode, error) { + selinuxConfigFile := filepath.Join(distroHandler.GetSELinuxConfigDir(), "config") + selinuxConfigFilePath := filepath.Join(imageChroot.RootDir(), selinuxConfigFile) // Read the SELinux config file. selinuxConfig, err := file.Read(selinuxConfigFilePath) if err != nil { return imagecustomizerapi.SELinuxModeDefault, fmt.Errorf("failed to read SELinux config file (%s):\n%w", - installutils.SELinuxConfigFile, err) + selinuxConfigFile, err) } match := selinuxConfigModeRegex.FindStringSubmatch(selinuxConfig) if match == nil { return imagecustomizerapi.SELinuxModeDefault, fmt.Errorf("failed to find SELinux mode in (%s) file", - installutils.SELinuxConfigFile) + selinuxConfigFile) } selinuxConfigMode := match[selinuxConfigModeRegexSELinuxMode] @@ -797,7 +798,7 @@ func getSELinuxModeFromConfigFile(imageChroot safechroot.ChrootInterface) (image default: return imagecustomizerapi.SELinuxModeDefault, fmt.Errorf("unknown SELinux mode (%s) found in (%s) file", - selinuxConfigMode, installutils.SELinuxConfigFile) + selinuxConfigMode, selinuxConfigFile) } } diff --git a/toolkit/tools/pkg/imagecustomizerlib/imagecustomizer.go b/toolkit/tools/pkg/imagecustomizerlib/imagecustomizer.go index 7551bb77f..51b3649c7 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/imagecustomizer.go +++ b/toolkit/tools/pkg/imagecustomizerlib/imagecustomizer.go @@ -26,26 +26,27 @@ import ( var ( // Validation errors - ErrInvalidOutputFormat = NewImageCustomizerError("Validation:InvalidOutputFormat", "invalid output image format") - ErrCannotGenerateOutputFormat = NewImageCustomizerError("Validation:CannotGenerateOutputFormat", "cannot generate output format from input format") - ErrCannotValidateTargetOS = NewImageCustomizerError("Validation:CannotValidateTargetOS", "cannot validate target OS of the base image") - ErrCannotCustomizePartitionsOnIso = NewImageCustomizerError("Validation:CannotCustomizePartitionsOnIso", "cannot customize partitions when input is ISO") - ErrInvalidBaseConfigs = NewImageCustomizerError("Validation:InvalidBaseConfigs", "base configs contain invalid image config") - ErrInvalidImageConfig = NewImageCustomizerError("Validation:InvalidImageConfig", "invalid image config") - ErrInvalidParameters = NewImageCustomizerError("Validation:InvalidParameters", "invalid parameters") - ErrVerityValidation = NewImageCustomizerError("Validation:VerityValidation", "verity validation failed") - ErrUnsupportedQemuImageFormat = NewImageCustomizerError("Validation:UnsupportedQemuImageFormat", "unsupported qemu-img format") - ErrToolNotRunAsRoot = NewImageCustomizerError("Validation:ToolNotRunAsRoot", "tool should be run as root (e.g. by using sudo)") - ErrPackageSnapshotPreviewRequired = NewImageCustomizerError("Validation:PackageSnapshotPreviewRequired", fmt.Sprintf("preview feature '%s' required to specify package snapshot time", imagecustomizerapi.PreviewFeaturePackageSnapshotTime)) - ErrVerityPreviewFeatureRequired = NewImageCustomizerError("Validation:VerityPreviewFeatureRequired", fmt.Sprintf("preview feature '%s' required to customize verity enabled base image", imagecustomizerapi.PreviewFeatureReinitializeVerity)) - ErrFedora42PreviewFeatureRequired = NewImageCustomizerError("Validation:Fedora42PreviewFeatureRequired", fmt.Sprintf("preview feature '%s' required to customize Fedora 42 base image", imagecustomizerapi.PreviewFeatureFedora42)) - ErrUbuntu2204PreviewFeatureRequired = NewImageCustomizerError("Validation:Ubuntu2204PreviewFeatureRequired", fmt.Sprintf("preview feature '%s' required to customize Ubuntu 22.04 base image", imagecustomizerapi.PreviewFeatureUbuntu2204)) - ErrUbuntu2404PreviewFeatureRequired = NewImageCustomizerError("Validation:Ubuntu2404PreviewFeatureRequired", fmt.Sprintf("preview feature '%s' required to customize Ubuntu 24.04 base image", imagecustomizerapi.PreviewFeatureUbuntu2404)) - ErrUbuntuBootLoaderHardReset = NewImageCustomizerError("Validation:UbuntuBootLoaderHardReset", "bootloader hard-reset is not supported for Ubuntu images") - ErrUnsupportedUbuntuFeature = NewImageCustomizerError("Validation:UnsupportedUbuntuFeature", "unsupported feature for Ubuntu images") - ErrInputImageOciPreviewRequired = NewImageCustomizerError("Validation:InputImageOciPreviewRequired", fmt.Sprintf("preview feature '%s' required to specify OCI input image", imagecustomizerapi.PreviewFeatureInputImageOci)) - ErrConvertUnsupportedInputFormat = NewImageCustomizerError("Validation:ConvertUnsupportedInputFormat", "input image format is not supported") - ErrConvertBuildDirRequired = NewImageCustomizerError("Validation:ConvertBuildDirRequired", "build directory is required for cosi and baremetal-image output formats") + ErrInvalidOutputFormat = NewImageCustomizerError("Validation:InvalidOutputFormat", "invalid output image format") + ErrCannotGenerateOutputFormat = NewImageCustomizerError("Validation:CannotGenerateOutputFormat", "cannot generate output format from input format") + ErrCannotValidateTargetOS = NewImageCustomizerError("Validation:CannotValidateTargetOS", "cannot validate target OS of the base image") + ErrCannotCustomizePartitionsOnIso = NewImageCustomizerError("Validation:CannotCustomizePartitionsOnIso", "cannot customize partitions when input is ISO") + ErrInvalidBaseConfigs = NewImageCustomizerError("Validation:InvalidBaseConfigs", "base configs contain invalid image config") + ErrInvalidImageConfig = NewImageCustomizerError("Validation:InvalidImageConfig", "invalid image config") + ErrInvalidParameters = NewImageCustomizerError("Validation:InvalidParameters", "invalid parameters") + ErrVerityValidation = NewImageCustomizerError("Validation:VerityValidation", "verity validation failed") + ErrUnsupportedQemuImageFormat = NewImageCustomizerError("Validation:UnsupportedQemuImageFormat", "unsupported qemu-img format") + ErrToolNotRunAsRoot = NewImageCustomizerError("Validation:ToolNotRunAsRoot", "tool should be run as root (e.g. by using sudo)") + ErrPackageSnapshotPreviewRequired = NewImageCustomizerError("Validation:PackageSnapshotPreviewRequired", fmt.Sprintf("preview feature '%s' required to specify package snapshot time", imagecustomizerapi.PreviewFeaturePackageSnapshotTime)) + ErrVerityPreviewFeatureRequired = NewImageCustomizerError("Validation:VerityPreviewFeatureRequired", fmt.Sprintf("preview feature '%s' required to customize verity enabled base image", imagecustomizerapi.PreviewFeatureReinitializeVerity)) + ErrFedora42PreviewFeatureRequired = NewImageCustomizerError("Validation:Fedora42PreviewFeatureRequired", fmt.Sprintf("preview feature '%s' required to customize Fedora 42 base image", imagecustomizerapi.PreviewFeatureFedora42)) + ErrUbuntu2204PreviewFeatureRequired = NewImageCustomizerError("Validation:Ubuntu2204PreviewFeatureRequired", fmt.Sprintf("preview feature '%s' required to customize Ubuntu 22.04 base image", imagecustomizerapi.PreviewFeatureUbuntu2204)) + ErrUbuntu2404PreviewFeatureRequired = NewImageCustomizerError("Validation:Ubuntu2404PreviewFeatureRequired", fmt.Sprintf("preview feature '%s' required to customize Ubuntu 24.04 base image", imagecustomizerapi.PreviewFeatureUbuntu2404)) + ErrAzureContainerLinux3PreviewFeatureRequired = NewImageCustomizerError("Validation:AzureContainerLinux3PreviewFeatureRequired", fmt.Sprintf("preview feature '%s' required to customize Azure Container Linux 3 base image", imagecustomizerapi.PreviewFeatureAzureContainerLinux3)) + ErrUbuntuBootLoaderHardReset = NewImageCustomizerError("Validation:UbuntuBootLoaderHardReset", "bootloader hard-reset is not supported for Ubuntu images") + ErrUnsupportedUbuntuFeature = NewImageCustomizerError("Validation:UnsupportedUbuntuFeature", "unsupported feature for Ubuntu images") + ErrInputImageOciPreviewRequired = NewImageCustomizerError("Validation:InputImageOciPreviewRequired", fmt.Sprintf("preview feature '%s' required to specify OCI input image", imagecustomizerapi.PreviewFeatureInputImageOci)) + ErrConvertUnsupportedInputFormat = NewImageCustomizerError("Validation:ConvertUnsupportedInputFormat", "input image format is not supported") + ErrConvertBuildDirRequired = NewImageCustomizerError("Validation:ConvertBuildDirRequired", "build directory is required for cosi and baremetal-image output formats") // Generic customization errors ErrGetAbsoluteConfigPath = NewImageCustomizerError("Customizer:GetAbsoluteConfigPath", "failed to get absolute path of config file directory") diff --git a/toolkit/tools/pkg/imagecustomizerlib/partitionutils.go b/toolkit/tools/pkg/imagecustomizerlib/partitionutils.go index 554ae8819..dff4f88ff 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/partitionutils.go +++ b/toolkit/tools/pkg/imagecustomizerlib/partitionutils.go @@ -98,6 +98,12 @@ func findBootPartitionFromEsp(efiSystemPartition *diskutils.PartitionInfo, diskP return nil, fmt.Errorf("failed to read EFI system partition's grub.cfg file:\n%w", err) } + // An empty UUID signals that the ESP itself is the boot partition (e.g. ACL, + // which has no separate /boot and no grub.cfg on the ESP). + if bootPartitionUuid == "" { + return efiSystemPartition, nil + } + // Close the EFI System Partition mount. err = efiSystemPartitionMount.CleanClose() if err != nil { diff --git a/toolkit/tools/pkg/osmodifierlib/modifierutils.go b/toolkit/tools/pkg/osmodifierlib/modifierutils.go index eab7ba275..79cc9f74f 100644 --- a/toolkit/tools/pkg/osmodifierlib/modifierutils.go +++ b/toolkit/tools/pkg/osmodifierlib/modifierutils.go @@ -105,7 +105,7 @@ func doModifications(ctx context.Context, baseConfigPath string, osConfig *osmod } if osConfig.SELinux.Mode != imagecustomizerapi.SELinuxModeDefault { - err = updateSELinuxForGrubBasedBoot(buildDir, osConfig.SELinux.Mode, bootCustomizer, dummyChroot) + err = updateSELinuxForGrubBasedBoot(buildDir, osConfig.SELinux.Mode, bootCustomizer, dummyChroot, distroHandler) if err != nil { return err } @@ -119,7 +119,7 @@ func doModifications(ctx context.Context, baseConfigPath string, osConfig *osmod if osConfig.SELinux.Mode != imagecustomizerapi.SELinuxModeDefault && bootloaderType == imagecustomizerlib.BootloaderTypeSystemdBoot { - err = updateSELinuxForUkiBoot(osConfig.SELinux.Mode, dummyChroot) + err = updateSELinuxForUkiBoot(osConfig.SELinux.Mode, dummyChroot, distroHandler) if err != nil { return err } @@ -183,14 +183,16 @@ func updateGrubConfigForOverlay(overlays []osmodifierapi.Overlay, bootCustomizer return nil } -func updateSELinuxForUkiBoot(selinuxMode imagecustomizerapi.SELinuxMode, installChroot safechroot.ChrootInterface) error { +func updateSELinuxForUkiBoot(selinuxMode imagecustomizerapi.SELinuxMode, installChroot safechroot.ChrootInterface, + distroHandler imagecustomizerlib.DistroHandler, +) error { if selinuxMode == imagecustomizerapi.SELinuxModeDefault { return nil } logger.Log.Infof("Applying SELinux mode ('%s') for UKI-based system", selinuxMode) - err := imagecustomizerlib.UpdateSELinuxModeInConfigFile(selinuxMode, installChroot) + err := imagecustomizerlib.UpdateSELinuxModeInConfigFile(selinuxMode, installChroot, distroHandler.GetSELinuxConfigDir()) if err != nil { return fmt.Errorf("failed to update SELinux mode in config file: %w", err) } @@ -198,7 +200,10 @@ func updateSELinuxForUkiBoot(selinuxMode imagecustomizerapi.SELinuxMode, install return nil } -func updateSELinuxForGrubBasedBoot(buildDir string, selinuxMode imagecustomizerapi.SELinuxMode, bootCustomizer *imagecustomizerlib.BootCustomizer, installChroot safechroot.ChrootInterface) error { +func updateSELinuxForGrubBasedBoot(buildDir string, selinuxMode imagecustomizerapi.SELinuxMode, + bootCustomizer *imagecustomizerlib.BootCustomizer, installChroot safechroot.ChrootInterface, + distroHandler imagecustomizerlib.DistroHandler, +) error { currentSELinuxMode, err := bootCustomizer.GetSELinuxMode(buildDir, installChroot) if err != nil { return fmt.Errorf("failed to get current SELinux mode: %w", err) @@ -215,7 +220,7 @@ func updateSELinuxForGrubBasedBoot(buildDir string, selinuxMode imagecustomizera return fmt.Errorf("failed to update SELinux kernel cmdline: %w", err) } - err = imagecustomizerlib.UpdateSELinuxModeInConfigFile(selinuxMode, installChroot) + err = imagecustomizerlib.UpdateSELinuxModeInConfigFile(selinuxMode, installChroot, distroHandler.GetSELinuxConfigDir()) if err != nil { return fmt.Errorf("failed to update SELinux mode in config file: %w", err) } From 5f7d1fa4495bcdc2db787c1c59babc7ff5cd4ca5 Mon Sep 17 00:00:00 2001 From: lanzeliu Date: Wed, 13 May 2026 21:36:42 -0700 Subject: [PATCH 2/3] Comments addressed. --- .../pkg/imagecustomizerlib/customizeos.go | 2 +- .../imagecustomizerlib/customizeselinux.go | 21 ++++++++----------- .../pkg/imagecustomizerlib/customizeuki.go | 3 +++ .../pkg/imagecustomizerlib/distrohandler.go | 17 ++++++++++++--- .../imagecustomizerlib/distrohandler_acl.go | 16 ++++++++++++-- .../distrohandler_azurelinux.go | 12 +++++++++-- .../distrohandler_fedora.go | 12 +++++++++-- .../distrohandler_ubuntu.go | 12 +++++++++-- .../pkg/imagecustomizerlib/grubcfgutils.go | 2 +- .../tools/pkg/osmodifierlib/modifierutils.go | 4 ++-- 10 files changed, 74 insertions(+), 27 deletions(-) diff --git a/toolkit/tools/pkg/imagecustomizerlib/customizeos.go b/toolkit/tools/pkg/imagecustomizerlib/customizeos.go index 8c25636a5..706a0e735 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/customizeos.go +++ b/toolkit/tools/pkg/imagecustomizerlib/customizeos.go @@ -207,7 +207,7 @@ func doOsCustomizations(ctx context.Context, rc *ResolvedConfig, imageConnection return err } - err = selinuxSetFiles(ctx, selinuxMode, imageChroot, distroHandler.GetSELinuxConfigDir()) + err = selinuxSetFiles(ctx, selinuxMode, imageChroot, distroHandler.GetSELinuxConfigFile()) if err != nil { return err } diff --git a/toolkit/tools/pkg/imagecustomizerlib/customizeselinux.go b/toolkit/tools/pkg/imagecustomizerlib/customizeselinux.go index 1052c0ec7..61557910e 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/customizeselinux.go +++ b/toolkit/tools/pkg/imagecustomizerlib/customizeselinux.go @@ -20,8 +20,8 @@ import ( ) const ( - // selinuxConfigDirDefault is the path to the SELinux configuration directory for most distros. - selinuxConfigDirDefault = "etc/selinux" + // selinuxConfigFileDefault is the path to the SELinux configuration file for most distros. + selinuxConfigFileDefault = "etc/selinux/config" ) var ( @@ -86,13 +86,13 @@ func handleSELinux(ctx context.Context, buildDir string, selinuxMode imagecustom } } - selinuxConfigDir := distroHandler.GetSELinuxConfigDir() - if isPathOnReadOnlyMount("/"+selinuxConfigDir, imageChroot) { - // The SELinux config dir is on a read-only partition (e.g. dm-verity /usr on ACL). + 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", selinuxConfigDir) + logger.Log.Debugf("Skipping SELinux config file update: %s is on a read-only mount", selinuxConfigFile) } else { - err = UpdateSELinuxModeInConfigFile(selinuxMode, imageChroot, selinuxConfigDir) + err = UpdateSELinuxModeInConfigFile(selinuxMode, imageChroot, selinuxConfigFile) if err != nil { return imagecustomizerapi.SELinuxModeDefault, err } @@ -102,14 +102,13 @@ func handleSELinux(ctx context.Context, buildDir string, selinuxMode imagecustom } func UpdateSELinuxModeInConfigFile(selinuxMode imagecustomizerapi.SELinuxMode, imageChroot safechroot.ChrootInterface, - selinuxConfigDir string, + selinuxConfigFile string, ) error { imagerSELinuxMode, err := selinuxModeToImager(selinuxMode) if err != nil { return err } - selinuxConfigFile := filepath.Join(selinuxConfigDir, "config") selinuxConfigFileFullPath := filepath.Join(imageChroot.RootDir(), selinuxConfigFile) selinuxConfigFileExists, err := file.PathExists(selinuxConfigFileFullPath) if err != nil { @@ -136,7 +135,7 @@ func UpdateSELinuxModeInConfigFile(selinuxMode imagecustomizerapi.SELinuxMode, i } func selinuxSetFiles(ctx context.Context, selinuxMode imagecustomizerapi.SELinuxMode, imageChroot *safechroot.Chroot, - selinuxConfigDir string, + selinuxConfigFile string, ) error { if selinuxMode == imagecustomizerapi.SELinuxModeDisabled { // SELinux is disabled in the kernel command line. @@ -160,8 +159,6 @@ func selinuxSetFiles(ctx context.Context, selinuxMode imagecustomizerapi.SELinux mountPointToFsTypeMap[mountPoint.GetTarget()] = mountPoint.GetFSType() } - selinuxConfigFile := filepath.Join(selinuxConfigDir, "config") - // Set the SELinux config file and relabel all the files. err := installutils.SELinuxRelabelFiles(imageChroot, mountPointToFsTypeMap, false, selinuxConfigFile) if err != nil { diff --git a/toolkit/tools/pkg/imagecustomizerlib/customizeuki.go b/toolkit/tools/pkg/imagecustomizerlib/customizeuki.go index f20ca545c..4ee9258c7 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/customizeuki.go +++ b/toolkit/tools/pkg/imagecustomizerlib/customizeuki.go @@ -200,6 +200,9 @@ func extractAndSaveUkiCmdline(buildDir string, imageChroot *safechroot.Chroot, d 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) diff --git a/toolkit/tools/pkg/imagecustomizerlib/distrohandler.go b/toolkit/tools/pkg/imagecustomizerlib/distrohandler.go index a7b507015..030ec754c 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/distrohandler.go +++ b/toolkit/tools/pkg/imagecustomizerlib/distrohandler.go @@ -59,9 +59,20 @@ type DistroHandler interface { // returns the UUID of the partition that contains the grub.cfg. FindBootPartitionUuidFromEsp(espMountDir string) (string, error) - // GetSELinuxConfigDir returns the path to the SELinux configuration - // directory relative to the image root. - GetSELinuxConfigDir() string + // 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. diff --git a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_acl.go b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_acl.go index 5c6eb8a6f..9bd65fa76 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_acl.go +++ b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_acl.go @@ -101,13 +101,25 @@ func (d *aclDistroHandler) FindBootPartitionUuidFromEsp(espMountDir string) (str return "", nil } -func (d *aclDistroHandler) GetSELinuxConfigDir() string { +func (d *aclDistroHandler) GetSELinuxConfigFile() string { // ACL uses overlayfs for /etc. At runtime, /etc is composed from the // immutable lowerdir and a writable upperdir on the ROOT ext4 partition. // When IC mounts the partitions individually (no overlay), /etc/selinux/ // does not exist on the bare rootfs — the actual SELinux config lives in // the overlay lowerdir. - return "usr/share/distro/etc/selinux" + return "usr/share/distro/etc/selinux/config" +} + +func (d *aclDistroHandler) IsSELinuxConfigFileReadOnly() bool { + // ACL's /usr is a btrfs+dm-verity volume and is always mounted read-only, + // so the SELinux config file cannot be written during image customization. + return true +} + +func (d *aclDistroHandler) AllowsMissingUkiAddon() bool { + // ACL ships with oem/firstboot addons but no IC-managed addon on first run. + // A missing addon is valid — IC will create it from scratch. + return true } func (d *aclDistroHandler) PreserveBootDirLayout() bool { diff --git a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_azurelinux.go b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_azurelinux.go index e2c507b93..cd4e80df4 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_azurelinux.go +++ b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_azurelinux.go @@ -119,8 +119,16 @@ func (d *azureLinuxDistroHandler) FindBootPartitionUuidFromEsp(espMountDir strin return readBootPartitionUuidFromGrubCfg(filepath.Join(espMountDir, espGrubCfgPath), bootPartitionRegex) } -func (d *azureLinuxDistroHandler) GetSELinuxConfigDir() string { - return selinuxConfigDirDefault +func (d *azureLinuxDistroHandler) GetSELinuxConfigFile() string { + return selinuxConfigFileDefault +} + +func (d *azureLinuxDistroHandler) IsSELinuxConfigFileReadOnly() bool { + return false +} + +func (d *azureLinuxDistroHandler) AllowsMissingUkiAddon() bool { + return false } func (d *azureLinuxDistroHandler) PreserveBootDirLayout() bool { diff --git a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_fedora.go b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_fedora.go index 8fe2927e5..bb98fac91 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_fedora.go +++ b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_fedora.go @@ -98,8 +98,16 @@ func (d *fedoraDistroHandler) FindBootPartitionUuidFromEsp(espMountDir string) ( return readBootPartitionUuidFromGrubCfg(filepath.Join(espMountDir, espGrubCfgPathAzl3), bootPartitionRegexAzl3) } -func (d *fedoraDistroHandler) GetSELinuxConfigDir() string { - return selinuxConfigDirDefault +func (d *fedoraDistroHandler) GetSELinuxConfigFile() string { + return selinuxConfigFileDefault +} + +func (d *fedoraDistroHandler) IsSELinuxConfigFileReadOnly() bool { + return false +} + +func (d *fedoraDistroHandler) AllowsMissingUkiAddon() bool { + return false } func (d *fedoraDistroHandler) PreserveBootDirLayout() bool { diff --git a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_ubuntu.go b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_ubuntu.go index 67c28aa1b..2b30e5a7b 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_ubuntu.go +++ b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_ubuntu.go @@ -118,8 +118,16 @@ func (d *ubuntuDistroHandler) FindBootPartitionUuidFromEsp(espMountDir string) ( return readBootPartitionUuidFromGrubCfg(filepath.Join(espMountDir, espGrubCfgPathAzl3), bootPartitionRegexAzl3) } -func (d *ubuntuDistroHandler) GetSELinuxConfigDir() string { - return selinuxConfigDirDefault +func (d *ubuntuDistroHandler) GetSELinuxConfigFile() string { + return selinuxConfigFileDefault +} + +func (d *ubuntuDistroHandler) IsSELinuxConfigFileReadOnly() bool { + return false +} + +func (d *ubuntuDistroHandler) AllowsMissingUkiAddon() bool { + return false } func (d *ubuntuDistroHandler) PreserveBootDirLayout() bool { diff --git a/toolkit/tools/pkg/imagecustomizerlib/grubcfgutils.go b/toolkit/tools/pkg/imagecustomizerlib/grubcfgutils.go index 2ed40e80c..1d4c91277 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/grubcfgutils.go +++ b/toolkit/tools/pkg/imagecustomizerlib/grubcfgutils.go @@ -768,7 +768,7 @@ func getSELinuxModeFromLinuxArgsDeferIfMissing(args []grubConfigLinuxArg) (image // Gets the SELinux mode set by the SELinux config file. func getSELinuxModeFromConfigFile(imageChroot safechroot.ChrootInterface, distroHandler DistroHandler) (imagecustomizerapi.SELinuxMode, error) { - selinuxConfigFile := filepath.Join(distroHandler.GetSELinuxConfigDir(), "config") + selinuxConfigFile := distroHandler.GetSELinuxConfigFile() selinuxConfigFilePath := filepath.Join(imageChroot.RootDir(), selinuxConfigFile) // Read the SELinux config file. diff --git a/toolkit/tools/pkg/osmodifierlib/modifierutils.go b/toolkit/tools/pkg/osmodifierlib/modifierutils.go index 79cc9f74f..c14647ce3 100644 --- a/toolkit/tools/pkg/osmodifierlib/modifierutils.go +++ b/toolkit/tools/pkg/osmodifierlib/modifierutils.go @@ -192,7 +192,7 @@ func updateSELinuxForUkiBoot(selinuxMode imagecustomizerapi.SELinuxMode, install logger.Log.Infof("Applying SELinux mode ('%s') for UKI-based system", selinuxMode) - err := imagecustomizerlib.UpdateSELinuxModeInConfigFile(selinuxMode, installChroot, distroHandler.GetSELinuxConfigDir()) + err := imagecustomizerlib.UpdateSELinuxModeInConfigFile(selinuxMode, installChroot, distroHandler.GetSELinuxConfigFile()) if err != nil { return fmt.Errorf("failed to update SELinux mode in config file: %w", err) } @@ -220,7 +220,7 @@ func updateSELinuxForGrubBasedBoot(buildDir string, selinuxMode imagecustomizera return fmt.Errorf("failed to update SELinux kernel cmdline: %w", err) } - err = imagecustomizerlib.UpdateSELinuxModeInConfigFile(selinuxMode, installChroot, distroHandler.GetSELinuxConfigDir()) + err = imagecustomizerlib.UpdateSELinuxModeInConfigFile(selinuxMode, installChroot, distroHandler.GetSELinuxConfigFile()) if err != nil { return fmt.Errorf("failed to update SELinux mode in config file: %w", err) } From 0625c3753220bb6d358feebb327beb0e783ce747 Mon Sep 17 00:00:00 2001 From: lanzeliu Date: Thu, 14 May 2026 12:25:30 -0700 Subject: [PATCH 3/3] Comment addressed. --- .../imagecustomizerlib/customizeselinux.go | 13 ++------ .../pkg/imagecustomizerlib/customizeuki.go | 33 ++++++++++--------- .../pkg/imagecustomizerlib/distrohandler.go | 21 ++++++------ .../imagecustomizerlib/distrohandler_acl.go | 28 +++++++++++----- .../distrohandler_azurelinux.go | 10 +++--- .../distrohandler_fedora.go | 10 +++--- .../distrohandler_ubuntu.go | 10 +++--- .../tools/pkg/osmodifierlib/modifierutils.go | 4 +-- 8 files changed, 71 insertions(+), 58 deletions(-) diff --git a/toolkit/tools/pkg/imagecustomizerlib/customizeselinux.go b/toolkit/tools/pkg/imagecustomizerlib/customizeselinux.go index 61557910e..f101eddc9 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/customizeselinux.go +++ b/toolkit/tools/pkg/imagecustomizerlib/customizeselinux.go @@ -86,16 +86,9 @@ func handleSELinux(ctx context.Context, buildDir string, selinuxMode imagecustom } } - 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 - } + err = distroHandler.UpdateSELinuxConfigFile(selinuxMode, imageChroot) + if err != nil { + return imagecustomizerapi.SELinuxModeDefault, err } return selinuxMode, nil diff --git a/toolkit/tools/pkg/imagecustomizerlib/customizeuki.go b/toolkit/tools/pkg/imagecustomizerlib/customizeuki.go index 4ee9258c7..a9ae996c8 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/customizeuki.go +++ b/toolkit/tools/pkg/imagecustomizerlib/customizeuki.go @@ -172,6 +172,20 @@ func validateUkiMode(imageConnection *imageconnection.ImageConnection, uki *imag return nil } +// defaultExtractUkiAddonCmdline extracts the cmdline from an IC-managed UKI addon. +// Returns an error if the addon file does not exist — callers that support a +// missing addon on first run should implement their own logic instead. +func defaultExtractUkiAddonCmdline(addonFilePath string, buildDir string) (string, error) { + _, err := os.Stat(addonFilePath) + if os.IsNotExist(err) { + return "", fmt.Errorf("IC addon not found at (%s): expected addon file is missing", addonFilePath) + } + if err != nil { + return "", fmt.Errorf("failed to stat addon file (%s):\n%w", addonFilePath, err) + } + return extractCmdlineFromSinglePE(addonFilePath, buildDir) +} + // extractAndSaveUkiCmdline extracts the kernel cmdline from existing UKI addons and saves them to uki-kernel-info.json. func extractAndSaveUkiCmdline(buildDir string, imageChroot *safechroot.Chroot, distroHandler DistroHandler) error { espDir := filepath.Join(imageChroot.RootDir(), distroHandler.GetEspDir()) @@ -192,22 +206,9 @@ 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)) - var cmdline string - _, statErr := os.Stat(addonFilePath) - if statErr == nil { - 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) + cmdline, err := distroHandler.ExtractUkiAddonCmdline(addonFilePath, buildDir) + if err != nil { + return err } // In modify mode, we don't have initramfs info, so leave it empty diff --git a/toolkit/tools/pkg/imagecustomizerlib/distrohandler.go b/toolkit/tools/pkg/imagecustomizerlib/distrohandler.go index 030ec754c..848309ed9 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/distrohandler.go +++ b/toolkit/tools/pkg/imagecustomizerlib/distrohandler.go @@ -63,16 +63,17 @@ type DistroHandler interface { // 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 + // UpdateSELinuxConfigFile writes the given SELinux mode to the distro's + // SELinux config file. Implementations may no-op when the config file + // resides on a read-only partition (e.g. dm-verity /usr on ACL), since + // the mode is already applied via the kernel command line in that case. + UpdateSELinuxConfigFile(selinuxMode imagecustomizerapi.SELinuxMode, imageChroot safechroot.ChrootInterface) error + + // ExtractUkiAddonCmdline returns the current kernel command line from the + // IC-managed UKI addon at addonFilePath. If the addon does not yet exist, + // distros that support a first-run addon-creation flow (e.g. ACL) return an + // empty string; all other distros return an error. + ExtractUkiAddonCmdline(addonFilePath string, buildDir string) (string, error) // PreserveBootDirLayout reports whether /boot is the ESP itself. // When true, cleanBootDirectory only removes kernel/initramfs files and preserves all directories. diff --git a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_acl.go b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_acl.go index 9bd65fa76..f13a36c72 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_acl.go +++ b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_acl.go @@ -7,6 +7,7 @@ import ( "context" "fmt" "io/fs" + "os" "slices" "github.com/sirupsen/logrus" @@ -110,16 +111,27 @@ func (d *aclDistroHandler) GetSELinuxConfigFile() string { return "usr/share/distro/etc/selinux/config" } -func (d *aclDistroHandler) IsSELinuxConfigFileReadOnly() bool { - // ACL's /usr is a btrfs+dm-verity volume and is always mounted read-only, - // so the SELinux config file cannot be written during image customization. - return true +func (d *aclDistroHandler) UpdateSELinuxConfigFile(selinuxMode imagecustomizerapi.SELinuxMode, + imageChroot safechroot.ChrootInterface, +) error { + // ACL's /usr is a btrfs+dm-verity volume and is always mounted read-only. + // The SELinux mode is applied solely via the kernel command line; skip the file update. + logger.Log.Debugf("Skipping SELinux config file update: /usr is read-only on ACL") + return nil } -func (d *aclDistroHandler) AllowsMissingUkiAddon() bool { - // ACL ships with oem/firstboot addons but no IC-managed addon on first run. - // A missing addon is valid — IC will create it from scratch. - return true +func (d *aclDistroHandler) ExtractUkiAddonCmdline(addonFilePath string, buildDir string) (string, error) { + _, statErr := os.Stat(addonFilePath) + if statErr == nil { + return extractCmdlineFromSinglePE(addonFilePath, buildDir) + } + if os.IsNotExist(statErr) { + // ACL ships with oem/firstboot addons but no IC-managed addon 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) + return "", nil + } + return "", fmt.Errorf("failed to stat addon file (%s):\n%w", addonFilePath, statErr) } func (d *aclDistroHandler) PreserveBootDirLayout() bool { diff --git a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_azurelinux.go b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_azurelinux.go index cd4e80df4..c741037a1 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_azurelinux.go +++ b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_azurelinux.go @@ -123,12 +123,14 @@ func (d *azureLinuxDistroHandler) GetSELinuxConfigFile() string { return selinuxConfigFileDefault } -func (d *azureLinuxDistroHandler) IsSELinuxConfigFileReadOnly() bool { - return false +func (d *azureLinuxDistroHandler) UpdateSELinuxConfigFile(selinuxMode imagecustomizerapi.SELinuxMode, + imageChroot safechroot.ChrootInterface, +) error { + return UpdateSELinuxModeInConfigFile(selinuxMode, imageChroot, selinuxConfigFileDefault) } -func (d *azureLinuxDistroHandler) AllowsMissingUkiAddon() bool { - return false +func (d *azureLinuxDistroHandler) ExtractUkiAddonCmdline(addonFilePath string, buildDir string) (string, error) { + return defaultExtractUkiAddonCmdline(addonFilePath, buildDir) } func (d *azureLinuxDistroHandler) PreserveBootDirLayout() bool { diff --git a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_fedora.go b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_fedora.go index bb98fac91..abea18151 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_fedora.go +++ b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_fedora.go @@ -102,12 +102,14 @@ func (d *fedoraDistroHandler) GetSELinuxConfigFile() string { return selinuxConfigFileDefault } -func (d *fedoraDistroHandler) IsSELinuxConfigFileReadOnly() bool { - return false +func (d *fedoraDistroHandler) UpdateSELinuxConfigFile(selinuxMode imagecustomizerapi.SELinuxMode, + imageChroot safechroot.ChrootInterface, +) error { + return UpdateSELinuxModeInConfigFile(selinuxMode, imageChroot, selinuxConfigFileDefault) } -func (d *fedoraDistroHandler) AllowsMissingUkiAddon() bool { - return false +func (d *fedoraDistroHandler) ExtractUkiAddonCmdline(addonFilePath string, buildDir string) (string, error) { + return defaultExtractUkiAddonCmdline(addonFilePath, buildDir) } func (d *fedoraDistroHandler) PreserveBootDirLayout() bool { diff --git a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_ubuntu.go b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_ubuntu.go index 2b30e5a7b..56b983f3b 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_ubuntu.go +++ b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_ubuntu.go @@ -122,12 +122,14 @@ func (d *ubuntuDistroHandler) GetSELinuxConfigFile() string { return selinuxConfigFileDefault } -func (d *ubuntuDistroHandler) IsSELinuxConfigFileReadOnly() bool { - return false +func (d *ubuntuDistroHandler) UpdateSELinuxConfigFile(selinuxMode imagecustomizerapi.SELinuxMode, + imageChroot safechroot.ChrootInterface, +) error { + return UpdateSELinuxModeInConfigFile(selinuxMode, imageChroot, selinuxConfigFileDefault) } -func (d *ubuntuDistroHandler) AllowsMissingUkiAddon() bool { - return false +func (d *ubuntuDistroHandler) ExtractUkiAddonCmdline(addonFilePath string, buildDir string) (string, error) { + return defaultExtractUkiAddonCmdline(addonFilePath, buildDir) } func (d *ubuntuDistroHandler) PreserveBootDirLayout() bool { diff --git a/toolkit/tools/pkg/osmodifierlib/modifierutils.go b/toolkit/tools/pkg/osmodifierlib/modifierutils.go index c14647ce3..13a3be23a 100644 --- a/toolkit/tools/pkg/osmodifierlib/modifierutils.go +++ b/toolkit/tools/pkg/osmodifierlib/modifierutils.go @@ -192,7 +192,7 @@ func updateSELinuxForUkiBoot(selinuxMode imagecustomizerapi.SELinuxMode, install logger.Log.Infof("Applying SELinux mode ('%s') for UKI-based system", selinuxMode) - err := imagecustomizerlib.UpdateSELinuxModeInConfigFile(selinuxMode, installChroot, distroHandler.GetSELinuxConfigFile()) + err := distroHandler.UpdateSELinuxConfigFile(selinuxMode, installChroot) if err != nil { return fmt.Errorf("failed to update SELinux mode in config file: %w", err) } @@ -220,7 +220,7 @@ func updateSELinuxForGrubBasedBoot(buildDir string, selinuxMode imagecustomizera return fmt.Errorf("failed to update SELinux kernel cmdline: %w", err) } - err = imagecustomizerlib.UpdateSELinuxModeInConfigFile(selinuxMode, installChroot, distroHandler.GetSELinuxConfigFile()) + err = distroHandler.UpdateSELinuxConfigFile(selinuxMode, installChroot) if err != nil { return fmt.Errorf("failed to update SELinux mode in config file: %w", err) }