feat: support kexec from uki

Support kexec from UKI for non-secureboot by extracting kernel,
initramfs and cmdline from UKI

Fixes: #10189

Signed-off-by: Noel Georgi <git@frezbo.dev>
This commit is contained in:
Noel Georgi 2025-01-27 08:19:51 +05:30
parent 8da264946c
commit 42e1669845
No known key found for this signature in database
GPG Key ID: 21A9F444075C9E36
6 changed files with 215 additions and 86 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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
}
bootloaderInfo, err := bootloader.Probe(systemDisk.DevPath, options.ProbeOptions{})
if err != nil {
return fmt.Errorf("failed to probe system disk: %w", err)
}
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
},
)
return err
return bootloaderInfo.KexecLoad(r, systemDisk.DevPath)
}, "kexecPrepare"
}

View File

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