From 601cdccb979640a6b2ffcba41cc698015b1dacde Mon Sep 17 00:00:00 2001 From: Noel Georgi Date: Thu, 23 Jan 2025 18:59:42 +0530 Subject: [PATCH] feat: extract kernel/initrd from uki for grub Extract Kernel, Initrd and Commandline from UKI for GRUB installs. Fixes: #10191 Signed-off-by: Noel Georgi --- .../v1alpha1/bootloader/grub/install.go | 51 ++++++++++++---- internal/pkg/uki/internal/pe/extract.go | 59 +++++++++++++++++++ internal/pkg/uki/uki.go | 5 ++ pkg/imager/imager.go | 4 +- pkg/imager/out.go | 20 +++++-- pkg/imager/profile/profile.go | 4 -- pkg/imager/utils/copy.go | 43 +++++++++++++- pkg/machinery/constants/constants.go | 2 +- pkg/machinery/imager/quirks/quirks.go | 13 ++++ 9 files changed, 176 insertions(+), 25 deletions(-) create mode 100644 internal/pkg/uki/internal/pe/extract.go diff --git a/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/install.go b/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/install.go index 3a1ccc866..6aec0833f 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/install.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/install.go @@ -6,6 +6,7 @@ package grub import ( "fmt" + "os" "path/filepath" "runtime" "strings" @@ -16,6 +17,7 @@ import ( "github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/bootloader/mount" "github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/bootloader/options" "github.com/siderolabs/talos/internal/pkg/partition" + "github.com/siderolabs/talos/internal/pkg/uki" "github.com/siderolabs/talos/pkg/imager/utils" "github.com/siderolabs/talos/pkg/machinery/constants" ) @@ -68,18 +70,43 @@ func (c *Config) install(opts options.InstallOptions) (*options.InstallResult, e return nil, err } - if err := utils.CopyFiles( - opts.Printf, - utils.SourceDestination( - opts.BootAssets.KernelPath, - filepath.Join(opts.MountPrefix, constants.BootMountPoint, string(c.Default), constants.KernelAsset), - ), - utils.SourceDestination( - opts.BootAssets.InitramfsPath, - filepath.Join(opts.MountPrefix, constants.BootMountPoint, string(c.Default), constants.InitramfsAsset), - ), - ); err != nil { - return nil, err + // if we have a kernel path, assume that the kernel and initramfs are available + if _, err := os.Stat(opts.BootAssets.KernelPath); err == nil { + if err := utils.CopyFiles( + opts.Printf, + utils.SourceDestination( + opts.BootAssets.KernelPath, + filepath.Join(opts.MountPrefix, constants.BootMountPoint, string(c.Default), constants.KernelAsset), + ), + utils.SourceDestination( + opts.BootAssets.InitramfsPath, + filepath.Join(opts.MountPrefix, constants.BootMountPoint, string(c.Default), constants.InitramfsAsset), + ), + ); err != nil { + return nil, err + } + } else { + // if the kernel path does not exist, assume that the kernel and initramfs are in the UKI + assetInfo, err := uki.Extract(opts.BootAssets.UKIPath) + if err != nil { + return nil, err + } + + defer assetInfo.Close() //nolint:errcheck + + if err := utils.CopyReader( + opts.Printf, + utils.ReaderDestination( + assetInfo.Kernel, + filepath.Join(opts.MountPrefix, constants.BootMountPoint, string(c.Default), constants.KernelAsset), + ), + utils.ReaderDestination( + assetInfo.Initrd, + filepath.Join(opts.MountPrefix, constants.BootMountPoint, string(c.Default), constants.InitramfsAsset), + ), + ); err != nil { + return nil, err + } } if err := c.Put(c.Default, opts.Cmdline, opts.Version); err != nil { diff --git a/internal/pkg/uki/internal/pe/extract.go b/internal/pkg/uki/internal/pe/extract.go new file mode 100644 index 000000000..ee85cdb16 --- /dev/null +++ b/internal/pkg/uki/internal/pe/extract.go @@ -0,0 +1,59 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package pe + +import ( + "debug/pe" + "fmt" + "io" +) + +// fileCloser is an interface that wraps the Close method. +type fileCloser interface { + Close() error +} + +// AssetInfo contains the kernel, initrd, and cmdline from a PE file. +type AssetInfo struct { + Kernel io.ReadSeeker + Initrd io.ReadSeeker + Cmdline io.ReadSeeker + fileCloser +} + +// Extract extracts the kernel, initrd, and cmdline from a PE file. +func Extract(ukiPath string) (assetInfo AssetInfo, err error) { + peFile, err := pe.Open(ukiPath) + if err != nil { + return assetInfo, fmt.Errorf("failed to open PE file: %w", err) + } + + assetInfo.fileCloser = peFile + + for _, section := range peFile.Sections { + switch section.Name { + case ".initrd": + assetInfo.Initrd = section.Open() + case ".cmdline": + assetInfo.Cmdline = section.Open() + case ".linux": + assetInfo.Kernel = section.Open() + } + } + + if assetInfo.Kernel == nil { + return assetInfo, fmt.Errorf("kernel not found in PE file") + } + + if assetInfo.Initrd == nil { + return assetInfo, fmt.Errorf("initrd not found in PE file") + } + + if assetInfo.Cmdline == nil { + return assetInfo, fmt.Errorf("cmdline not found in PE file") + } + + return assetInfo, nil +} diff --git a/internal/pkg/uki/uki.go b/internal/pkg/uki/uki.go index 1a6c29a2c..87c77bdf2 100644 --- a/internal/pkg/uki/uki.go +++ b/internal/pkg/uki/uki.go @@ -198,3 +198,8 @@ func (builder *Builder) BuildSigned(printf func(string, ...any)) error { // sign the UKI file return builder.peSigner.Sign(builder.unsignedUKIPath, builder.OutUKIPath) } + +// Extract extracts the kernel, initrd, and cmdline from the UKI file. +func Extract(ukiPath string) (asset pe.AssetInfo, err error) { + return pe.Extract(ukiPath) +} diff --git a/pkg/imager/imager.go b/pkg/imager/imager.go index c5c7e8be1..4bbbc88cf 100644 --- a/pkg/imager/imager.go +++ b/pkg/imager/imager.go @@ -111,8 +111,10 @@ func (i *Imager) Execute(ctx context.Context, outputPath string, report *reporte if !needBuildUKI { return "", fmt.Errorf("UKI output is not supported in this Talos version") } - case profile.OutKindISO, profile.OutKindImage, profile.OutKindInstaller: + case profile.OutKindISO, profile.OutKindImage: needBuildUKI = needBuildUKI && i.prof.SecureBootEnabled() + case profile.OutKindInstaller: + needBuildUKI = needBuildUKI || quirks.New(i.prof.Version).UseSDBootForUEFI() case profile.OutKindCmdline, profile.OutKindKernel, profile.OutKindInitramfs: needBuildUKI = false case profile.OutKindUnknown: diff --git a/pkg/imager/out.go b/pkg/imager/out.go index 5b837ec05..92485feb4 100644 --- a/pkg/imager/out.go +++ b/pkg/imager/out.go @@ -393,16 +393,24 @@ func (i *Imager) outInstaller(ctx context.Context, path string, report *reporter printf("generating artifacts layer") - if i.prof.SecureBootEnabled() { + ukiPath := strings.TrimLeft(fmt.Sprintf(constants.UKIAssetPath, i.prof.Arch), "/") + + quirks := quirks.New(i.prof.Version) + + if i.prof.SecureBootEnabled() && !quirks.UseSDBootForUEFI() { + ukiPath += ".signed" // support for older secureboot installers + } + + if quirks.UseSDBootForUEFI() { artifacts = append(artifacts, - filemap.File{ - ImagePath: strings.TrimLeft(fmt.Sprintf(constants.UKIAssetPath, i.prof.Arch), "/"), - SourcePath: i.ukiPath, - }, filemap.File{ ImagePath: strings.TrimLeft(fmt.Sprintf(constants.SDBootAssetPath, i.prof.Arch), "/"), SourcePath: i.sdBootPath, }, + filemap.File{ + ImagePath: ukiPath, + SourcePath: i.ukiPath, + }, ) } else { artifacts = append(artifacts, @@ -417,7 +425,7 @@ func (i *Imager) outInstaller(ctx context.Context, path string, report *reporter ) } - if !quirks.New(i.prof.Version).SupportsOverlay() { + if !quirks.SupportsOverlay() { for _, extraArtifact := range []struct { sourcePath string imagePath string diff --git a/pkg/imager/profile/profile.go b/pkg/imager/profile/profile.go index bb5ff9554..93a287cbe 100644 --- a/pkg/imager/profile/profile.go +++ b/pkg/imager/profile/profile.go @@ -108,10 +108,6 @@ func (p *Profile) Validate() error { return errors.New("disk size is required for image output") } case OutKindInstaller: - if !p.SecureBootEnabled() && len(p.Customization.ExtraKernelArgs) > 0 { - return fmt.Errorf("customization of kernel args is not supported for %s output in !secureboot mode", p.Output.Kind) - } - if len(p.Customization.MetaContents) > 0 { return fmt.Errorf("customization of meta partition is not supported for %s output", p.Output.Kind) } diff --git a/pkg/imager/utils/copy.go b/pkg/imager/utils/copy.go index 704df8e99..f96232501 100644 --- a/pkg/imager/utils/copy.go +++ b/pkg/imager/utils/copy.go @@ -18,7 +18,7 @@ type CopyInstruction = ordered.Pair[string, string] // SourceDestination returns a CopyInstruction that copies src to dest. func SourceDestination(src, dest string) CopyInstruction { - return ordered.MakePair[string, string](src, dest) + return ordered.MakePair(src, dest) } // CopyFiles copies files according to the given instructions. @@ -57,3 +57,44 @@ func CopyFiles(printf func(string, ...any), instructions ...CopyInstruction) err return nil } + +// CopyReaderInstruction describes a reader copy operation. +type CopyReaderInstruction struct { + Reader io.Reader + Dest string +} + +// ReaderDestination returns a CopyReaderInstruction that copies reader to dest. +func ReaderDestination(reader io.Reader, dest string) CopyReaderInstruction { + return CopyReaderInstruction{Reader: reader, Dest: dest} +} + +// CopyReader copies readers according to the given instructions. +func CopyReader(printf func(string, ...any), instructions ...CopyReaderInstruction) error { + for _, instruction := range instructions { + if err := func(instruction CopyReaderInstruction) error { + dest := instruction.Dest + + if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil { + return err + } + + printf("copying from io reader to %s", dest) + + to, err := os.OpenFile(dest, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o666) + if err != nil { + return err + } + //nolint:errcheck + defer to.Close() + + _, err = io.Copy(to, instruction.Reader) + + return err + }(instruction); err != nil { + return fmt.Errorf("error copying reader -> %s: %w", instruction.Dest, err) + } + } + + return nil +} diff --git a/pkg/machinery/constants/constants.go b/pkg/machinery/constants/constants.go index 3006c11a0..19caa49f3 100644 --- a/pkg/machinery/constants/constants.go +++ b/pkg/machinery/constants/constants.go @@ -630,7 +630,7 @@ const ( RootfsAsset = "rootfs.sqsh" // UKIAsset defines a well known name for our UKI filename. - UKIAsset = "vmlinuz.efi.signed" + UKIAsset = "vmlinuz.efi" // UKIAssetPath is the path to the UKI in the installer. UKIAssetPath = "/usr/install/%s/" + UKIAsset diff --git a/pkg/machinery/imager/quirks/quirks.go b/pkg/machinery/imager/quirks/quirks.go index b7153b7d9..fb70cf7dd 100644 --- a/pkg/machinery/imager/quirks/quirks.go +++ b/pkg/machinery/imager/quirks/quirks.go @@ -161,3 +161,16 @@ func (q Quirks) SupportsSELinux() bool { return q.v.GTE(minVersionSELinux) } + +// minVersionUseSDBootOnly is the version that supports only SDBoot for UEFI. +var minTalosVersionUseSDBootOnly = semver.MustParse("1.10.0") + +// UseSDBootForUEFI returns true if the Talos version supports only SDBoot for UEFI. +func (q Quirks) UseSDBootForUEFI() bool { + // if the version doesn't parse, we assume it's latest Talos + if q.v == nil { + return false + } + + return q.v.GTE(minTalosVersionUseSDBootOnly) +}