Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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 27, 2025

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent 83d007c commit cfde4ac
Showing 5 changed files with 249 additions and 81 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 using the bootloader.
KexecLoad(r runtime.Runtime, disk string) error
}

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

"github.com/siderolabs/go-blockdevice/v2/blkid"

"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"
mountv2 "github.com/siderolabs/talos/internal/pkg/mount/v2"
"github.com/siderolabs/talos/internal/pkg/partition"
"github.com/siderolabs/talos/internal/pkg/zboot"
"github.com/siderolabs/talos/pkg/machinery/constants"
"github.com/siderolabs/talos/pkg/machinery/version"
)
@@ -44,6 +56,79 @@ func NewConfig() *Config {
}
}

// KexecLoad does a kexec using the bootloader.
func (c *Config) KexecLoad(r runtime.Runtime, disk string) error {
return mount.PartitionOp(
disk,
[]mount.Spec{
{
PartitionLabel: constants.BootPartitionLabel,
FilesystemType: partition.FilesystemTypeXFS,
MountTarget: constants.BootMountPoint,
},
},
func() error {
defaultEntry, ok := c.Entries[c.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 = kexec.Load(r, fd, int(initrd.Fd()), cmdline); err != nil {
return err
}

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

return nil
},
[]blkid.ProbeOption{},
[]mountv2.NewPointOption{
mountv2.WithReadonly(),
},
[]mountv2.OperationOption{
mountv2.WithSkipIfMounted(),
},
nil,
)
}

// 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,42 @@
// 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"
"log"

"golang.org/x/sys/unix"

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

// Load calls unix.KexecFileLoad with error handling and sets the machine state to kexec prepared.
func Load(r runtime.Runtime, kernelFD, initrdFD int, cmdline string) error {
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
}
105 changes: 105 additions & 0 deletions internal/app/machined/pkg/runtime/v1alpha1/bootloader/sdboot/sdboot.go
Original file line number Diff line number Diff line change
@@ -8,19 +8,26 @@ package sdboot
import (
"errors"
"fmt"
"io"
"log"
"os"
"path/filepath"
goruntime "runtime"
"strings"

"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/internal/pkg/zboot"
"github.com/siderolabs/talos/pkg/imager/utils"
"github.com/siderolabs/talos/pkg/machinery/constants"
)
@@ -160,6 +167,104 @@ func Probe(disk string, options options.ProbeOptions) (*Config, error) {
}, nil
}

// KexecLoad does a kexec using the bootloader.
//
//nolint:gocyclo
func (c *Config) KexecLoad(r runtime.Runtime, disk string) error {
return mount.PartitionOp(
disk,
[]mount.Spec{
{
PartitionLabel: constants.EFIPartitionLabel,
FilesystemType: partition.FilesystemTypeVFAT,
MountTarget: constants.EFIMountPoint,
},
},
func() error {
var kernelFd int

assetInfo, err := uki.Extract(filepath.Join(constants.EFIMountPoint, "EFI", "Linux", c.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

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

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

defer func() {
if fileCloser != nil {
fileCloser.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, kernelFd, 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
},
[]blkid.ProbeOption{},
[]mountv2.NewPointOption{
mountv2.WithReadonly(),
},
[]mountv2.OperationOption{
mountv2.WithSkipIfMounted(),
},
nil,
)
}

// 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"
}

0 comments on commit cfde4ac

Please sign in to comment.