diff --git a/internal/app/machined/pkg/runtime/v1alpha1/bootloader/bootloader.go b/internal/app/machined/pkg/runtime/v1alpha1/bootloader/bootloader.go index 5130ea114..534695103 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/bootloader/bootloader.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/bootloader/bootloader.go @@ -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. diff --git a/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/grub.go b/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/grub.go index 8ff1e4f25..5c3d13bdb 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/grub.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/grub.go @@ -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{ diff --git a/internal/app/machined/pkg/runtime/v1alpha1/bootloader/kexec/kexec.go b/internal/app/machined/pkg/runtime/v1alpha1/bootloader/kexec/kexec.go new file mode 100644 index 000000000..43f02c273 --- /dev/null +++ b/internal/app/machined/pkg/runtime/v1alpha1/bootloader/kexec/kexec.go @@ -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 +} diff --git a/internal/app/machined/pkg/runtime/v1alpha1/bootloader/sdboot/sdboot.go b/internal/app/machined/pkg/runtime/v1alpha1/bootloader/sdboot/sdboot.go index 06aab6e28..febf0cd71 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/bootloader/sdboot/sdboot.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/bootloader/sdboot/sdboot.go @@ -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{ diff --git a/internal/app/machined/pkg/runtime/v1alpha1/v1alpha1_sequencer_tasks.go b/internal/app/machined/pkg/runtime/v1alpha1/v1alpha1_sequencer_tasks.go index b6fe93742..594250c7a 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/v1alpha1_sequencer_tasks.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/v1alpha1_sequencer_tasks.go @@ -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" } diff --git a/internal/pkg/uki/internal/pe/extract.go b/internal/pkg/uki/internal/pe/extract.go index 570df9bf9..bbc275f9e 100644 --- a/internal/pkg/uki/internal/pe/extract.go +++ b/internal/pkg/uki/internal/pe/extract.go @@ -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 }