Skip to content

Commit

Permalink
feat: support kexec from uki
Browse files Browse the repository at this point in the history
Support kexec from UKI for non-secureboot by extracting kernel,
initramfs and cmdline from UKI

Fixes: #10189

Signed-off-by: Noel Georgi <[email protected]>
frezbo committed Jan 28, 2025

Verified

This commit was signed with the committer’s verified signature.
GeekyEggo Richard Herman
1 parent 8da2649 commit 42e1669
Showing 6 changed files with 215 additions and 86 deletions.
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@ package bootloader
import (
"os"

"github.com/siderolabs/talos/internal/app/machined/pkg/runtime"
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub"
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/bootloader/options"
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/bootloader/sdboot"
@@ -27,6 +28,9 @@ type Bootloader interface {
Revert(disk string) error
// RequiredPartitions returns the required partitions for the bootloader.
RequiredPartitions() []partition.Options

// KexecLoad does a kexec_file_load using the current entry of the bootloader.
KexecLoad(r runtime.Runtime, disk string) error
}

// Probe checks if any supported bootloaders are installed.
46 changes: 46 additions & 0 deletions internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/grub.go
Original file line number Diff line number Diff line change
@@ -8,8 +8,14 @@ package grub
import (
"errors"
"fmt"
"log"
"os"
"path/filepath"
"strings"

"github.com/siderolabs/talos/internal/app/machined/pkg/runtime"
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/bootloader/kexec"
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/bootloader/options"
"github.com/siderolabs/talos/internal/pkg/partition"
"github.com/siderolabs/talos/pkg/machinery/constants"
"github.com/siderolabs/talos/pkg/machinery/version"
@@ -44,6 +50,46 @@ func NewConfig() *Config {
}
}

// KexecLoad does a kexec using the bootloader config.
func (c *Config) KexecLoad(r runtime.Runtime, disk string) error {
_, err := ProbeWithCallback(disk, options.ProbeOptions{}, func(grubConf *Config) error {
defaultEntry, ok := grubConf.Entries[grubConf.Default]

if !ok {
return nil
}

kernelPath := filepath.Join(constants.BootMountPoint, defaultEntry.Linux)
initrdPath := filepath.Join(constants.BootMountPoint, defaultEntry.Initrd)

kernel, err := os.Open(kernelPath)
if err != nil {
return err
}

defer kernel.Close() //nolint:errcheck

initrd, err := os.Open(initrdPath)
if err != nil {
return err
}

defer initrd.Close() //nolint:errcheck

cmdline := strings.TrimSpace(defaultEntry.Cmdline)

if err = kexec.Load(r, kernel, int(initrd.Fd()), cmdline); err != nil {
return err
}

log.Printf("prepared kexec environment kernel=%q initrd=%q cmdline=%q", kernelPath, initrdPath, cmdline)

return nil
})

return err
}

// RequiredPartitions returns the list of partitions required by the bootloader.
func (c *Config) RequiredPartitions() []partition.Options {
return []partition.Options{
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// 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 kexec call unix.KexecFileLoad with error handling.
package kexec

import (
"errors"
"fmt"
"io"
"log"
"os"
goruntime "runtime"

"golang.org/x/sys/unix"

"github.com/siderolabs/talos/internal/app/machined/pkg/runtime"
"github.com/siderolabs/talos/internal/pkg/zboot"
)

// Load handles zboot for arm64 and calls unix.KexecFileLoad with error handling and sets the machine state to kexec prepared.
func Load(r runtime.Runtime, kernel *os.File, initrdFD int, cmdline string) error {
kernelFD := int(kernel.Fd())

// on arm64 we need to extract the kernel from the zboot image if it's compressed
if goruntime.GOARCH == "arm64" {
var (
fileCloser io.Closer
extractErr error
)

kernelFD, fileCloser, extractErr = zboot.Extract(kernel)
if extractErr != nil {
return fmt.Errorf("failed to extract kernel from zboot: %w", extractErr)
}

defer func() {
if fileCloser != nil {
fileCloser.Close() //nolint:errcheck
}
}()
}

if err := unix.KexecFileLoad(kernelFD, initrdFD, cmdline, 0); err != nil {
switch {
case errors.Is(err, unix.ENOSYS):
log.Printf("kexec support is disabled in the kernel")

return nil
case errors.Is(err, unix.EPERM):
log.Printf("kexec support is disabled via sysctl")

return nil
case errors.Is(err, unix.EBUSY):
log.Printf("kexec is busy")

return nil
default:
return fmt.Errorf("error loading kernel for kexec: %w", err)
}
}

r.State().Machine().KexecPrepared(true)

return nil
}
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@ package sdboot
import (
"errors"
"fmt"
"io"
"log"
"os"
"path/filepath"
@@ -16,11 +17,15 @@ import (
"github.com/ecks/uefi/efi/efivario"
"github.com/siderolabs/gen/xerrors"
"github.com/siderolabs/go-blockdevice/v2/blkid"
"golang.org/x/sys/unix"

"github.com/siderolabs/talos/internal/app/machined/pkg/runtime"
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/bootloader/kexec"
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/bootloader/mount"
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/bootloader/options"
mountv2 "github.com/siderolabs/talos/internal/pkg/mount/v2"
"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"
)
@@ -52,10 +57,10 @@ func New() *Config {
return &Config{}
}

// Probe for existing sd-boot bootloader.
// ProbeWithCallback probes the sd-boot bootloader, and calls the callback function with the Config.
//
//nolint:gocyclo
func Probe(disk string, options options.ProbeOptions) (*Config, error) {
func ProbeWithCallback(disk string, options options.ProbeOptions, callback func(*Config) error) (*Config, error) {
// if not UEFI boot, nothing to do
if !isUEFIBoot() {
return nil, nil
@@ -137,6 +142,12 @@ func Probe(disk string, options options.ProbeOptions) (*Config, error) {

for _, file := range files {
if strings.EqualFold(filepath.Base(file), bootedEntry) {
if callback != nil {
return callback(&Config{
Default: bootedEntry,
})
}

return nil
}
}
@@ -160,6 +171,75 @@ func Probe(disk string, options options.ProbeOptions) (*Config, error) {
}, nil
}

// Probe for existing sd-boot bootloader.
func Probe(disk string, options options.ProbeOptions) (*Config, error) {
return ProbeWithCallback(disk, options, nil)
}

// KexecLoad does a kexec using the bootloader config.
func (c *Config) KexecLoad(r runtime.Runtime, disk string) error {
_, err := ProbeWithCallback(disk, options.ProbeOptions{}, func(conf *Config) error {
var kernelFd int

assetInfo, err := uki.Extract(filepath.Join(constants.EFIMountPoint, "EFI", "Linux", conf.Default))
if err != nil {
return fmt.Errorf("failed to extract kernel and initrd from uki: %w", err)
}

defer assetInfo.Close() //nolint:errcheck

kernelFd, err = unix.MemfdCreate("vmlinux", 0)
if err != nil {
return fmt.Errorf("memfdCreate: %v", err)
}

kernelMemfd := os.NewFile(uintptr(kernelFd), "vmlinux")

defer kernelMemfd.Close() //nolint:errcheck

if _, err := io.Copy(kernelMemfd, assetInfo.Kernel); err != nil {
return fmt.Errorf("failed to read kernel from uki: %w", err)
}

if _, err = kernelMemfd.Seek(0, io.SeekStart); err != nil {
return fmt.Errorf("failed to seek kernel: %w", err)
}

initrdFd, err := unix.MemfdCreate("initrd", 0)
if err != nil {
return fmt.Errorf("memfdCreate: %v", err)
}

initrdMemfd := os.NewFile(uintptr(initrdFd), "initrd")

defer initrdMemfd.Close() //nolint:errcheck

if _, err := io.Copy(initrdMemfd, assetInfo.Initrd); err != nil {
return fmt.Errorf("failed to read initrd from uki: %w", err)
}

if _, err = initrdMemfd.Seek(0, io.SeekStart); err != nil {
return fmt.Errorf("failed to seek initrd: %w", err)
}

var cmdline strings.Builder

if _, err := io.Copy(&cmdline, assetInfo.Cmdline); err != nil {
return fmt.Errorf("failed to read cmdline from uki: %w", err)
}

if err := kexec.Load(r, kernelMemfd, initrdFd, cmdline.String()); err != nil {
return fmt.Errorf("failed to load kernel for kexec: %w", err)
}

log.Printf("prepared kexec environment with kernel and initrd extracted from uki, cmdline=%q", cmdline.String())

return nil
})

return err
}

// RequiredPartitions returns the list of partitions required by the bootloader.
func (c *Config) RequiredPartitions() []partition.Options {
return []partition.Options{
Original file line number Diff line number Diff line change
@@ -11,11 +11,9 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"log"
"os"
"path/filepath"
goruntime "runtime"
"slices"
"strconv"
"strings"
@@ -26,11 +24,11 @@ import (
"github.com/cosi-project/runtime/pkg/safe"
"github.com/cosi-project/runtime/pkg/state"
"github.com/dustin/go-humanize"
"github.com/foxboron/go-uefi/efi"
"github.com/hashicorp/go-multierror"
pprocfs "github.com/prometheus/procfs"
"github.com/siderolabs/gen/maps"
"github.com/siderolabs/gen/xslices"
"github.com/siderolabs/go-blockdevice/v2/blkid"
"github.com/siderolabs/go-blockdevice/v2/block"
"github.com/siderolabs/go-cmd/pkg/cmd"
"github.com/siderolabs/go-cmd/pkg/cmd/proc"
@@ -42,7 +40,7 @@ import (

"github.com/siderolabs/talos/internal/app/machined/pkg/runtime"
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime/emergency"
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub"
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/bootloader"
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/bootloader/options"
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/platform"
"github.com/siderolabs/talos/internal/app/machined/pkg/system"
@@ -59,7 +57,6 @@ import (
"github.com/siderolabs/talos/internal/pkg/secureboot"
"github.com/siderolabs/talos/internal/pkg/secureboot/tpm2"
"github.com/siderolabs/talos/internal/pkg/selinux"
"github.com/siderolabs/talos/internal/pkg/zboot"
"github.com/siderolabs/talos/pkg/conditions"
"github.com/siderolabs/talos/pkg/images"
"github.com/siderolabs/talos/pkg/kernel/kspp"
@@ -1865,8 +1862,6 @@ func Install(runtime.Sequence, any) (runtime.TaskExecutionFunc, string) {
}

// KexecPrepare loads next boot kernel via kexec_file_load.
//
//nolint:gocyclo
func KexecPrepare(_ runtime.Sequence, data any) (runtime.TaskExecutionFunc, string) {
return func(ctx context.Context, logger *log.Logger, r runtime.Runtime) error {
if req, ok := data.(*machineapi.RebootRequest); ok {
@@ -1877,6 +1872,12 @@ func KexecPrepare(_ runtime.Sequence, data any) (runtime.TaskExecutionFunc, stri
}
}

if efi.GetSecureBoot() {
log.Print("kexec skipped as secure boot is enabled")

return nil
}

systemDisk, err := blockres.GetSystemDisk(ctx, r.State().V1Alpha2().Resources())
if err != nil {
return err
@@ -1901,81 +1902,12 @@ func KexecPrepare(_ runtime.Sequence, data any) (runtime.TaskExecutionFunc, stri

defer dev.Unlock() //nolint:errcheck

_, err = grub.ProbeWithCallback(systemDisk.DevPath,
options.ProbeOptions{
BlockProbeOptions: []blkid.ProbeOption{blkid.WithSkipLocking(true)},
},
func(conf *grub.Config) error {
defaultEntry, ok := conf.Entries[conf.Default]
if !ok {
return nil
}

kernelPath := filepath.Join(constants.BootMountPoint, defaultEntry.Linux)
initrdPath := filepath.Join(constants.BootMountPoint, defaultEntry.Initrd)

kernel, err := os.Open(kernelPath)
if err != nil {
return err
}

defer kernel.Close() //nolint:errcheck

fd := int(kernel.Fd())

// on arm64 we need to extract the kernel from the zboot image if it's compressed
if goruntime.GOARCH == "arm64" {
var fileCloser io.Closer

fd, fileCloser, err = zboot.Extract(kernel)
if err != nil {
return err
}

defer func() {
if fileCloser != nil {
fileCloser.Close() //nolint:errcheck
}
}()
}

initrd, err := os.Open(initrdPath)
if err != nil {
return err
}

defer initrd.Close() //nolint:errcheck

cmdline := strings.TrimSpace(defaultEntry.Cmdline)

if err = unix.KexecFileLoad(fd, int(initrd.Fd()), cmdline, 0); err != nil {
switch {
case errors.Is(err, unix.ENOSYS):
log.Printf("kexec support is disabled in the kernel")

return nil
case errors.Is(err, unix.EPERM):
log.Printf("kexec support is disabled via sysctl")

return nil
case errors.Is(err, unix.EBUSY):
log.Printf("kexec is busy")

return nil
default:
return fmt.Errorf("error loading kernel for kexec: %w", err)
}
}

log.Printf("prepared kexec environment kernel=%q initrd=%q cmdline=%q", kernelPath, initrdPath, cmdline)

r.State().Machine().KexecPrepared(true)

return nil
},
)
bootloaderInfo, err := bootloader.Probe(systemDisk.DevPath, options.ProbeOptions{})
if err != nil {
return fmt.Errorf("failed to probe system disk: %w", err)
}

return err
return bootloaderInfo.KexecLoad(r, systemDisk.DevPath)
}, "kexecPrepare"
}

6 changes: 3 additions & 3 deletions internal/pkg/uki/internal/pe/extract.go
Original file line number Diff line number Diff line change
@@ -17,9 +17,9 @@ type fileCloser interface {

// AssetInfo contains the kernel, initrd, and cmdline from a PE file.
type AssetInfo struct {
Kernel io.ReadSeeker
Initrd io.ReadSeeker
Cmdline io.ReadSeeker
Kernel io.Reader
Initrd io.Reader
Cmdline io.Reader
fileCloser
}

0 comments on commit 42e1669

Please sign in to comment.