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 <git@frezbo.dev>
This commit is contained in:
Noel Georgi 2025-01-23 18:59:42 +05:30
parent ff175b9fbd
commit 601cdccb97
No known key found for this signature in database
GPG Key ID: 21A9F444075C9E36
9 changed files with 176 additions and 25 deletions

View File

@ -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 {

View File

@ -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
}

View File

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

View File

@ -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:

View File

@ -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

View File

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

View File

@ -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
}

View File

@ -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

View File

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