diff --git a/docs/imagecustomizer/api/configuration/configuration.md b/docs/imagecustomizer/api/configuration/configuration.md index 34fe6d5dce..ab4f921e6d 100644 --- a/docs/imagecustomizer/api/configuration/configuration.md +++ b/docs/imagecustomizer/api/configuration/configuration.md @@ -237,7 +237,7 @@ os: - [options](./module.md#options-mapstring-string) - [overlays](./os.md#overlays-overlay) ([overlay type](./overlay.md)) - [uki](./os.md#uki-uki) ([uki type](./uki.md)) - - [kernels](./uki.md#kernels) + - [mode](./uki.md#mode-string) - [imageHistory](./os.md#imagehistory-string) - [scripts](./config.md#scripts-scripts) ([scripts type](./scripts.md)) - [postCustomization](./scripts.md#postcustomization-script) ([script type](./script.md)) diff --git a/docs/imagecustomizer/api/configuration/uki.md b/docs/imagecustomizer/api/configuration/uki.md index 0b34e7bbfd..00a17eb654 100644 --- a/docs/imagecustomizer/api/configuration/uki.md +++ b/docs/imagecustomizer/api/configuration/uki.md @@ -25,47 +25,97 @@ os: bootLoader: resetType: hard-reset uki: - kernels: auto + mode: create previewFeatures: - uki ``` Added in v0.8. -## kernels +## mode [string] -Specifies which kernels to produce UKIs for. +Specifies how to handle UKI creation or preservation. -The value can either contain: +Required. -- The string `"auto"` -- A list of kernel version strings. +Supported values: -When `"auto"` is specified, the tool automatically searches for all the -installed kernels and produces UKIs for all the found kernels. +- `create`: Create UKI files for all installed kernels. When used with a base image that + already has UKIs, the new UKIs will be generated and override the old ones. -If a list of kernel versions is provided, then the tool will only produce UKIs -for the kernels specified. +- `passthrough`: Preserve existing UKI files without modification. -The kernel versions must match the regex: `^\d+\.\d+\.\d+(\.\d+)?(-[\w\-\.]+)?$`. -Examples of valid kernel formats: `6.6.51.1-5.azl3`, `5.10.120-4.custom`, `4.18.0-80.el8`. +Example (creating UKIs): -Example: +```yaml +os: + bootLoader: + resetType: hard-reset + uki: + mode: create + + kernelCommandLine: + extraCommandLine: + - rd.info + +previewFeatures: + - uki +``` + +Example (passthrough mode): ```yaml +# Customize an existing UKI image without regenerating UKI files. +# This preserves the existing kernel, initramfs, and cmdline in the UKI. os: uki: - kernels: auto + mode: passthrough + + # You can still perform OS customizations: + packages: + install: + - nginx + - vim + + additionalFiles: + - path: /etc/app-config.txt + content: | + Application configuration + +previewFeatures: + - uki ``` -Example: +Example (re-customizing UKI with verity): ```yaml +# Recustomize an existing UKI+verity image with updated verity hashes. +storage: + reinitializeVerity: all + os: + bootloader: + resetType: hard-reset + uki: - kernels: - - 6.6.51.1-5.azl3 - - 5.10.120-4.custom + mode: create + + kernelCommandLine: + extraCommandLine: + - rd.info + + packages: + install: + - openssh-server + + additionalFiles: + - path: /etc/uki-recustomization.txt + content: | + UKI recustomization with verity refresh + +previewFeatures: + - uki + - reinitialize-verity ``` -Added in v0.8. +Added in v1.2.0 diff --git a/toolkit/tools/imagecustomizerapi/config.go b/toolkit/tools/imagecustomizerapi/config.go index b7efe4ee51..66ef0527cc 100644 --- a/toolkit/tools/imagecustomizerapi/config.go +++ b/toolkit/tools/imagecustomizerapi/config.go @@ -69,12 +69,12 @@ func (c *Config) IsValid() (err error) { return fmt.Errorf("the 'uki' preview feature must be enabled to use 'os.uki'") } - // Temporary limitation: We currently require 'os.bootloader.reset' to be 'hard-reset' when 'os.uki' is enabled. - // In the future, as we design and develop the bootloader further, this hard-reset limitation may be lifted. - if c.OS.BootLoader.ResetType != ResetBootLoaderTypeHard { - return fmt.Errorf( - "'os.bootloader.reset' must be '%s' when 'os.uki' is enabled", ResetBootLoaderTypeHard, - ) + // Validate passthrough mode compatibility + if c.OS.Uki.Mode == UkiModePassthrough { + err = c.validateUkiPassthroughMode() + if err != nil { + return err + } } } @@ -154,3 +154,58 @@ func (c *Config) IsValid() (err error) { func (c *Config) CustomizePartitions() bool { return c.Storage.CustomizePartitions() } + +func (c *Config) validateUkiPassthroughMode() error { + var incompatibleConfigs []string + + // Check for bootloader hard-reset (modifies bootloader configuration) + if c.OS != nil && c.OS.BootLoader.ResetType == ResetBootLoaderTypeHard { + incompatibleConfigs = append(incompatibleConfigs, + "os.bootloader.resetType: hard-reset modifies bootloader configuration") + } + + // Check for SELinux mode changes (modifies kernel command line) + if c.OS != nil && c.OS.SELinux.Mode != SELinuxModeDefault { + incompatibleConfigs = append(incompatibleConfigs, + "os.selinux.mode: changes SELinux mode which modifies kernel command line embedded in UKI") + } + + // Check for kernel command line modifications + if c.OS != nil && len(c.OS.KernelCommandLine.ExtraCommandLine) > 0 { + incompatibleConfigs = append(incompatibleConfigs, + "os.kernelCommandLine.extraCommandLine: modifies kernel command line embedded in UKI") + } + + // Check for verity configuration (modifies initramfs and kernel cmdline) + if len(c.Storage.Verity) > 0 { + incompatibleConfigs = append(incompatibleConfigs, + "storage.verity: adds verity devices which modifies initramfs and kernel cmdline") + } + + // Check for verity reinitialization (modifies initramfs and kernel cmdline) + if c.Storage.ReinitializeVerity == ReinitializeVerityTypeAll { + incompatibleConfigs = append(incompatibleConfigs, + "storage.reinitializeVerity: reinitializes verity which modifies initramfs and kernel cmdline") + } + + // Check for overlay configurations (might trigger initramfs regeneration) + if c.OS != nil && c.OS.Overlays != nil && len(*c.OS.Overlays) > 0 { + incompatibleConfigs = append(incompatibleConfigs, + "os.overlays: overlay configuration might trigger initramfs regeneration") + } + + if len(incompatibleConfigs) > 0 { + errorMsg := "UKI passthrough mode is incompatible with the following configurations:\n" + for _, cfg := range incompatibleConfigs { + errorMsg += fmt.Sprintf(" - %s\n", cfg) + } + errorMsg += "\nPassthrough mode preserves existing UKIs without modification.\n" + errorMsg += "To make these changes, use mode: create to regenerate UKIs, or remove the incompatible configurations." + return fmt.Errorf("%s", errorMsg) + } + + // Note: Kernel package modifications are validated at runtime by checking /boot + // for new kernel binaries after package operations. This is more reliable than + // checking package names statically. + return nil +} diff --git a/toolkit/tools/imagecustomizerapi/config_test.go b/toolkit/tools/imagecustomizerapi/config_test.go index 689f1998c2..4d2ad2fc7a 100644 --- a/toolkit/tools/imagecustomizerapi/config_test.go +++ b/toolkit/tools/imagecustomizerapi/config_test.go @@ -163,10 +163,7 @@ func TestConfigIsValidWithPreviewFeaturesAndUki(t *testing.T) { ResetType: "hard-reset", }, Uki: &Uki{ - Kernels: UkiKernels{ - Auto: false, - Kernels: []string{"6.6.51.1-5.azl3"}, - }, + Mode: UkiModeCreate, }, }, PreviewFeatures: []PreviewFeature{"uki"}, @@ -183,10 +180,7 @@ func TestConfigIsValidWithMissingUkiPreviewFeature(t *testing.T) { ResetType: "hard-reset", }, Uki: &Uki{ - Kernels: UkiKernels{ - Auto: false, - Kernels: []string{"6.6.51.1-5.azl3"}, - }, + Mode: UkiModeCreate, }, }, PreviewFeatures: []PreviewFeature{}, @@ -197,24 +191,6 @@ func TestConfigIsValidWithMissingUkiPreviewFeature(t *testing.T) { assert.ErrorContains(t, err, "the 'uki' preview feature must be enabled to use 'os.uki'") } -func TestConfigIsValidWithUkiAndMissingHardReset(t *testing.T) { - config := &Config{ - OS: &OS{ - Uki: &Uki{ - Kernels: UkiKernels{ - Auto: true, - Kernels: nil, - }, - }, - }, - PreviewFeatures: []PreviewFeature{"uki"}, - } - - err := config.IsValid() - assert.Error(t, err) - assert.ErrorContains(t, err, "'os.bootloader.reset' must be 'hard-reset' when 'os.uki' is enabled") -} - func TestConfigIsValidWithInvalidBootType(t *testing.T) { config := &Config{ Storage: Storage{ diff --git a/toolkit/tools/imagecustomizerapi/os_test.go b/toolkit/tools/imagecustomizerapi/os_test.go index ebd42eff33..39e1200ab3 100644 --- a/toolkit/tools/imagecustomizerapi/os_test.go +++ b/toolkit/tools/imagecustomizerapi/os_test.go @@ -235,10 +235,7 @@ func TestOSValidWithUki(t *testing.T) { ResetType: ResetBootLoaderTypeHard, }, Uki: &Uki{ - Kernels: UkiKernels{ - Auto: false, - Kernels: []string{"6.6.51.1-5.azl3"}, - }, + Mode: UkiModeCreate, }, } @@ -246,15 +243,13 @@ func TestOSValidWithUki(t *testing.T) { assert.NoError(t, err) } -func TestOSValidUkiAutoMode(t *testing.T) { +func TestOSValidUkiPassthroughMode(t *testing.T) { os := OS{ BootLoader: BootLoader{ ResetType: ResetBootLoaderTypeHard, }, Uki: &Uki{ - Kernels: UkiKernels{ - Auto: true, - }, + Mode: UkiModePassthrough, }, } @@ -262,39 +257,32 @@ func TestOSValidUkiAutoMode(t *testing.T) { assert.NoError(t, err) } -func TestOSInvalidUkiInvalidKernels(t *testing.T) { +func TestOSInvalidUkiInvalidMode(t *testing.T) { os := OS{ BootLoader: BootLoader{ ResetType: ResetBootLoaderTypeHard, }, Uki: &Uki{ - Kernels: UkiKernels{ - Auto: false, - Kernels: []string{"invalid-kernel-version"}, - }, + Mode: UkiMode("invalid-mode"), }, } err := os.IsValid() assert.Error(t, err) assert.ErrorContains(t, err, "invalid uki") - assert.ErrorContains(t, err, "invalid kernel version at index 0:") - assert.ErrorContains(t, err, "invalid kernel version format (invalid-kernel-version)") + assert.ErrorContains(t, err, "invalid uki mode value (invalid-mode)") } -func TestOSInvalidUkiEmptyKernels(t *testing.T) { +func TestOSValidUkiUnspecifiedMode(t *testing.T) { os := OS{ BootLoader: BootLoader{ ResetType: ResetBootLoaderTypeHard, }, Uki: &Uki{ - Kernels: UkiKernels{ - Auto: false, - Kernels: []string{}, - }, + Mode: UkiModeUnspecified, }, } err := os.IsValid() - assert.ErrorContains(t, err, "must specify either 'auto' or a non-empty list of kernel names") + assert.NoError(t, err) } diff --git a/toolkit/tools/imagecustomizerapi/schema.json b/toolkit/tools/imagecustomizerapi/schema.json index 4be02ddd4e..8ea44a74d2 100644 --- a/toolkit/tools/imagecustomizerapi/schema.json +++ b/toolkit/tools/imagecustomizerapi/schema.json @@ -662,29 +662,13 @@ }, "Uki": { "properties": { - "kernels": { - "$ref": "#/$defs/UkiKernels" + "mode": { + "type": "string" } }, "additionalProperties": false, "type": "object" }, - "UkiKernels": { - "oneOf": [ - { - "type": "string", - "enum": [ - "auto" - ] - }, - { - "items": { - "type": "string" - }, - "type": "array" - } - ] - }, "User": { "properties": { "name": { diff --git a/toolkit/tools/imagecustomizerapi/uki.go b/toolkit/tools/imagecustomizerapi/uki.go index 1fc55aea2e..98eb1ae674 100644 --- a/toolkit/tools/imagecustomizerapi/uki.go +++ b/toolkit/tools/imagecustomizerapi/uki.go @@ -8,13 +8,13 @@ import ( ) type Uki struct { - Kernels UkiKernels `yaml:"kernels" json:"kernels"` + Mode UkiMode `yaml:"mode" json:"mode"` } func (u *Uki) IsValid() error { - err := u.Kernels.IsValid() + err := u.Mode.IsValid() if err != nil { - return fmt.Errorf("invalid uki kernels:\n%w", err) + return fmt.Errorf("invalid uki mode:\n%w", err) } return nil diff --git a/toolkit/tools/imagecustomizerapi/uki_test.go b/toolkit/tools/imagecustomizerapi/uki_test.go index e0edaad08d..b8e4020ee7 100644 --- a/toolkit/tools/imagecustomizerapi/uki_test.go +++ b/toolkit/tools/imagecustomizerapi/uki_test.go @@ -9,46 +9,39 @@ import ( "github.com/stretchr/testify/assert" ) -func TestUkiIsValid(t *testing.T) { +func TestUkiIsValidWithCreate(t *testing.T) { validUki := Uki{ - Kernels: UkiKernels{ - Auto: false, - Kernels: []string{ - "6.6.51.1-5.azl3", - "5.10.120-4.custom", - }, - }, + Mode: UkiModeCreate, } err := validUki.IsValid() assert.NoError(t, err) } -func TestUkiIsValidWithAuto(t *testing.T) { +func TestUkiIsValidWithPassthrough(t *testing.T) { validUki := Uki{ - Kernels: UkiKernels{ - Auto: true, - Kernels: nil, // Auto mode does not require explicit kernel versions - }, + Mode: UkiModePassthrough, } err := validUki.IsValid() assert.NoError(t, err) } -func TestUkiKernelsIsValidInvalidKernelList(t *testing.T) { +func TestUkiIsValidWithUnspecified(t *testing.T) { + validUki := Uki{ + Mode: UkiModeUnspecified, + } + + err := validUki.IsValid() + assert.NoError(t, err) +} + +func TestUkiIsValidInvalidMode(t *testing.T) { invalidUki := Uki{ - Kernels: UkiKernels{ - Auto: false, - Kernels: []string{ - "6.6.51.1-5.azl3", - "invalid-kernel-version", - }, - }, + Mode: UkiMode("invalid-mode"), } err := invalidUki.IsValid() assert.Error(t, err) - assert.ErrorContains(t, err, "invalid kernel version at index 1:") - assert.ErrorContains(t, err, "invalid kernel version format (invalid-kernel-version)") + assert.ErrorContains(t, err, "invalid uki mode value (invalid-mode)") } diff --git a/toolkit/tools/imagecustomizerapi/ukikernels.go b/toolkit/tools/imagecustomizerapi/ukikernels.go deleted file mode 100644 index d3f19db964..0000000000 --- a/toolkit/tools/imagecustomizerapi/ukikernels.go +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -package imagecustomizerapi - -import ( - "fmt" - "regexp" - - "github.com/invopop/jsonschema" - "gopkg.in/yaml.v3" -) - -type UkiKernels struct { - Auto bool - Kernels []string -} - -// UnmarshalYAML enables UkiKernels to handle both shorthand "auto" and a structured list of kernel versions. -func (u *UkiKernels) UnmarshalYAML(value *yaml.Node) error { - switch value.Kind { - case yaml.ScalarNode: - // Handle "kernels: auto". - if value.Value == "auto" { - u.Auto = true - u.Kernels = nil - return nil - } - return fmt.Errorf("invalid value for 'kernels': expected 'auto' or a list of kernel names, got '%s'", value.Value) - - case yaml.SequenceNode: - // Handle "kernels: - ". - var kernels []string - if err := value.Decode(&kernels); err != nil { - return fmt.Errorf("failed to decode kernel list:\n%w", err) - } - u.Kernels = kernels - u.Auto = false - return nil - - default: - // Invalid YAML structure. - return fmt.Errorf("invalid YAML structure for 'kernels': must be either 'auto' or a list of kernel names") - } -} - -func (UkiKernels) JSONSchema() *jsonschema.Schema { - return &jsonschema.Schema{ - OneOf: []*jsonschema.Schema{ - { - Type: "string", - Enum: []interface{}{"auto"}, - }, - { - Type: "array", - Items: &jsonschema.Schema{ - Type: "string", - }, - }, - }, - } -} - -func (u *UkiKernels) IsValid() error { - if u.Auto && len(u.Kernels) > 0 { - return fmt.Errorf("'auto' cannot coexist with a list of kernel names") - } - - if !u.Auto && len(u.Kernels) == 0 { - return fmt.Errorf("must specify either 'auto' or a non-empty list of kernel names") - } - - for i, kernel := range u.Kernels { - if err := ukiKernelVersionIsValid(kernel); err != nil { - return fmt.Errorf("invalid kernel version at index %d:\n%w", i, err) - } - } - - return nil -} - -func ukiKernelVersionIsValid(kernel string) error { - if kernel == "" { - return fmt.Errorf("empty kernel name") - } - - versionRegex := regexp.MustCompile(`^\d+\.\d+\.\d+(\.\d+)?(-[\w\-\.]+)?$`) - if !versionRegex.MatchString(kernel) { - return fmt.Errorf("invalid kernel version format (%s)", kernel) - } - - return nil -} diff --git a/toolkit/tools/imagecustomizerapi/ukikernels_test.go b/toolkit/tools/imagecustomizerapi/ukikernels_test.go deleted file mode 100644 index ccb19a64b2..0000000000 --- a/toolkit/tools/imagecustomizerapi/ukikernels_test.go +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -package imagecustomizerapi - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "gopkg.in/yaml.v3" -) - -func TestUkiKernelsUnmarshalYAML_Invalid(t *testing.T) { - yamlContent := `kernels: invalid` - - var kernels UkiKernels - err := yaml.Unmarshal([]byte(yamlContent), &kernels) - assert.Error(t, err) - assert.ErrorContains(t, err, "invalid YAML structure for 'kernels': must be either 'auto' or a list of kernel names") -} - -func TestUkiKernelsIsValid_EmptyList(t *testing.T) { - invalidKernels := UkiKernels{ - Auto: false, - Kernels: []string{}, - } - - err := invalidKernels.IsValid() - assert.Error(t, err) - assert.ErrorContains(t, err, "must specify either 'auto' or a non-empty list of kernel names") -} - -func TestUkiKernelsIsValid_AutoAndList(t *testing.T) { - invalidKernels := UkiKernels{ - Auto: true, - Kernels: []string{"6.6.51.1-5.azl3"}, - } - - err := invalidKernels.IsValid() - assert.Error(t, err) - assert.ErrorContains(t, err, "'auto' cannot coexist with a list of kernel names") -} diff --git a/toolkit/tools/imagecustomizerapi/ukimode.go b/toolkit/tools/imagecustomizerapi/ukimode.go new file mode 100644 index 0000000000..d09626a406 --- /dev/null +++ b/toolkit/tools/imagecustomizerapi/ukimode.go @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package imagecustomizerapi + +import ( + "fmt" +) + +type UkiMode string + +const ( + UkiModeUnspecified UkiMode = "" + UkiModeCreate UkiMode = "create" + UkiModePassthrough UkiMode = "passthrough" +) + +func (u UkiMode) IsValid() error { + switch u { + case UkiModeUnspecified, UkiModeCreate, UkiModePassthrough: + return nil + default: + return fmt.Errorf("invalid uki mode value (%s): must be one of ['', 'create', 'passthrough']", u) + } +} diff --git a/toolkit/tools/imagecustomizerapi/ukimode_test.go b/toolkit/tools/imagecustomizerapi/ukimode_test.go new file mode 100644 index 0000000000..6de1576311 --- /dev/null +++ b/toolkit/tools/imagecustomizerapi/ukimode_test.go @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package imagecustomizerapi + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestUkiModeIsValidCreate(t *testing.T) { + mode := UkiModeCreate + err := mode.IsValid() + assert.NoError(t, err) +} + +func TestUkiModeIsValidPassthrough(t *testing.T) { + mode := UkiModePassthrough + err := mode.IsValid() + assert.NoError(t, err) +} + +func TestUkiModeIsValidUnspecified(t *testing.T) { + mode := UkiModeUnspecified + err := mode.IsValid() + assert.NoError(t, err) +} + +func TestUkiModeIsValidInvalid(t *testing.T) { + mode := UkiMode("invalid-mode") + err := mode.IsValid() + assert.Error(t, err) + assert.ErrorContains(t, err, "invalid uki mode value (invalid-mode)") +} + +func TestUkiModeIsValidTypo(t *testing.T) { + mode := UkiMode("Create") + err := mode.IsValid() + assert.Error(t, err) + assert.ErrorContains(t, err, "invalid uki mode value (Create)") +} diff --git a/toolkit/tools/pkg/imagecustomizerlib/baseconfigs_test.go b/toolkit/tools/pkg/imagecustomizerlib/baseconfigs_test.go index 5aaba7de93..092a602b1b 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/baseconfigs_test.go +++ b/toolkit/tools/pkg/imagecustomizerlib/baseconfigs_test.go @@ -207,7 +207,8 @@ func TestBaseConfigsFullRun(t *testing.T) { assert.Contains(t, string(moduleDisableContent), "vfio") // Verify SELinux - verifyKernelCommandLine(t, imageConnection, []string{}, []string{"security=selinux", "selinux=1", "enforcing=1"}) + hasUkis := true + verifyKernelCommandLine(t, imageConnection, hasUkis, []string{}, []string{"security=selinux", "selinux=1", "enforcing=1"}) verifySELinuxConfigFile(t, imageConnection, "disabled") // Verify overlays @@ -239,9 +240,5 @@ func TestBaseConfigsFullRun(t *testing.T) { assert.Len(t, ukiFiles, 1, "expected one UKI .efi file to be created") // Verify kernel commandline - grubCfgFilePath := filepath.Join(imageConnection.Chroot().RootDir(), "/boot/grub2/grub.cfg") - grubCfgContents, err := file.Read(grubCfgFilePath) - assert.NoError(t, err) - assert.NotContains(t, grubCfgContents, "rd.info") - assert.Contains(t, grubCfgContents, "console=tty0 console=ttyS0") + verifyKernelCommandLine(t, imageConnection, hasUkis, []string{"console=tty0", "console=ttyS0"}, []string{"rd.info"}) } diff --git a/toolkit/tools/pkg/imagecustomizerlib/bootcustomizer.go b/toolkit/tools/pkg/imagecustomizerlib/bootcustomizer.go index a76d0e5b26..49b599020d 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/bootcustomizer.go +++ b/toolkit/tools/pkg/imagecustomizerlib/bootcustomizer.go @@ -4,7 +4,10 @@ package imagecustomizerlib import ( + "errors" "fmt" + "io/fs" + "path/filepath" "github.com/microsoft/azure-linux-image-tools/toolkit/tools/imagecustomizerapi" "github.com/microsoft/azure-linux-image-tools/toolkit/tools/imagegen/installutils" @@ -13,7 +16,22 @@ import ( var ( // Boot customization errors - ErrBootGrubMkconfigGeneration = NewImageCustomizerError("Boot:GrubMkconfigGeneration", "failed to generate grub.cfg via grub2-mkconfig") + ErrBootGrubMkconfigGeneration = NewImageCustomizerError("Boot:GrubMkconfigGeneration", "failed to generate grub.cfg via grub2-mkconfig") + ErrBootNoConfigFound = NewImageCustomizerError("Boot:NoConfigFound", "no boot configuration found: grub.cfg does not exist and no UKI files found") + ErrBootUkiPassthroughCmdlineModified = NewImageCustomizerError("Boot:UkiPassthroughCmdlineModified", + "cannot modify kernel command-line in UKI passthrough mode: use 'mode: create' to customize kernel cmdline") +) + +// bootConfigType represents the type of boot configuration used by the image. +type bootConfigType string + +const ( + // bootConfigTypeGrubMkconfig indicates the image uses grub-mkconfig to generate grub.cfg. + bootConfigTypeGrubMkconfig bootConfigType = "grub-mkconfig" + // bootConfigTypeGrubLegacy indicates the image uses a manually maintained grub.cfg. + bootConfigTypeGrubLegacy bootConfigType = "grub-legacy" + // bootConfigTypeUki indicates the image uses UKI without grub.cfg. + bootConfigTypeUki bootConfigType = "uki" ) type BootCustomizer struct { @@ -23,34 +41,54 @@ type BootCustomizer struct { // The contents of the /etc/default/grub file. defaultGrubFileContent string - // Whether or not the image is using grub-mkconfig. - isGrubMkconfig bool + // The type of boot configuration used by the image. + bootConfigType bootConfigType } func NewBootCustomizer(imageChroot safechroot.ChrootInterface) (*BootCustomizer, error) { grubCfgContent, err := ReadGrub2ConfigFile(imageChroot) - if err != nil { + if err != nil && !errors.Is(err, fs.ErrNotExist) { return nil, err } defaultGrubFileContent, err := readDefaultGrubFile(imageChroot) - if err != nil { + if err != nil && !errors.Is(err, fs.ErrNotExist) { return nil, err } - isGrubMkconfig := isGrubMkconfigConfig(grubCfgContent) + // Determine boot configuration type + bootConfigType, err := determineBootConfigType(grubCfgContent, imageChroot) + if err != nil { + return nil, err + } b := &BootCustomizer{ grubCfgContent: grubCfgContent, defaultGrubFileContent: defaultGrubFileContent, - isGrubMkconfig: isGrubMkconfig, + bootConfigType: bootConfigType, } return b, nil } -// Returns whether or not the OS uses grub-mkconfig. -func (b *BootCustomizer) IsGrubMkconfigImage() bool { - return b.isGrubMkconfig +func determineBootConfigType(grubCfgContent string, imageChroot safechroot.ChrootInterface) (bootConfigType, error) { + // If grub.cfg doesn't exist, check for UKI + if grubCfgContent == "" { + hasUkis, err := baseImageHasUkis(imageChroot.(*safechroot.Chroot)) + if err == nil && hasUkis { + // UKI images without grub.cfg are in passthrough mode (grub.cfg not regenerated) + // For UKI create mode, grub.cfg is regenerated during kernel extraction, so it would exist + return bootConfigTypeUki, nil + } + // No grub.cfg and no UKIs - this is an error + return "", ErrBootNoConfigFound + } + + // If grub.cfg exists, check if it's generated by grub-mkconfig + if isGrubMkconfigConfig(grubCfgContent) { + return bootConfigTypeGrubMkconfig, nil + } + + return bootConfigTypeGrubLegacy, nil } // Inserts new kernel command-line args into the grub config file. @@ -61,14 +99,16 @@ func (b *BootCustomizer) AddKernelCommandLine(extraCommandLine []string) error { combinedArgs := GrubArgsToString(extraCommandLine) - if b.isGrubMkconfig { + switch b.bootConfigType { + case bootConfigTypeGrubMkconfig: defaultGrubFileContent, err := addExtraCommandLineToDefaultGrubFile(b.defaultGrubFileContent, combinedArgs) if err != nil { return err } b.defaultGrubFileContent = defaultGrubFileContent - } else { + + case bootConfigTypeGrubLegacy: // Add the args directly to the /boot/grub2/grub.cfg file. grubCfgContent, err := appendKernelCommandLineArgsAll(b.grubCfgContent, combinedArgs) if err != nil { @@ -76,54 +116,79 @@ func (b *BootCustomizer) AddKernelCommandLine(extraCommandLine []string) error { } b.grubCfgContent = grubCfgContent + + case bootConfigTypeUki: + // UKI passthrough mode: preserve existing UKI boot configuration. + // Cmdline args are embedded in UKI files and cannot be modified in passthrough mode. + return ErrBootUkiPassthroughCmdlineModified } return nil } -// Gets the image's configured SELinux mode. -func (b *BootCustomizer) getSELinuxModeFromGrub() (imagecustomizerapi.SELinuxMode, error) { +// getSELinuxModeFromCmdline gets the image's configured SELinux mode from kernel command-line. +// Returns (mode, found, error) where: +// - mode: The detected SELinux mode (or SELinuxModeDefault if not found) +// - found: true if cmdline exists and was successfully parsed +// - error: Any error encountered during parsing +func (b *BootCustomizer) getSELinuxModeFromCmdline(buildDir string, imageChroot safechroot.ChrootInterface) (imagecustomizerapi.SELinuxMode, bool, error) { var err error var args []grubConfigLinuxArg // Get the SELinux kernel command-line args. - if b.isGrubMkconfig { + switch b.bootConfigType { + case bootConfigTypeGrubMkconfig: // Check both GRUB_CMDLINE_LINUX and GRUB_CMDLINE_LINUX_DEFAULT variables // and merge arguments from both if they exist args, err = GetDefaultGrubFileLinuxArgsFromMultipleVars(b.defaultGrubFileContent) if err != nil { - return "", fmt.Errorf("failed to find SELinux args in grub file (%s):\n%w", installutils.GrubDefFile, err) + return imagecustomizerapi.SELinuxModeDefault, false, fmt.Errorf("failed to find SELinux args in grub file (%s):\n%w", installutils.GrubDefFile, err) } - } else { + + case bootConfigTypeGrubLegacy: args, _, err = getLinuxCommandLineArgs(b.grubCfgContent) if err != nil { - return imagecustomizerapi.SELinuxModeDefault, + return imagecustomizerapi.SELinuxModeDefault, false, fmt.Errorf("failed to parse SELinux args from grub file (%s):\n%w", installutils.GrubCfgFile, err) } + + case bootConfigTypeUki: + espDir := filepath.Join(imageChroot.RootDir(), EspDir) + + kernelToArgs, err := extractKernelCmdlineFromUkiEfis(espDir, buildDir) + if err != nil { + return imagecustomizerapi.SELinuxModeDefault, false, err + } + + args, err = parseFirstCmdlineFromUkiMap(kernelToArgs) + if err != nil { + return imagecustomizerapi.SELinuxModeDefault, false, err + } } // Get the SELinux mode from the kernel command-line args. selinuxMode, err := getSELinuxModeFromLinuxArgs(args) if err != nil { - return imagecustomizerapi.SELinuxModeDefault, err + return imagecustomizerapi.SELinuxModeDefault, false, err } - return selinuxMode, nil + return selinuxMode, true, nil } -func (b *BootCustomizer) GetSELinuxMode(imageChroot safechroot.ChrootInterface) (imagecustomizerapi.SELinuxMode, error) { - // Get the SELinux mode from the kernel command-line args. - selinuxMode, err := b.getSELinuxModeFromGrub() +func (b *BootCustomizer) GetSELinuxMode(buildDir string, imageChroot safechroot.ChrootInterface) (imagecustomizerapi.SELinuxMode, error) { + // Try to get the SELinux mode from kernel command-line (grub or UKI). + selinuxMode, found, err := b.getSELinuxModeFromCmdline(buildDir, imageChroot) if err != nil { return imagecustomizerapi.SELinuxModeDefault, err } + if found && selinuxMode != imagecustomizerapi.SELinuxModeDefault { + return selinuxMode, nil + } - if selinuxMode == imagecustomizerapi.SELinuxModeDefault { - // Get the SELinux mode from the /etc/selinux/config file. - selinuxMode, err = getSELinuxModeFromConfigFile(imageChroot) - if err != nil { - return imagecustomizerapi.SELinuxModeDefault, err - } + // Fallback: Get the SELinux mode from the /etc/selinux/config file. + selinuxMode, err = getSELinuxModeFromConfigFile(imageChroot) + if err != nil { + return imagecustomizerapi.SELinuxModeDefault, err } return selinuxMode, nil @@ -162,7 +227,8 @@ func (b *BootCustomizer) UpdateSELinuxCommandLineForEMU(selinuxMode imagecustomi func (b *BootCustomizer) UpdateKernelCommandLineArgs(defaultGrubFileVarName defaultGrubFileVarName, argsToRemove []string, newArgs []string, ) error { - if b.isGrubMkconfig { + switch b.bootConfigType { + case bootConfigTypeGrubMkconfig: defaultGrubFileContent, err := updateDefaultGrubFileKernelCommandLineArgs(b.defaultGrubFileContent, defaultGrubFileVarName, argsToRemove, newArgs) if err != nil { @@ -170,13 +236,19 @@ func (b *BootCustomizer) UpdateKernelCommandLineArgs(defaultGrubFileVarName defa } b.defaultGrubFileContent = defaultGrubFileContent - } else { + + case bootConfigTypeGrubLegacy: grubCfgContent, err := updateKernelCommandLineArgsAll(b.grubCfgContent, argsToRemove, newArgs) if err != nil { return err } b.grubCfgContent = grubCfgContent + + case bootConfigTypeUki: + // UKI passthrough mode: preserve existing UKI boot configuration. + // Cmdline args are embedded in UKI files and cannot be modified in passthrough mode. + return ErrBootUkiPassthroughCmdlineModified } return nil @@ -184,7 +256,8 @@ func (b *BootCustomizer) UpdateKernelCommandLineArgs(defaultGrubFileVarName defa // Makes changes to the /etc/default/grub file that are needed/useful for enabling verity. func (b *BootCustomizer) PrepareForVerity() error { - if b.isGrubMkconfig { + switch b.bootConfigType { + case bootConfigTypeGrubMkconfig: // Force root command-line arg to be referenced by /dev path instead of by UUID. defaultGrubFileContent, err := UpdateDefaultGrubFileVariable(b.defaultGrubFileContent, "GRUB_DISABLE_UUID", "true") @@ -201,14 +274,23 @@ func (b *BootCustomizer) PrepareForVerity() error { } b.defaultGrubFileContent = defaultGrubFileContent + + case bootConfigTypeGrubLegacy: + // Legacy grub configurations don't need special preparation for verity. + // The verity args will be directly added to grub.cfg. + + case bootConfigTypeUki: + // UKI passthrough mode: preserve existing UKI boot configuration. + // Verity configuration is embedded in UKI files during UKI regeneration. } return nil } func (b *BootCustomizer) WriteToFile(imageChroot safechroot.ChrootInterface) error { - if b.isGrubMkconfig { - // Update /etc/defaukt/grub file. + switch b.bootConfigType { + case bootConfigTypeGrubMkconfig: + // Update /etc/default/grub file. err := WriteDefaultGrubFile(b.defaultGrubFileContent, imageChroot) if err != nil { return err @@ -218,12 +300,18 @@ func (b *BootCustomizer) WriteToFile(imageChroot safechroot.ChrootInterface) err if err != nil { return fmt.Errorf("%w:\n%w", ErrBootGrubMkconfigGeneration, err) } - } else { + + case bootConfigTypeGrubLegacy: // Update grub.cfg file. err := writeGrub2ConfigFile(b.grubCfgContent, imageChroot) if err != nil { return err } + + case bootConfigTypeUki: + // UKI passthrough mode: preserve existing UKI boot configuration. + // UKI files are managed by UKI-specific customization logic, not through grub. + // Future: May need to update boot loader configuration when native UKI boot support is added. } return nil diff --git a/toolkit/tools/pkg/imagecustomizerlib/bootcustomizer_test.go b/toolkit/tools/pkg/imagecustomizerlib/bootcustomizer_test.go index 016ee75573..62c9fcbcc8 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/bootcustomizer_test.go +++ b/toolkit/tools/pkg/imagecustomizerlib/bootcustomizer_test.go @@ -50,15 +50,17 @@ func TestBootCustomizerAddKernelCommandLine30(t *testing.T) { func TestBootCustomizerSELinuxMode20(t *testing.T) { b := createBootCustomizerFor20(t) - selinuxMode, err := b.getSELinuxModeFromGrub() + selinuxMode, found, err := b.getSELinuxModeFromCmdline("", nil) assert.NoError(t, err) + assert.True(t, found) assert.Equal(t, imagecustomizerapi.SELinuxModeDisabled, selinuxMode) err = b.UpdateSELinuxCommandLine(imagecustomizerapi.SELinuxModePermissive) assert.NoError(t, err) - selinuxMode, err = b.getSELinuxModeFromGrub() + selinuxMode, found, err = b.getSELinuxModeFromCmdline("", nil) assert.NoError(t, err) + assert.True(t, found) assert.Equal(t, imagecustomizerapi.SELinuxModeDefault, selinuxMode) expectedGrubCfgDiff := `22c22 @@ -71,8 +73,9 @@ func TestBootCustomizerSELinuxMode20(t *testing.T) { err = b.UpdateSELinuxCommandLine(imagecustomizerapi.SELinuxModeForceEnforcing) assert.NoError(t, err) - selinuxMode, err = b.getSELinuxModeFromGrub() + selinuxMode, found, err = b.getSELinuxModeFromCmdline("", nil) assert.NoError(t, err) + assert.True(t, found) assert.Equal(t, imagecustomizerapi.SELinuxModeForceEnforcing, selinuxMode) expectedGrubCfgDiff = `22c22 @@ -85,8 +88,9 @@ func TestBootCustomizerSELinuxMode20(t *testing.T) { err = b.UpdateSELinuxCommandLine(imagecustomizerapi.SELinuxModeDisabled) assert.NoError(t, err) - selinuxMode, err = b.getSELinuxModeFromGrub() + selinuxMode, found, err = b.getSELinuxModeFromCmdline("", nil) assert.NoError(t, err) + assert.True(t, found) assert.Equal(t, imagecustomizerapi.SELinuxModeDisabled, selinuxMode) expectedGrubCfgDiff = `22c22 @@ -99,15 +103,17 @@ func TestBootCustomizerSELinuxMode20(t *testing.T) { func TestBootCustomizerSELinuxMode30(t *testing.T) { b := createBootCustomizerFor30(t) - selinuxMode, err := b.getSELinuxModeFromGrub() + selinuxMode, found, err := b.getSELinuxModeFromCmdline("", nil) assert.NoError(t, err) + assert.True(t, found) assert.Equal(t, imagecustomizerapi.SELinuxModeDisabled, selinuxMode) err = b.UpdateSELinuxCommandLine(imagecustomizerapi.SELinuxModePermissive) assert.NoError(t, err) - selinuxMode, err = b.getSELinuxModeFromGrub() + selinuxMode, found, err = b.getSELinuxModeFromCmdline("", nil) assert.NoError(t, err) + assert.True(t, found) assert.Equal(t, imagecustomizerapi.SELinuxModeDefault, selinuxMode) expectedDefaultGrubFileDiff := `5c5 @@ -120,8 +126,9 @@ func TestBootCustomizerSELinuxMode30(t *testing.T) { err = b.UpdateSELinuxCommandLine(imagecustomizerapi.SELinuxModeForceEnforcing) assert.NoError(t, err) - selinuxMode, err = b.getSELinuxModeFromGrub() + selinuxMode, found, err = b.getSELinuxModeFromCmdline("", nil) assert.NoError(t, err) + assert.True(t, found) assert.Equal(t, imagecustomizerapi.SELinuxModeForceEnforcing, selinuxMode) expectedDefaultGrubFileDiff = `5c5 @@ -134,8 +141,9 @@ func TestBootCustomizerSELinuxMode30(t *testing.T) { err = b.UpdateSELinuxCommandLine(imagecustomizerapi.SELinuxModeDisabled) assert.NoError(t, err) - selinuxMode, err = b.getSELinuxModeFromGrub() + selinuxMode, found, err = b.getSELinuxModeFromCmdline("", nil) assert.NoError(t, err) + assert.True(t, found) assert.Equal(t, imagecustomizerapi.SELinuxModeDisabled, selinuxMode) expectedDefaultGrubFileDiff = `5c5 @@ -222,10 +230,15 @@ func createBootCustomizer(t *testing.T, sampleGrubCfgPath string, sampleDefaultG sampleDefaultGrubFileContent, err := os.ReadFile(sampleDefaultGrubFilePath) assert.NoError(t, err, "failed to read sample /etc/default/grub file") + bootConfigType := bootConfigTypeGrubLegacy + if isGrubMkconfig { + bootConfigType = bootConfigTypeGrubMkconfig + } + b := &BootCustomizer{ grubCfgContent: string(sampleGrubCfgContent), defaultGrubFileContent: string(sampleDefaultGrubFileContent), - isGrubMkconfig: isGrubMkconfig, + bootConfigType: bootConfigType, } return b } diff --git a/toolkit/tools/pkg/imagecustomizerlib/customizebootloader.go b/toolkit/tools/pkg/imagecustomizerlib/customizebootloader.go index 7e723d39b9..6aecaed6af 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/customizebootloader.go +++ b/toolkit/tools/pkg/imagecustomizerlib/customizebootloader.go @@ -35,8 +35,7 @@ func handleBootLoader(ctx context.Context, rc *ResolvedConfig, imageConnection * ) error { switch { case rc.BootLoader.ResetType == imagecustomizerapi.ResetBootLoaderTypeHard || newImage: - err := hardResetBootLoader(ctx, rc.BaseConfigPath, rc.Config, imageConnection, partitionsLayout, - newImage, rc.SELinux) + err := hardResetBootLoader(ctx, rc, imageConnection, partitionsLayout, newImage) if err != nil { return fmt.Errorf("%w:\n%w", ErrBootloaderHardReset, err) } @@ -52,8 +51,8 @@ func handleBootLoader(ctx context.Context, rc *ResolvedConfig, imageConnection * return nil } -func hardResetBootLoader(ctx context.Context, baseConfigPath string, config *imagecustomizerapi.Config, imageConnection *imageconnection.ImageConnection, - partitionsLayout []fstabEntryPartNum, newImage bool, selinuxConfig imagecustomizerapi.SELinux, +func hardResetBootLoader(ctx context.Context, rc *ResolvedConfig, imageConnection *imageconnection.ImageConnection, + partitionsLayout []fstabEntryPartNum, newImage bool, ) error { var err error logger.Log.Infof("Hard reset bootloader config") @@ -70,7 +69,7 @@ func hardResetBootLoader(ctx context.Context, baseConfigPath string, config *ima return err } - currentSelinuxMode, err = bootCustomizer.GetSELinuxMode(imageConnection.Chroot()) + currentSelinuxMode, err = bootCustomizer.GetSELinuxMode(rc.BuildDirAbs, imageConnection.Chroot()) if err != nil { return fmt.Errorf("%w:\n%w", ErrBootloaderSelinuxModeGet, err) } @@ -78,8 +77,8 @@ func hardResetBootLoader(ctx context.Context, baseConfigPath string, config *ima var rootMountIdType imagecustomizerapi.MountIdentifierType var bootType imagecustomizerapi.BootType - if config.CustomizePartitions() { - rootFileSystem, foundRootFileSystem := sliceutils.FindValueFunc(config.Storage.FileSystems, + if rc.Config.CustomizePartitions() { + rootFileSystem, foundRootFileSystem := sliceutils.FindValueFunc(rc.Config.Storage.FileSystems, func(fileSystem imagecustomizerapi.FileSystem) bool { return fileSystem.MountPoint != nil && fileSystem.MountPoint.Path == "/" @@ -90,7 +89,7 @@ func hardResetBootLoader(ctx context.Context, baseConfigPath string, config *ima } rootMountIdType = rootFileSystem.MountPoint.IdType - bootType = config.Storage.BootType + bootType = rc.Config.Storage.BootType } else { rootMountIdType, err = findRootMountIdType(partitionsLayout) if err != nil { @@ -104,8 +103,8 @@ func hardResetBootLoader(ctx context.Context, baseConfigPath string, config *ima } // Hard-reset the grub config. - err = configureDiskBootLoader(imageConnection, rootMountIdType, bootType, selinuxConfig, - config.OS.KernelCommandLine, currentSelinuxMode, newImage) + err = configureDiskBootLoader(imageConnection, rootMountIdType, bootType, rc.SELinux, + rc.Config.OS.KernelCommandLine, currentSelinuxMode, newImage) if err != nil { return fmt.Errorf("%w:\n%w", ErrBootloaderDiskConfigure, err) } diff --git a/toolkit/tools/pkg/imagecustomizerlib/customizeos.go b/toolkit/tools/pkg/imagecustomizerlib/customizeos.go index 2b8d8ea997..4d82e3d1e6 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/customizeos.go +++ b/toolkit/tools/pkg/imagecustomizerlib/customizeos.go @@ -5,6 +5,10 @@ package imagecustomizerlib import ( "context" + "fmt" + "os" + "path/filepath" + "strings" "time" "github.com/microsoft/azure-linux-image-tools/toolkit/tools/imagecustomizerapi" @@ -15,6 +19,12 @@ const ( buildTimeFormat = "2006-01-02T15:04:05Z" ) +var ( + ErrUkiPassthroughKernelModified = NewImageCustomizerError("UKI:PassthroughKernelModified", + "kernel binaries detected in /boot after package operations. "+ + "Use 'mode: create' to regenerate UKIs with updated kernels") +) + func doOsCustomizations(ctx context.Context, rc *ResolvedConfig, imageConnection *imageconnection.ImageConnection, partitionsCustomized bool, partitionsLayout []fstabEntryPartNum, distroHandler distroHandler, ) error { @@ -29,6 +39,25 @@ func doOsCustomizations(ctx context.Context, rc *ResolvedConfig, imageConnection return err } + // If UKI mode is 'create' and base image has UKIs, extract kernel and + // initramfs from existing UKIs for re-customization. For 'passthrough' + // mode, we skip extraction to preserve existing UKIs. + if rc.Config.OS.Uki != nil && rc.Config.OS.Uki.Mode == imagecustomizerapi.UkiModeCreate { + // Check if base image has UKIs to determine if extraction is needed + hasUkis, err := baseImageHasUkis(imageChroot) + if err != nil { + return err + } + + if hasUkis { + // Base image has UKIs and mode is create - extract for re-customization + err = extractKernelAndInitramfsFromUkis(ctx, imageChroot, rc.BuildDirAbs) + if err != nil { + return err + } + } + } + for _, configWithBase := range rc.ConfigChain { snapshotTime := configWithBase.Config.OS.Packages.SnapshotTime if rc.Options.PackageSnapshotTime != "" { @@ -43,6 +72,17 @@ func doOsCustomizations(ctx context.Context, rc *ResolvedConfig, imageConnection } } + // If UKI passthrough mode, verify no kernel binaries appeared in /boot + if rc.Config.OS.Uki != nil && rc.Config.OS.Uki.Mode == imagecustomizerapi.UkiModePassthrough { + hasKernels, err := hasKernelBinariesInBoot(imageChroot.RootDir()) + if err != nil { + return err + } + if hasKernels { + return ErrUkiPassthroughKernelModified + } + } + err = UpdateHostname(ctx, rc.Config.OS.Hostname, imageChroot) if err != nil { return err @@ -106,7 +146,8 @@ func doOsCustomizations(ctx context.Context, rc *ResolvedConfig, imageConnection return err } - selinuxMode, err := handleSELinux(ctx, rc.SELinux.Mode, rc.BootLoader.ResetType, imageChroot) + selinuxMode, err := handleSELinux(ctx, rc.BuildDirAbs, rc.SELinux.Mode, rc.BootLoader.ResetType, + imageChroot) if err != nil { return err } @@ -165,3 +206,27 @@ func doOsCustomizations(ctx context.Context, rc *ResolvedConfig, imageConnection return nil } + +func hasKernelBinariesInBoot(rootDir string) (bool, error) { + bootDir := filepath.Join(rootDir, "boot") + entries, err := os.ReadDir(bootDir) + if err != nil { + if os.IsNotExist(err) { + // /boot doesn't exist, no kernels + return false, nil + } + return false, fmt.Errorf("failed to read /boot directory:\n%w", err) + } + + for _, entry := range entries { + if entry.IsDir() { + continue + } + // Check for kernel binaries: vmlinuz-* (e.g., vmlinuz-6.6.104.2-4.azl3) + if strings.HasPrefix(entry.Name(), "vmlinuz-") { + return true, nil + } + } + + return false, nil +} diff --git a/toolkit/tools/pkg/imagecustomizerlib/customizeselinux.go b/toolkit/tools/pkg/imagecustomizerlib/customizeselinux.go index 88a07edf5c..87c2c58ace 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/customizeselinux.go +++ b/toolkit/tools/pkg/imagecustomizerlib/customizeselinux.go @@ -28,7 +28,7 @@ var ( ErrSELinuxRelabelFiles = NewImageCustomizerError("SELinux:RelabelFiles", "failed to set SELinux file labels") ) -func handleSELinux(ctx context.Context, selinuxMode imagecustomizerapi.SELinuxMode, resetBootLoaderType imagecustomizerapi.ResetBootLoaderType, +func handleSELinux(ctx context.Context, buildDir string, selinuxMode imagecustomizerapi.SELinuxMode, resetBootLoaderType imagecustomizerapi.ResetBootLoaderType, imageChroot *safechroot.Chroot, ) (imagecustomizerapi.SELinuxMode, error) { var err error @@ -47,7 +47,7 @@ func handleSELinux(ctx context.Context, selinuxMode imagecustomizerapi.SELinuxMo if selinuxMode == imagecustomizerapi.SELinuxModeDefault { // No changes to the SELinux have been requested. // So, return the current SELinux mode. - currentSELinuxMode, err := bootCustomizer.GetSELinuxMode(imageChroot) + currentSELinuxMode, err := bootCustomizer.GetSELinuxMode(buildDir, imageChroot) if err != nil { return imagecustomizerapi.SELinuxModeDefault, fmt.Errorf("%w:\n%w", ErrSELinuxGetCurrentMode, err) } diff --git a/toolkit/tools/pkg/imagecustomizerlib/customizeselinux_test.go b/toolkit/tools/pkg/imagecustomizerlib/customizeselinux_test.go index 230e8e70cf..46450f3f64 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/customizeselinux_test.go +++ b/toolkit/tools/pkg/imagecustomizerlib/customizeselinux_test.go @@ -8,10 +8,12 @@ import ( "os" "path/filepath" "regexp" + "strings" "testing" "github.com/microsoft/azure-linux-image-tools/toolkit/tools/internal/file" "github.com/microsoft/azure-linux-image-tools/toolkit/tools/internal/imageconnection" + "github.com/microsoft/azure-linux-image-tools/toolkit/tools/internal/shell" "github.com/microsoft/azure-linux-image-tools/toolkit/tools/internal/testutils" "github.com/stretchr/testify/assert" ) @@ -50,7 +52,7 @@ func testCustomizeImageSELinuxHelper(t *testing.T, testName string, baseImageInf defer imageConnection.Close() // Verify bootloader config. - verifyKernelCommandLine(t, imageConnection, []string{"security=selinux", "selinux=1", "enforcing=1"}, []string{}) + verifyKernelCommandLine(t, imageConnection, false, []string{"security=selinux", "selinux=1", "enforcing=1"}, []string{}) verifySELinuxConfigFile(t, imageConnection, "enforcing") // Verify packages are installed. @@ -79,7 +81,7 @@ func testCustomizeImageSELinuxHelper(t *testing.T, testName string, baseImageInf defer imageConnection.Close() // Verify bootloader config. - verifyKernelCommandLine(t, imageConnection, []string{}, []string{"security=selinux", "selinux=1", "enforcing=1"}) + verifyKernelCommandLine(t, imageConnection, false, []string{}, []string{"security=selinux", "selinux=1", "enforcing=1"}) verifySELinuxConfigFile(t, imageConnection, "disabled") // Verify packages are still installed. @@ -108,7 +110,7 @@ func testCustomizeImageSELinuxHelper(t *testing.T, testName string, baseImageInf defer imageConnection.Close() // Verify bootloader config. - verifyKernelCommandLine(t, imageConnection, []string{"security=selinux", "selinux=1"}, []string{"enforcing=1"}) + verifyKernelCommandLine(t, imageConnection, false, []string{"security=selinux", "selinux=1"}, []string{"enforcing=1"}) verifySELinuxConfigFile(t, imageConnection, "permissive") } @@ -164,7 +166,7 @@ func testCustomizeImageSELinuxAndPartitionsHelper(t *testing.T, testName string, defer imageConnection.Close() // Verify bootloader config. - verifyKernelCommandLine(t, imageConnection, []string{"security=selinux", "selinux=1"}, []string{"enforcing=1"}) + verifyKernelCommandLine(t, imageConnection, false, []string{"security=selinux", "selinux=1"}, []string{"enforcing=1"}) verifySELinuxConfigFile(t, imageConnection, "enforcing") // Verify packages are installed. @@ -206,24 +208,93 @@ func TestCustomizeImageSELinuxNoPolicy(t *testing.T) { } } -func verifyKernelCommandLine(t *testing.T, imageConnection *imageconnection.ImageConnection, existsArgs []string, - notExistsArgs []string, +func verifyKernelCommandLine(t *testing.T, imageConnection *imageconnection.ImageConnection, hasUkis bool, + existsArgs []string, notExistsArgs []string, ) { - grubCfgFilePath := filepath.Join(imageConnection.Chroot().RootDir(), "/boot/grub2/grub.cfg") - grubCfgContents, err := file.Read(grubCfgFilePath) - assert.NoError(t, err, "read grub.cfg file") + var grubCfgContents string + + if hasUkis { + // UKI image - extract cmdline from UKI files + ukiDir := filepath.Join(imageConnection.Chroot().RootDir(), "boot/efi/EFI/Linux") + cmdlineFromUki, err := extractCmdlineFromUkiForTest(ukiDir) + if err != nil { + t.Fatalf("Failed to extract cmdline from UKI: %v", err) + return + } + grubCfgContents = cmdlineFromUki + } else { + // GRUB image - read grub.cfg + grubCfgFilePath := filepath.Join(imageConnection.Chroot().RootDir(), "/boot/grub2/grub.cfg") + contents, err := file.Read(grubCfgFilePath) + if err != nil { + t.Fatalf("Failed to read grub.cfg: %v", err) + return + } + grubCfgContents = contents + } for _, existsArg := range existsArgs { - assert.Regexpf(t, fmt.Sprintf("linux.* %s ", regexp.QuoteMeta(existsArg)), grubCfgContents, - "ensure kernel command arg exists (%s)", existsArg) + if hasUkis { + // UKI cmdline is a plain string of args (no "linux" keyword) + assert.Containsf(t, grubCfgContents, existsArg, + "ensure kernel command arg exists (%s)", existsArg) + } else { + // GRUB cfg has "linux /boot/vmlinuz... args" format + assert.Regexpf(t, fmt.Sprintf("linux.* %s ", regexp.QuoteMeta(existsArg)), grubCfgContents, + "ensure kernel command arg exists (%s)", existsArg) + } } for _, notExistsArg := range notExistsArgs { - assert.NotRegexpf(t, fmt.Sprintf("linux.* %s ", regexp.QuoteMeta(notExistsArg)), grubCfgContents, + assert.NotContainsf(t, grubCfgContents, notExistsArg, "ensure kernel command arg not exists (%s)", notExistsArg) } } +func extractCmdlineFromUkiForTest(ukiDir string) (string, error) { + files, err := os.ReadDir(ukiDir) + if err != nil { + return "", fmt.Errorf("failed to read UKI directory: %w", err) + } + + for _, f := range files { + if strings.HasSuffix(f.Name(), ".efi") { + ukiPath := filepath.Join(ukiDir, f.Name()) + + tempDir, err := os.MkdirTemp("", "test-cmdline-extraction-*") + if err != nil { + return "", fmt.Errorf("failed to create temp directory: %w", err) + } + defer os.RemoveAll(tempDir) + + cmdlinePath := filepath.Join(tempDir, "cmdline.txt") + + tempCopy := filepath.Join(tempDir, "uki-copy.efi") + input, err := os.ReadFile(ukiPath) + if err != nil { + return "", fmt.Errorf("failed to read UKI file: %w", err) + } + if err := os.WriteFile(tempCopy, input, 0o644); err != nil { + return "", fmt.Errorf("failed to write temp UKI file: %w", err) + } + + _, _, err = shell.Execute("objcopy", "--dump-section", ".cmdline="+cmdlinePath, tempCopy) + if err != nil { + return "", fmt.Errorf("objcopy failed to extract cmdline: %w", err) + } + + content, err := os.ReadFile(cmdlinePath) + if err != nil { + return "", fmt.Errorf("failed to read cmdline file: %w", err) + } + + return string(content), nil + } + } + + return "", fmt.Errorf("no UKI files found or cmdline not extracted") +} + func verifySELinuxConfigFile(t *testing.T, imageConnection *imageconnection.ImageConnection, mode string) { selinuxConfigPath := filepath.Join(imageConnection.Chroot().RootDir(), "/etc/selinux/config") selinuxConfigContents, err := file.Read(selinuxConfigPath) diff --git a/toolkit/tools/pkg/imagecustomizerlib/customizeuki.go b/toolkit/tools/pkg/imagecustomizerlib/customizeuki.go index b36b9061f2..7fc5a3c0d1 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/customizeuki.go +++ b/toolkit/tools/pkg/imagecustomizerlib/customizeuki.go @@ -6,7 +6,9 @@ package imagecustomizerlib import ( "context" "encoding/json" + "errors" "fmt" + "io/fs" "os" "path/filepath" "regexp" @@ -17,7 +19,10 @@ import ( "github.com/microsoft/azure-linux-image-tools/toolkit/tools/imagecustomizerapi" "github.com/microsoft/azure-linux-image-tools/toolkit/tools/imagegen/diskutils" + "github.com/microsoft/azure-linux-image-tools/toolkit/tools/imagegen/installutils" "github.com/microsoft/azure-linux-image-tools/toolkit/tools/internal/file" + "github.com/microsoft/azure-linux-image-tools/toolkit/tools/internal/grub" + "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/safeloopback" @@ -38,6 +43,9 @@ var ( ErrUKIFileCopy = NewImageCustomizerError("UKI:FileCopy", "failed to copy UKI files") ErrUKIKernelCmdlineExtract = NewImageCustomizerError("UKI:KernelCmdlineExtract", "failed to extract kernel command-line arguments") ErrUKICmdlineFileWrite = NewImageCustomizerError("UKI:CmdlineFileWrite", "failed to write kernel cmdline args JSON") + ErrUKIExtractComponents = NewImageCustomizerError("UKI:ExtractComponents", "failed to extract kernel/initramfs from UKI") + ErrUKICleanOldFiles = NewImageCustomizerError("UKI:CleanOldFiles", "failed to clean old UKI files") + ErrUKICleanBootDir = NewImageCustomizerError("UKI:CleanBootDir", "failed to clean /boot directory") ) const ( @@ -59,8 +67,67 @@ type UkiKernelInfo struct { Initramfs string `json:"initramfs"` } -func prepareUki(ctx context.Context, buildDir string, uki *imagecustomizerapi.Uki, imageChroot *safechroot.Chroot, - distroHandler distroHandler, +func baseImageHasUkis(imageChroot *safechroot.Chroot) (bool, error) { + espDir := filepath.Join(imageChroot.RootDir(), EspDir) + ukiFiles, err := getUkiFiles(espDir) + if err != nil { + return false, fmt.Errorf("failed to check for UKI files:\n%w", err) + } + return len(ukiFiles) > 0, nil +} + +// validateUkiMode validates the UKI mode against the base image state. +// Rules: +// - If base image has NO UKIs: +// - No mode specified (os.uki == nil): No UKI created +// - mode: create: Create UKI +// - mode: passthrough: FAIL (can't passthrough if no UKIs exist) +// +// - If base image HAS UKIs: +// - No mode specified (os.uki == nil): FAIL (must explicitly specify mode) +// - mode: create: Extract and regenerate UKIs +// - mode: passthrough: Preserve existing UKIs without modification +func validateUkiMode(imageConnection *imageconnection.ImageConnection, config *imagecustomizerapi.Config) error { + hasUkis, err := baseImageHasUkis(imageConnection.Chroot()) + if err != nil { + return err + } + + if !hasUkis { + // Base image doesn't have UKIs + if config.OS != nil && config.OS.Uki != nil { + // User specified os.uki + if config.OS.Uki.Mode == imagecustomizerapi.UkiModePassthrough { + return fmt.Errorf("base image does not contain UKIs but os.uki.mode is set to 'passthrough': " + + "cannot passthrough UKIs when base image has no UKIs. " + + "Use mode: create to create UKIs, or omit os.uki entirely", + ) + } + // mode: create or unspecified (with os.uki present) - both are OK for creating UKIs + } + // No os.uki specified - that's fine, no UKI will be created + return nil + } + + // Base image has UKIs + if config.OS == nil || config.OS.Uki == nil { + return fmt.Errorf("base image contains UKI files but os.uki is not specified: " + + "when base image has UKIs, you must explicitly specify how to handle them using os.uki.mode " + + "with one of the following values:\n" + + " - 'create': extract and regenerate UKIs with updated configurations\n" + + " - 'passthrough': preserve existing UKIs without modification (e.g., to keep signatures intact)") + } + + if config.OS.Uki.Mode == imagecustomizerapi.UkiModeUnspecified { + return fmt.Errorf("base image contains UKI files but os.uki.mode is not specified: " + + "when base image has UKIs, you must explicitly set mode to either 'create' or 'passthrough'") + } + + return nil +} + +func prepareUki(ctx context.Context, buildDir string, uki *imagecustomizerapi.Uki, + imageChroot *safechroot.Chroot, distroHandler distroHandler, ) error { err := prepareUkiHelper(ctx, buildDir, uki, imageChroot, distroHandler) if err != nil { @@ -70,8 +137,8 @@ func prepareUki(ctx context.Context, buildDir string, uki *imagecustomizerapi.Uk return nil } -func prepareUkiHelper(ctx context.Context, buildDir string, uki *imagecustomizerapi.Uki, imageChroot *safechroot.Chroot, - distroHandler distroHandler, +func prepareUkiHelper(ctx context.Context, buildDir string, uki *imagecustomizerapi.Uki, + imageChroot *safechroot.Chroot, distroHandler distroHandler, ) error { var err error @@ -79,6 +146,12 @@ func prepareUkiHelper(ctx context.Context, buildDir string, uki *imagecustomizer return nil } + // If mode is 'passthrough', skip UKI regeneration to preserve existing UKIs + if uki.Mode == imagecustomizerapi.UkiModePassthrough { + logger.Log.Infof("UKI mode is 'passthrough', skipping UKI regeneration") + return nil + } + logger.Log.Infof("Enabling UKI") _, span := otel.GetTracerProvider().Tracer(OtelTracerName).Start(ctx, "enable_uki") @@ -156,7 +229,7 @@ func prepareUkiHelper(ctx context.Context, buildDir string, uki *imagecustomizer // Map kernels and initramfs. bootDir := filepath.Join(imageChroot.RootDir(), BootDir) - kernelToInitramfs, err := getKernelToInitramfsMap(bootDir, uki.Kernels) + kernelToInitramfs, err := getKernelToInitramfsMap(bootDir) if err != nil { return fmt.Errorf("%w (bootDir='%s'):\n%w", ErrUKIKernelInitramfsMap, bootDir, err) } @@ -174,13 +247,20 @@ func prepareUkiHelper(ctx context.Context, buildDir string, uki *imagecustomizer return fmt.Errorf("%w:\n%w", ErrUKIKernelCmdlineExtract, err) } + err = cleanBootDirectory(imageChroot) + if err != nil { + return fmt.Errorf("%w:\n%w", ErrUKICleanBootDir, err) + } + // Combine kernel-to-initramfs mapping and kernel command line arguments into a single structure. 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) } + kernelInfo[kernel] = UkiKernelInfo{ Cmdline: cmdline, Initramfs: initramfs, @@ -260,22 +340,11 @@ func copyUkiFiles(buildDir string, kernelToInitramfs map[string]string, imageChr return nil } -func getKernelToInitramfsMap(bootDir string, ukiKernels imagecustomizerapi.UkiKernels) (map[string]string, error) { - if ukiKernels.Auto { - // Auto mode: Find all kernels and their initramfs. - kernelToInitramfs, err := findKernelsAndInitramfs(bootDir) - if err != nil { - return nil, fmt.Errorf("failed to find kernels and initramfs in auto mode:\n%w", err) - } - return kernelToInitramfs, nil - } - - // User-specified mode: Match kernels and initramfs with the specified versions. - kernelToInitramfs, err := findSpecificKernelsAndInitramfs(bootDir, ukiKernels.Kernels) +func getKernelToInitramfsMap(bootDir string) (map[string]string, error) { + kernelToInitramfs, err := findKernelsAndInitramfs(bootDir) if err != nil { - return nil, fmt.Errorf("failed to find specific kernels and initramfs:\n%w", err) + return nil, fmt.Errorf("failed to find kernels and initramfs:\n%w", err) } - return kernelToInitramfs, nil } @@ -316,41 +385,15 @@ func findKernelsAndInitramfs(bootDir string) (map[string]string, error) { return kernelToInitramfs, nil } -func findSpecificKernelsAndInitramfs(bootDir string, versions []string) (map[string]string, error) { - kernelToInitramfs := make(map[string]string) - - for _, version := range versions { - kernelName := fmt.Sprintf("vmlinuz-%s", version) - initramfsName := fmt.Sprintf("initramfs-%s.img", version) - - kernelPath := filepath.Join(bootDir, kernelName) - initramfsPath := filepath.Join(bootDir, initramfsName) - - kernelExists, err := file.PathExists(kernelPath) - if err != nil { - return nil, fmt.Errorf("error checking existence of kernel (%s):\n%w", kernelPath, err) - } - if !kernelExists { - return nil, fmt.Errorf("missing kernel: (%s)", kernelName) - } - - initramfsExists, err := file.PathExists(initramfsPath) - if err != nil { - return nil, fmt.Errorf("error checking existence of initramfs (%s):\n%w", initramfsPath, err) - } - if !initramfsExists { - return nil, fmt.Errorf("missing initramfs for kernel: (%s), expected (%s)", kernelName, initramfsName) - } +func createUki(ctx context.Context, buildDir string, buildImageFile string, uki *imagecustomizerapi.Uki) error { + logger.Log.Infof("Creating UKIs") - kernelToInitramfs[kernelName] = initramfsName + // If mode is 'passthrough', skip UKI creation to preserve existing UKIs + if uki != nil && uki.Mode == imagecustomizerapi.UkiModePassthrough { + logger.Log.Infof("UKI mode is 'passthrough', skipping UKI creation") + return nil } - return kernelToInitramfs, nil -} - -func createUki(ctx context.Context, buildDir string, buildImageFile string) error { - logger.Log.Infof("Creating UKIs") - _, span := otel.GetTracerProvider().Tracer(OtelTracerName).Start(ctx, "customize_uki") defer span.End() @@ -384,6 +427,12 @@ func createUki(ctx context.Context, buildDir string, buildImageFile string) erro } defer systemBootPartitionMount.Close() + ukiOutputFullPath := filepath.Join(systemBootPartitionTmpDir, UkiOutputDir) + err = cleanUkiDirectory(ukiOutputFullPath) + if err != nil { + return fmt.Errorf("%w:\n%w", ErrUKICleanOldFiles, err) + } + stubPath := filepath.Join(buildDir, UkiBuildDir, bootConfig.ukiEfiStubBinary) osSubreleaseFullPath := filepath.Join(buildDir, UkiBuildDir, "os-release") cmdlineFilePath := filepath.Join(buildDir, UkiBuildDir, UkiKernelInfoJson) @@ -425,20 +474,25 @@ func extractKernelToArgs(espPath string, bootDir string, buildDir string) (map[s // Try extracting from grub.cfg first grubCfgPath := filepath.Join(bootDir, DefaultGrubCfgPath) kernelToArgs, err := extractKernelToArgsFromGrub(grubCfgPath) - if err != nil && !os.IsNotExist(err) { + if err != nil && !errors.Is(err, fs.ErrNotExist) { return nil, fmt.Errorf("failed to extract kernel args from grub.cfg:\n%w", err) - } else if !os.IsNotExist(err) { + } else if !errors.Is(err, fs.ErrNotExist) && len(kernelToArgs) > 0 { + // Successfully extracted kernel cmdline from grub.cfg return kernelToArgs, nil } // Fallback to extracting from UKI kernelToArgs, err = extractKernelCmdlineFromUkiEfis(espPath, buildDir) - if err != nil && !os.IsNotExist(err) { + if err != nil && !errors.Is(err, fs.ErrNotExist) { return nil, fmt.Errorf("failed to extract kernel args from UKI:\n%w", err) - } else if os.IsNotExist(err) { + } else if errors.Is(err, fs.ErrNotExist) { return nil, fmt.Errorf("no kernel arguments found from either grub.cfg or UKI") } + if len(kernelToArgs) == 0 { + return nil, fmt.Errorf("no kernel command-line arguments extracted from UKI files in (%s)", espPath) + } + return kernelToArgs, nil } @@ -559,7 +613,9 @@ func appendKernelArgsToUkiCmdlineFile(buildDir string, newArgs []string) error { // Append newArgs. newArgsStr := GrubArgsToString(newArgs) for kernel, info := range kernelInfo { - updatedArgs := fmt.Sprintf("%s %s", strings.TrimSpace(info.Cmdline), strings.TrimSpace(newArgsStr)) + // Remove old verity args before appending new ones to avoid duplicates. + cleanedCmdline := removeVerityArgsFromCmdline(info.Cmdline) + updatedArgs := fmt.Sprintf("%s %s", strings.TrimSpace(cleanedCmdline), strings.TrimSpace(newArgsStr)) kernelInfo[kernel] = UkiKernelInfo{ Cmdline: updatedArgs, Initramfs: info.Initramfs, @@ -574,6 +630,58 @@ func appendKernelArgsToUkiCmdlineFile(buildDir string, newArgs []string) error { return nil } +// removeVerityArgsFromCmdline removes all verity-related kernel arguments from a command line string. +// This is used when updating verity parameters during UKI recustomization to prevent duplicate args. +func removeVerityArgsFromCmdline(cmdline string) string { + // List of verity-related argument prefixes that need to be removed + verityArgPrefixes := []string{ + "rd.systemd.verity=", + "roothash=", + "usrhash=", + "systemd.verity_root_data=", + "systemd.verity_root_hash=", + "systemd.verity_root_options=", + "systemd.verity_usr_data=", + "systemd.verity_usr_hash=", + "systemd.verity_usr_options=", + "pre.verity.mount=", + } + + tokens, err := grub.TokenizeConfig(cmdline) + if err != nil { + logger.Log.Errorf("Failed to tokenize cmdline with GRUB parser: %v", err) + return cmdline + } + + args, err := ParseCommandLineArgs(tokens) + if err != nil { + logger.Log.Errorf("Failed to parse command line args: %v", err) + return cmdline + } + + filteredArgs := []string{} + for _, arg := range args { + if arg.ValueHasVarExpansion { + // Skip args with variable expansions + continue + } + + isVerityArg := false + for _, prefix := range verityArgPrefixes { + if strings.HasPrefix(arg.Arg, prefix) { + isVerityArg = true + break + } + } + + if !isVerityArg { + filteredArgs = append(filteredArgs, arg.Arg) + } + } + + return GrubArgsToString(filteredArgs) +} + func getKernelVersion(kernelName string) (string, error) { if !strings.HasPrefix(kernelName, KernelPrefix) { return "", fmt.Errorf("invalid kernel name: (%s), expected to start with prefix: (%s)", kernelName, KernelPrefix) @@ -624,3 +732,163 @@ func getKernelNameFromUki(ukiPath string) (string, error) { kernelName := "vmlinuz-" + matches[1] return kernelName, nil } + +func extractSectionFromUkiWithObjcopy(ukiPath string, sectionName string, outputPath string, buildDir string) error { + tempCopy, err := os.CreateTemp(buildDir, "uki-copy-*.efi") + if err != nil { + return fmt.Errorf("failed to create temp UKI copy:\n%w", err) + } + defer os.Remove(tempCopy.Name()) + + input, err := os.ReadFile(ukiPath) + if err != nil { + return fmt.Errorf("failed to read UKI file:\n%w", err) + } + if err := os.WriteFile(tempCopy.Name(), input, 0o644); err != nil { + return fmt.Errorf("failed to write temp UKI file:\n%w", err) + } + + // Extract the section using objcopy on the temp copy + _, _, err = shell.Execute("objcopy", "--dump-section", sectionName+"="+outputPath, tempCopy.Name()) + if err != nil { + return fmt.Errorf("objcopy failed to extract section %s:\n%w", sectionName, err) + } + + return nil +} + +func extractKernelAndInitramfsFromUkis(ctx context.Context, imageChroot *safechroot.Chroot, buildDir string) error { + err := extractKernelAndInitramfsFromUkisHelper(ctx, imageChroot, buildDir) + if err != nil { + return fmt.Errorf("%w:\n%w", ErrUKIExtractComponents, err) + } + + return nil +} + +func extractKernelAndInitramfsFromUkisHelper(ctx context.Context, imageChroot *safechroot.Chroot, buildDir string) error { + logger.Log.Infof("Extracting kernel and initramfs from existing UKIs for re-customization") + + _, span := otel.GetTracerProvider().Tracer(OtelTracerName).Start(ctx, "extract_kernel_initramfs_from_ukis") + defer span.End() + + espDir := filepath.Join(imageChroot.RootDir(), EspDir) + ukiFiles, err := getUkiFiles(espDir) + if err != nil { + return err + } + + if len(ukiFiles) == 0 { + logger.Log.Infof("No existing UKI files found, skipping extraction") + return nil + } + + bootDir := filepath.Join(imageChroot.RootDir(), BootDir) + + tempDir := filepath.Join(buildDir, "uki-extraction-temp") + err = os.MkdirAll(tempDir, 0o755) + if err != nil { + return fmt.Errorf("failed to create temp directory:\n%w", err) + } + defer os.RemoveAll(tempDir) + + for _, ukiFile := range ukiFiles { + kernelName, err := getKernelNameFromUki(ukiFile) + if err != nil { + return err + } + + kernelVersion, err := getKernelVersion(kernelName) + if err != nil { + return err + } + + kernelPath := filepath.Join(bootDir, kernelName) + logger.Log.Infof("Extracting kernel from UKI (%s) to (%s)", ukiFile, kernelPath) + err = extractSectionFromUkiWithObjcopy(ukiFile, ".linux", kernelPath, tempDir) + if err != nil { + return fmt.Errorf("failed to extract kernel from UKI (%s):\n%w", ukiFile, err) + } + + initramfsName := fmt.Sprintf("initramfs-%s.img", kernelVersion) + initramfsPath := filepath.Join(bootDir, initramfsName) + logger.Log.Infof("Extracting initramfs from UKI (%s) to (%s)", ukiFile, initramfsPath) + err = extractSectionFromUkiWithObjcopy(ukiFile, ".initrd", initramfsPath, tempDir) + if err != nil { + return fmt.Errorf("failed to extract initramfs from UKI (%s):\n%w", ukiFile, err) + } + + 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.GrubCfgFile)) + 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) + } + + return nil +} + +func cleanUkiDirectory(ukiOutputDir string) error { + if _, err := os.Stat(ukiOutputDir); errors.Is(err, fs.ErrNotExist) { + logger.Log.Debugf("UKI output directory does not exist, nothing to clean: (%s)", ukiOutputDir) + return nil + } + + files, err := os.ReadDir(ukiOutputDir) + if err != nil { + return fmt.Errorf("failed to read UKI output directory (%s):\n%w", ukiOutputDir, err) + } + + for _, file := range files { + if file.IsDir() { + continue + } + + if strings.HasSuffix(strings.ToLower(file.Name()), ".efi") { + filePath := filepath.Join(ukiOutputDir, file.Name()) + err := os.Remove(filePath) + if err != nil { + return fmt.Errorf("failed to delete old UKI file (%s):\n%w", filePath, err) + } + logger.Log.Infof("Deleted old UKI file: (%s)", filePath) + } + } + + return nil +} + +func cleanBootDirectory(imageChroot *safechroot.Chroot) error { + bootPath := filepath.Join(imageChroot.RootDir(), BootDir) + espPath := filepath.Join(imageChroot.RootDir(), EspDir) + + dirEntries, err := os.ReadDir(bootPath) + if err != nil { + return fmt.Errorf("failed to read boot directory (%s):\n%w", bootPath, err) + } + + for _, entry := range dirEntries { + entryPath := filepath.Join(bootPath, entry.Name()) + + if entryPath == espPath { + continue + } + + err := os.RemoveAll(entryPath) + if err != nil { + return fmt.Errorf("failed to remove (%s):\n%w", entryPath, err) + } + } + + return nil +} diff --git a/toolkit/tools/pkg/imagecustomizerlib/customizeuki_test.go b/toolkit/tools/pkg/imagecustomizerlib/customizeuki_test.go index e5231e80a0..0b2077bcf7 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/customizeuki_test.go +++ b/toolkit/tools/pkg/imagecustomizerlib/customizeuki_test.go @@ -62,6 +62,151 @@ func TestCustomizeImageVerityUsrUki(t *testing.T) { verifyUsrVerity(t, buildDir, outImageFilePath2, ukiFilesChecksums) } +func TestCustomizeImageVerityUsrUkiRecustomize(t *testing.T) { + baseImageInfo := testBaseImageAzl3CoreEfi + baseImage := checkSkipForCustomizeImage(t, baseImageInfo) + + ukifyExists, err := file.CommandExists("ukify") + assert.NoError(t, err) + if !ukifyExists { + t.Skip("The 'ukify' command is not available") + } + + if runtime.GOARCH == "arm64" { + t.Skip("systemd-boot not available on AZL3 ARM64 yet") + } + + testTempDir := filepath.Join(tmpDir, "TestCustomizeImageUsrVerityUkiRecustomize") + defer os.RemoveAll(testTempDir) + + buildDir := filepath.Join(testTempDir, "build") + outImageFilePath := filepath.Join(testTempDir, "image.raw") + configFile := filepath.Join(testDir, "verity-usr-uki.yaml") + + err = CustomizeImageWithConfigFile(t.Context(), buildDir, configFile, baseImage, nil, outImageFilePath, "raw", + true /*useBaseImageRpmRepos*/, "" /*packageSnapshotTime*/) + if !assert.NoError(t, err) { + return + } + + ukiFilesChecksums, ok := verifyUsrVerity(t, buildDir, outImageFilePath, nil) + if !ok { + return + } + + outImageFilePath2 := filepath.Join(testTempDir, "image2.raw") + configFile2 := filepath.Join(testDir, "verity-reinit-usr-uki.yaml") + + err = CustomizeImageWithConfigFile(t.Context(), buildDir, configFile2, outImageFilePath, nil, outImageFilePath2, "raw", + true /*useBaseImageRpmRepos*/, "" /*packageSnapshotTime*/) + if !assert.NoError(t, err) { + return + } + + newUkiFilesChecksums, ok := verifyUsrVerity(t, buildDir, outImageFilePath2, nil) + if !ok { + return + } + + for ukiFile := range ukiFilesChecksums { + oldChecksum := ukiFilesChecksums[ukiFile] + newChecksum, exists := newUkiFilesChecksums[ukiFile] + assert.True(t, exists, "UKI file should exist after re-customization: %s", ukiFile) + assert.NotEqual(t, oldChecksum, newChecksum, "UKI checksum should change after verity re-initialization: %s", ukiFile) + } + + mountPoints := []testutils.MountPoint{ + { + PartitionNum: 1, + Path: "/boot/efi", + FileSystemType: "vfat", + }, + { + PartitionNum: 2, + Path: "/boot", + FileSystemType: "ext4", + }, + { + PartitionNum: 5, + Path: "/", + FileSystemType: "ext4", + }, + } + + imageConnection, err := testutils.ConnectToImage(buildDir, outImageFilePath2, false /*includeDefaultMounts*/, mountPoints) + if !assert.NoError(t, err) { + return + } + defer imageConnection.Close() + + bootPath := filepath.Join(imageConnection.Chroot().RootDir(), "/boot") + bootEntries, err := os.ReadDir(bootPath) + assert.NoError(t, err) + + // /boot should only contain the "efi" directory. + assert.Equal(t, 1, len(bootEntries), "/boot should only contain the 'efi' directory") + if len(bootEntries) == 1 { + assert.Equal(t, "efi", bootEntries[0].Name(), "/boot should only contain the 'efi' directory") + assert.True(t, bootEntries[0].IsDir(), "'efi' should be a directory") + } +} + +func TestCustomizeImageVerityUsrUkiPassthrough(t *testing.T) { + baseImageInfo := testBaseImageAzl3CoreEfi + baseImage := checkSkipForCustomizeImage(t, baseImageInfo) + + ukifyExists, err := file.CommandExists("ukify") + assert.NoError(t, err) + if !ukifyExists { + t.Skip("The 'ukify' command is not available") + } + + if runtime.GOARCH == "arm64" { + t.Skip("systemd-boot not available on AZL3 ARM64 yet") + } + + testTempDir := filepath.Join(tmpDir, "TestCustomizeImageUsrVerityUkiPassthrough") + defer os.RemoveAll(testTempDir) + + buildDir := filepath.Join(testTempDir, "build") + outImageFilePath := filepath.Join(testTempDir, "image.raw") + configFile := filepath.Join(testDir, "verity-usr-uki.yaml") + + err = CustomizeImageWithConfigFile(t.Context(), buildDir, configFile, baseImage, nil, outImageFilePath, "raw", + true /*useBaseImageRpmRepos*/, "" /*packageSnapshotTime*/) + if !assert.NoError(t, err) { + return + } + + ukiFilesChecksums, ok := verifyUsrVerity(t, buildDir, outImageFilePath, nil) + if !ok { + return + } + + outImageFilePath2 := filepath.Join(testTempDir, "image-passthrough.raw") + configFile2 := filepath.Join(testDir, "verity-usr-uki-passthrough.yaml") + + err = CustomizeImageWithConfigFile(t.Context(), buildDir, configFile2, outImageFilePath, nil, outImageFilePath2, "raw", + true /*useBaseImageRpmRepos*/, "" /*packageSnapshotTime*/) + if !assert.NoError(t, err) { + return + } + + passthroughUkiChecksums, ok := verifyUsrVerity(t, buildDir, outImageFilePath2, ukiFilesChecksums) + if !ok { + return + } + + // Verify UKI checksums are unchanged. + for ukiFile := range ukiFilesChecksums { + originalChecksum := ukiFilesChecksums[ukiFile] + passthroughChecksum, exists := passthroughUkiChecksums[ukiFile] + assert.True(t, exists, "UKI file should exist after passthrough customization: %s", ukiFile) + assert.Equal(t, originalChecksum, passthroughChecksum, + "UKI checksum MUST NOT change in passthrough mode: %s", ukiFile) + } +} + func TestCustomizeImageVerityRootUki(t *testing.T) { baseImageInfo := testBaseImageAzl3CoreEfi baseImage := checkSkipForCustomizeImage(t, baseImageInfo) diff --git a/toolkit/tools/pkg/imagecustomizerlib/customizeverity.go b/toolkit/tools/pkg/imagecustomizerlib/customizeverity.go index 5abb7ac189..8118140c15 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/customizeverity.go +++ b/toolkit/tools/pkg/imagecustomizerlib/customizeverity.go @@ -683,7 +683,7 @@ func customizeVerityImageHelper(ctx context.Context, buildDir string, config *im } // Update kernel args. - isUki := config.OS.Uki != nil + isUki := config.OS.Uki != nil && config.OS.Uki.Mode == imagecustomizerapi.UkiModeCreate err = updateKernelArgsForVerity(buildDir, diskPartitions, verityMetadata, isUki, partitionsLayout) if err != nil { return nil, err @@ -770,6 +770,16 @@ func updateKernelArgsForVerity(buildDir string, diskPartitions []diskutils.Parti return err } + if isUki { + err = updateUkiKernelArgsForVerity(verityMetadata, diskPartitions, buildDir, bootPartition.Uuid) + if err != nil { + return fmt.Errorf("%w:\n%w", ErrUpdateKernelArgs, err) + } + + // When UKI is enabled, /boot is cleaned. Return early to skip grub.cfg update. + return nil + } + bootPartitionTmpDir := filepath.Join(buildDir, tmpBootPartitionDirName) // Temporarily mount the partition. bootPartitionMount, err := safemount.NewMount(bootPartition.Path, bootPartitionTmpDir, bootPartition.FileSystemType, @@ -785,17 +795,7 @@ func updateKernelArgsForVerity(buildDir string, diskPartitions []diskutils.Parti return fmt.Errorf("%w (file='%s'):\n%w", ErrStatFile, grubCfgFullPath, err) } - if isUki { - // UKI is enabled, update kernel cmdline args file. - err = updateUkiKernelArgsForVerity(verityMetadata, diskPartitions, buildDir, bootPartition.Uuid) - if err != nil { - return fmt.Errorf("%w:\n%w", ErrUpdateKernelArgs, err) - } - } - - // Temporarily always update grub.cfg for verity, even when UKI is used. - // Since grub dependencies are still kept under /boot and won't be cleaned. - // This will be decoupled once the bootloader project is in place. + // Update grub.cfg for verity of non-UKI path only. err = updateGrubConfigForVerity(verityMetadata, grubCfgFullPath, diskPartitions, buildDir, bootPartition.Uuid) if err != nil { return fmt.Errorf("%w:\n%w", ErrUpdateGrubConfig, err) diff --git a/toolkit/tools/pkg/imagecustomizerlib/defaultgrubutils.go b/toolkit/tools/pkg/imagecustomizerlib/defaultgrubutils.go index 8ad6e00b80..a7cd2873dc 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/defaultgrubutils.go +++ b/toolkit/tools/pkg/imagecustomizerlib/defaultgrubutils.go @@ -350,17 +350,6 @@ func UpdateDefaultGrubFileVariable(defaultGrubFileContent string, varName string return defaultGrubFileContent, nil } -// Checks if the image uses grub-mkconfig. -func isGrubMkconfigEnabled(imageChroot *safechroot.Chroot) (bool, error) { - grub2ConfigFile, err := ReadGrub2ConfigFile(imageChroot) - if err != nil { - return false, err - } - - grubMkconfigEnabled := isGrubMkconfigConfig(grub2ConfigFile) - return grubMkconfigEnabled, nil -} - // Takes the string contents of the grub.cfg file and checks if it was generated by the grub-mkconfig tool. func isGrubMkconfigConfig(grub2Config string) bool { grubMkconfigEnabled := strings.Contains(grub2Config, grubMkconfigHeader) diff --git a/toolkit/tools/pkg/imagecustomizerlib/imagecustomizer.go b/toolkit/tools/pkg/imagecustomizerlib/imagecustomizer.go index 5cc3c6b544..d86457970d 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/imagecustomizer.go +++ b/toolkit/tools/pkg/imagecustomizerlib/imagecustomizer.go @@ -536,7 +536,7 @@ func customizeOSContents(ctx context.Context, rc *ResolvedConfig) (imageMetadata } if rc.Uki != nil { - err = createUki(ctx, rc.BuildDirAbs, rc.RawImageFile) + err = createUki(ctx, rc.BuildDirAbs, rc.RawImageFile, rc.Uki) if err != nil { return im, fmt.Errorf("%w:\n%w", ErrCustomizeCreateUkis, err) } @@ -702,6 +702,11 @@ func customizeImageHelper(ctx context.Context, rc *ResolvedConfig, partitionsCus return nil }) + err = validateUkiMode(imageConnection, rc.Config) + if err != nil { + return nil, nil, nil, "", err + } + err = validateVerityMountPaths(imageConnection, rc.Config, partitionsLayout, baseImageVerityMetadata) if err != nil { return nil, nil, nil, "", fmt.Errorf("%w:\n%w", ErrVerityValidation, err) diff --git a/toolkit/tools/pkg/imagecustomizerlib/imageutils.go b/toolkit/tools/pkg/imagecustomizerlib/imageutils.go index 40fe81488c..3d14300a4f 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/imageutils.go +++ b/toolkit/tools/pkg/imagecustomizerlib/imageutils.go @@ -231,16 +231,22 @@ func configureDiskBootLoader(imageConnection *imageconnection.ImageConnection, r return err } - // TODO: Remove this once we have a way to determine if grub-mkconfig is enabled. - grubMkconfigEnabled := true - if !newImage { - grubMkconfigEnabled, err = isGrubMkconfigEnabled(imageConnection.Chroot()) + // Determine the boot configuration type. + // For new images, default to grub-mkconfig (AZL3 default). + // For existing images, detect the actual boot configuration. + var bootConfigType bootConfigType + if newImage { + bootConfigType = bootConfigTypeGrubMkconfig + } else { + bootCustomizer, err := NewBootCustomizer(imageConnection.Chroot()) if err != nil { return err } - + bootConfigType = bootCustomizer.bootConfigType } + grubMkconfigEnabled := (bootConfigType == bootConfigTypeGrubMkconfig) + mountPointMap := make(map[string]string) for _, mountPoint := range imageConnection.Chroot().GetMountPoints() { mountPointMap[mountPoint.GetTarget()] = mountPoint.GetSource() diff --git a/toolkit/tools/pkg/imagecustomizerlib/liveosisoartifactstore.go b/toolkit/tools/pkg/imagecustomizerlib/liveosisoartifactstore.go index 8a3bcf57ae..3842bc2b5d 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/liveosisoartifactstore.go +++ b/toolkit/tools/pkg/imagecustomizerlib/liveosisoartifactstore.go @@ -97,13 +97,13 @@ func containsGrubNoPrefix(filePaths []string) (bool, error) { return false, nil } -func getSELinuxMode(imageChroot *safechroot.Chroot) (imagecustomizerapi.SELinuxMode, error) { +func getSELinuxMode(buildDir string, imageChroot *safechroot.Chroot) (imagecustomizerapi.SELinuxMode, error) { bootCustomizer, err := NewBootCustomizer(imageChroot) if err != nil { return imagecustomizerapi.SELinuxModeDefault, err } - imageSELinuxMode, err := bootCustomizer.GetSELinuxMode(imageChroot) + imageSELinuxMode, err := bootCustomizer.GetSELinuxMode(buildDir, imageChroot) if err != nil { return imagecustomizerapi.SELinuxModeDefault, fmt.Errorf("failed to get current SELinux mode:\n%w", err) } @@ -370,7 +370,7 @@ func createIsoFilesStoreFromMountedImage(inputArtifactsStore *IsoArtifactsStore, return filesStore, nil } -func createIsoInfoStoreFromMountedImage(imageRootDir string) (infoStore *IsoInfoStore, err error) { +func createIsoInfoStoreFromMountedImage(buildDir string, imageRootDir string) (infoStore *IsoInfoStore, err error) { infoStore = &IsoInfoStore{} chroot := safechroot.NewChroot(imageRootDir, true /*isExistingDir*/) @@ -384,7 +384,7 @@ func createIsoInfoStoreFromMountedImage(imageRootDir string) (infoStore *IsoInfo return nil, fmt.Errorf("failed to initialize chroot object for (%s):\n%w", imageRootDir, err) } - imageSELinuxMode, err := getSELinuxMode(chroot) + imageSELinuxMode, err := getSELinuxMode(buildDir, chroot) if err != nil { return nil, fmt.Errorf("failed to determine SELinux mode for (%s):\n%w", imageRootDir, err) } @@ -520,7 +520,7 @@ func createIsoArtifactStoreFromMountedImage(inputArtifactsStore *IsoArtifactsSto } artifactStore.files = filesStore - infoStore, err := createIsoInfoStoreFromMountedImage(imageRootDir) + infoStore, err := createIsoInfoStoreFromMountedImage(storeDir, imageRootDir) if err != nil { return nil, err } diff --git a/toolkit/tools/pkg/imagecustomizerlib/liveosisobuilder.go b/toolkit/tools/pkg/imagecustomizerlib/liveosisobuilder.go index b6f1b5cb13..f725c20d16 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/liveosisobuilder.go +++ b/toolkit/tools/pkg/imagecustomizerlib/liveosisobuilder.go @@ -210,7 +210,7 @@ func createLiveOSFromRawHelper(ctx context.Context, buildDir, baseConfigPath str return fmt.Errorf("failed to attach to raw image to inspect selinux status:\n%w", err) } - selinuxMode, err := bootCustomizer.GetSELinuxMode(rawImageConnection.Chroot()) + selinuxMode, err := bootCustomizer.GetSELinuxMode(isoBuildDir, rawImageConnection.Chroot()) if err != nil { return fmt.Errorf("failed to get selinux mode:\n%w", err) } diff --git a/toolkit/tools/pkg/imagecustomizerlib/partitionutils.go b/toolkit/tools/pkg/imagecustomizerlib/partitionutils.go index 7833d1ba70..3ecec9ced8 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/partitionutils.go +++ b/toolkit/tools/pkg/imagecustomizerlib/partitionutils.go @@ -4,7 +4,9 @@ package imagecustomizerlib import ( + "errors" "fmt" + "io/fs" "iter" "os" "path/filepath" @@ -599,10 +601,11 @@ func extractKernelCmdline(fstabEntries []diskutils.FstabEntry, diskPartitions [] return nil, fmt.Errorf("failed to find /boot partition:\n%w", err) } - cmdline, err := extractKernelCmdlineFromGrub(bootDirPartition, bootDirPath, buildDir) - if err != nil && !os.IsNotExist(err) { + cmdline, found, err := extractKernelCmdlineFromGrub(bootDirPartition, bootDirPath, buildDir) + if err != nil { return nil, fmt.Errorf("failed to extract kernel arguments from grub.cfg:\n%w", err) - } else if !os.IsNotExist(err) { + } + if found { return cmdline, nil } @@ -612,9 +615,9 @@ func extractKernelCmdline(fstabEntries []diskutils.FstabEntry, diskPartitions [] } cmdline, err = extractKernelCmdlineFromUki(espPartition, buildDir) - if err != nil && !os.IsNotExist(err) { + if err != nil && !errors.Is(err, fs.ErrNotExist) { return nil, fmt.Errorf("failed to extract kernel arguments from UKI:\n%w", err) - } else if os.IsNotExist(err) { + } else if errors.Is(err, fs.ErrNotExist) { return nil, fmt.Errorf("no kernel arguments found from either grub.cfg or UKI") } @@ -669,21 +672,9 @@ func extractKernelCmdlineFromUki(espPartition *diskutils.PartitionInfo, return nil, err } - // Assumes only one UKI is needed, uses the first entry in the map. - var firstCmdline string - for _, cmdline := range kernelToArgs { - firstCmdline = cmdline - break - } - - tokens, err := grub.TokenizeConfig(firstCmdline) - if err != nil { - return nil, fmt.Errorf("failed to tokenize kernel command-line from UKI:\n%w", err) - } - - args, err := ParseCommandLineArgs(tokens) + args, err := parseFirstCmdlineFromUkiMap(kernelToArgs) if err != nil { - return nil, fmt.Errorf("failed to parse kernel command-line from UKI:\n%w", err) + return nil, err } err = espPartitionMount.CleanClose() @@ -704,6 +695,33 @@ func getUkiFiles(espPath string) ([]string, error) { return ukiFiles, nil } +// parseFirstCmdlineFromUkiMap extracts the first cmdline from a kernel->cmdline map, +// tokenizes it, and parses it into grub command-line arguments. +func parseFirstCmdlineFromUkiMap(kernelToArgs map[string]string) ([]grubConfigLinuxArg, error) { + if len(kernelToArgs) == 0 { + return nil, fmt.Errorf("no UKI kernel command-lines found") + } + + // Use the first kernel's cmdline since they should all have the same settings. + var firstCmdline string + for _, cmdline := range kernelToArgs { + firstCmdline = cmdline + break + } + + tokens, err := grub.TokenizeConfig(firstCmdline) + if err != nil { + return nil, fmt.Errorf("failed to tokenize kernel command-line from UKI:\n%w", err) + } + + args, err := ParseCommandLineArgs(tokens) + if err != nil { + return nil, fmt.Errorf("failed to parse kernel command-line from UKI:\n%w", err) + } + + return args, nil +} + func extractKernelCmdlineFromUkiEfis(espPath string, buildDir string) (map[string]string, error) { ukiFiles, err := getUkiFiles(espPath) if err != nil { @@ -729,32 +747,15 @@ func extractKernelCmdlineFromUkiEfis(espPath string, buildDir string) (map[strin } func extractCmdlineFromUkiWithObjcopy(originalPath, buildDir string) (string, error) { - // Create a temporary copy of UKI files to avoid modifying the original file, - // since objcopy might tamper with signatures or hashes. - tempCopy, err := os.CreateTemp(buildDir, "uki-copy-*.efi") - if err != nil { - return "", fmt.Errorf("failed to create temp UKI copy:\n%w", err) - } - defer os.Remove(tempCopy.Name()) - - input, err := os.ReadFile(originalPath) - if err != nil { - return "", fmt.Errorf("failed to read UKI file:\n%w", err) - } - if err := os.WriteFile(tempCopy.Name(), input, 0o644); err != nil { - return "", fmt.Errorf("failed to write temp UKI file:\n%w", err) - } - cmdlinePath, err := os.CreateTemp(buildDir, "cmdline-*.txt") if err != nil { return "", fmt.Errorf("failed to create temp cmdline file:\n%w", err) } - cmdlinePath.Close() defer os.Remove(cmdlinePath.Name()) - _, _, err = shell.Execute("objcopy", "--dump-section", ".cmdline="+cmdlinePath.Name(), tempCopy.Name()) + err = extractSectionFromUkiWithObjcopy(originalPath, ".cmdline", cmdlinePath.Name(), buildDir) if err != nil { - return "", fmt.Errorf("objcopy failed:\n%w", err) + return "", fmt.Errorf("failed to extract cmdline section:\n%w", err) } content, err := os.ReadFile(cmdlinePath.Name()) @@ -767,33 +768,46 @@ func extractCmdlineFromUkiWithObjcopy(originalPath, buildDir string) (string, er func extractKernelCmdlineFromGrub(bootPartition diskutils.PartitionInfo, bootDirPath string, buildDir string, -) ([]grubConfigLinuxArg, error) { +) ([]grubConfigLinuxArg, bool, error) { tmpDirBoot := filepath.Join(buildDir, tmpBootPartitionDirName) bootPartitionMount, err := safemount.NewMount(bootPartition.Path, tmpDirBoot, bootPartition.FileSystemType, unix.MS_RDONLY, "", true) if err != nil { - return nil, fmt.Errorf("failed to mount boot partition (%s):\n%w", bootPartition.Path, err) + return nil, false, fmt.Errorf("failed to mount boot partition (%s):\n%w", bootPartition.Path, err) } defer bootPartitionMount.Close() grubCfgPath := filepath.Join(tmpDirBoot, bootDirPath, DefaultGrubCfgPath) kernelToArgs, err := extractKernelCmdlineFromGrubFile(grubCfgPath) if err != nil { - return nil, fmt.Errorf("failed to read grub.cfg:\n%w", err) + // Check if the error is because grub.cfg doesn't exist + if errors.Is(err, fs.ErrNotExist) { + closeErr := bootPartitionMount.CleanClose() + if closeErr != nil { + return nil, false, fmt.Errorf("failed to close bootPartitionMount after missing grub.cfg:\n%w", closeErr) + } + return nil, false, nil + } + // For other errors, attempt cleanup and return the error + closeErr := bootPartitionMount.CleanClose() + if closeErr != nil { + return nil, false, fmt.Errorf("failed to close bootPartitionMount (original error: %v):\n%w", err, closeErr) + } + return nil, false, fmt.Errorf("failed to read grub.cfg:\n%w", err) } err = bootPartitionMount.CleanClose() if err != nil { - return nil, fmt.Errorf("failed to close bootPartitionMount:\n%w", err) + return nil, false, fmt.Errorf("failed to close bootPartitionMount:\n%w", err) } for _, args := range kernelToArgs { // Pick the first set of the args. // (Hopefully they are all the same.) - return args, nil + return args, true, nil } - return nil, fmt.Errorf("no kernel args found in grub.cfg file") + return nil, false, fmt.Errorf("no kernel args found in grub.cfg file") } // Extracts the kernel args for each kernel from the grub.cfg file. diff --git a/toolkit/tools/pkg/imagecustomizerlib/testdata/artifacts-output-verity.yaml b/toolkit/tools/pkg/imagecustomizerlib/testdata/artifacts-output-verity.yaml index eb824f46f4..85af4ea96a 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/testdata/artifacts-output-verity.yaml +++ b/toolkit/tools/pkg/imagecustomizerlib/testdata/artifacts-output-verity.yaml @@ -70,7 +70,7 @@ os: resetType: hard-reset uki: - kernels: auto + mode: create packages: remove: diff --git a/toolkit/tools/pkg/imagecustomizerlib/testdata/artifacts-output.yaml b/toolkit/tools/pkg/imagecustomizerlib/testdata/artifacts-output.yaml index c7c8be28cd..548efa33c3 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/testdata/artifacts-output.yaml +++ b/toolkit/tools/pkg/imagecustomizerlib/testdata/artifacts-output.yaml @@ -48,7 +48,7 @@ os: resetType: hard-reset uki: - kernels: auto + mode: create packages: remove: diff --git a/toolkit/tools/pkg/imagecustomizerlib/testdata/hierarchical-config.yaml b/toolkit/tools/pkg/imagecustomizerlib/testdata/hierarchical-config.yaml index 4040e7cb23..c46bafddef 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/testdata/hierarchical-config.yaml +++ b/toolkit/tools/pkg/imagecustomizerlib/testdata/hierarchical-config.yaml @@ -59,7 +59,7 @@ os: resetType: hard-reset uki: - kernels: auto + mode: create packages: remove: diff --git a/toolkit/tools/pkg/imagecustomizerlib/testdata/verity-reinit-root-nop.yaml b/toolkit/tools/pkg/imagecustomizerlib/testdata/verity-reinit-root-nop.yaml index 8ff82da54d..8d591a5aea 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/testdata/verity-reinit-root-nop.yaml +++ b/toolkit/tools/pkg/imagecustomizerlib/testdata/verity-reinit-root-nop.yaml @@ -9,7 +9,7 @@ os: bootloader: resetType: hard-reset uki: - kernels: auto + mode: create additionalFiles: - content: | cat, dog, elephant, squirrel diff --git a/toolkit/tools/pkg/imagecustomizerlib/testdata/verity-reinit-usr-nop.yaml b/toolkit/tools/pkg/imagecustomizerlib/testdata/verity-reinit-usr-nop.yaml index 727c0bc111..dc5cf92fb4 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/testdata/verity-reinit-usr-nop.yaml +++ b/toolkit/tools/pkg/imagecustomizerlib/testdata/verity-reinit-usr-nop.yaml @@ -1,10 +1,14 @@ previewFeatures: - reinitialize-verity +- uki storage: reinitializeVerity: none os: + uki: + mode: passthrough + additionalFiles: - content: | cat, dog, elephant, squirrel diff --git a/toolkit/tools/pkg/imagecustomizerlib/testdata/verity-reinit-usr-uki.yaml b/toolkit/tools/pkg/imagecustomizerlib/testdata/verity-reinit-usr-uki.yaml new file mode 100644 index 0000000000..73870219b3 --- /dev/null +++ b/toolkit/tools/pkg/imagecustomizerlib/testdata/verity-reinit-usr-uki.yaml @@ -0,0 +1,23 @@ +previewFeatures: +- reinitialize-verity +- uki + +storage: + reinitializeVerity: all + +os: + kernelCommandLine: + extraCommandLine: + - rd.info + + bootloader: + resetType: hard-reset + + uki: + mode: create + + additionalFiles: + - content: | + Testing UKI re-customization + destination: /etc/uki-test.txt + diff --git a/toolkit/tools/pkg/imagecustomizerlib/testdata/verity-root-uki.yaml b/toolkit/tools/pkg/imagecustomizerlib/testdata/verity-root-uki.yaml index 55d594f92a..2443bd7f12 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/testdata/verity-root-uki.yaml +++ b/toolkit/tools/pkg/imagecustomizerlib/testdata/verity-root-uki.yaml @@ -59,7 +59,7 @@ os: - rd.info uki: - kernels: auto + mode: create packages: remove: diff --git a/toolkit/tools/pkg/imagecustomizerlib/testdata/verity-usr-uki-passthrough.yaml b/toolkit/tools/pkg/imagecustomizerlib/testdata/verity-usr-uki-passthrough.yaml new file mode 100644 index 0000000000..0732bd8159 --- /dev/null +++ b/toolkit/tools/pkg/imagecustomizerlib/testdata/verity-usr-uki-passthrough.yaml @@ -0,0 +1,10 @@ +previewFeatures: +- uki +- reinitialize-verity + +storage: + reinitializeVerity: none + +os: + uki: + mode: passthrough diff --git a/toolkit/tools/pkg/imagecustomizerlib/testdata/verity-usr-uki.yaml b/toolkit/tools/pkg/imagecustomizerlib/testdata/verity-usr-uki.yaml index 1adb8191b6..f3747874e8 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/testdata/verity-usr-uki.yaml +++ b/toolkit/tools/pkg/imagecustomizerlib/testdata/verity-usr-uki.yaml @@ -68,7 +68,7 @@ os: - rd.info uki: - kernels: auto + mode: create packages: remove: diff --git a/toolkit/tools/pkg/osmodifierlib/modifierutils.go b/toolkit/tools/pkg/osmodifierlib/modifierutils.go index 86ba38884e..98e506ed32 100644 --- a/toolkit/tools/pkg/osmodifierlib/modifierutils.go +++ b/toolkit/tools/pkg/osmodifierlib/modifierutils.go @@ -6,6 +6,7 @@ package osmodifierlib import ( "context" "fmt" + "os" "strings" "github.com/microsoft/azure-linux-image-tools/toolkit/tools/imagecustomizerapi" @@ -18,7 +19,14 @@ import ( func doModifications(ctx context.Context, baseConfigPath string, osConfig *osmodifierapi.OS) error { var dummyChroot safechroot.ChrootInterface = &safechroot.DummyChroot{} - err := imagecustomizerlib.AddOrUpdateUsers(ctx, osConfig.Users, baseConfigPath, dummyChroot) + // Create a temporary directory for operations that need a build directory + buildDir, err := os.MkdirTemp("", "osmodifier-*") + if err != nil { + return fmt.Errorf("failed to create temporary build directory: %w", err) + } + defer os.RemoveAll(buildDir) + + err = imagecustomizerlib.AddOrUpdateUsers(ctx, osConfig.Users, baseConfigPath, dummyChroot) if err != nil { return err } @@ -87,7 +95,7 @@ func doModifications(ctx context.Context, baseConfigPath string, osConfig *osmod } if osConfig.SELinux.Mode != "" { - err = updateSELinuxForGrubBasedBoot(osConfig.SELinux.Mode, bootCustomizer, dummyChroot) + err = updateSELinuxForGrubBasedBoot(buildDir, osConfig.SELinux.Mode, bootCustomizer, dummyChroot) if err != nil { return err } @@ -179,8 +187,8 @@ func updateSELinuxForUkiBoot(selinuxMode imagecustomizerapi.SELinuxMode, install return nil } -func updateSELinuxForGrubBasedBoot(selinuxMode imagecustomizerapi.SELinuxMode, bootCustomizer *imagecustomizerlib.BootCustomizer, installChroot safechroot.ChrootInterface) error { - currentSELinuxMode, err := bootCustomizer.GetSELinuxMode(installChroot) +func updateSELinuxForGrubBasedBoot(buildDir string, selinuxMode imagecustomizerapi.SELinuxMode, bootCustomizer *imagecustomizerlib.BootCustomizer, installChroot safechroot.ChrootInterface) error { + currentSELinuxMode, err := bootCustomizer.GetSELinuxMode(buildDir, installChroot) if err != nil { return fmt.Errorf("failed to get current SELinux mode: %w", err) }