Skip to content

Commit

Permalink
feat: extract kernel/initrd from uki for grub
Browse files Browse the repository at this point in the history
Extract Kernel, Initrd and Commandline from UKI for GRUB installs.

Fixes: #10191

Signed-off-by: Noel Georgi <[email protected]>
  • Loading branch information
frezbo committed Jan 23, 2025
1 parent ff175b9 commit 601cdcc
Show file tree
Hide file tree
Showing 9 changed files with 176 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package grub

import (
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
Expand All @@ -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"
)
Expand Down Expand Up @@ -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 {
Expand Down
59 changes: 59 additions & 0 deletions internal/pkg/uki/internal/pe/extract.go
Original file line number Diff line number Diff line change
@@ -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
}
5 changes: 5 additions & 0 deletions internal/pkg/uki/uki.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
4 changes: 3 additions & 1 deletion pkg/imager/imager.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
20 changes: 14 additions & 6 deletions pkg/imager/out.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down
4 changes: 0 additions & 4 deletions pkg/imager/profile/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
43 changes: 42 additions & 1 deletion pkg/imager/utils/copy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
}
2 changes: 1 addition & 1 deletion pkg/machinery/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions pkg/machinery/imager/quirks/quirks.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

0 comments on commit 601cdcc

Please sign in to comment.