mirror of
https://github.com/siderolabs/talos.git
synced 2026-05-05 12:26:21 +02:00
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:
parent
8da264946c
commit
42e1669845
@ -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.
|
||||
|
||||
@ -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{
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -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{
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user