Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 74 additions & 62 deletions boot.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"os"
"path/filepath"
"strings"
"syscall"
"unsafe"

"github.com/google/go-containerregistry/pkg/crane"
Expand Down Expand Up @@ -104,9 +103,17 @@ func createMemfdFromReader(name string, reader io.Reader) (*os.File, error) {
return file, nil
}

// kexec_segment structure for kexec_load syscall
type kexecSegment struct {
buf uintptr
bufsz uint64
mem uint64
memsz uint64
}

func kexecLoadFromUKI(ukiPath string, extraCmdline string) error {
// Use KexecFileLoad - it's simpler and more correct
log.Printf("using KexecFileLoad")
// Use old kexec_load API - it doesn't require signature verification
log.Printf("using kexec_load (legacy API without signature verification)")

// Extract assets from UKI
assets, err := ExtractUKI(ukiPath)
Expand All @@ -115,20 +122,19 @@ func kexecLoadFromUKI(ukiPath string, extraCmdline string) error {
}
defer assets.Close()

// Create memfd for kernel from reader
kernelFile, err := createMemfdFromReader("kernel", assets.Kernel)
// Read kernel into memory
kernelData, err := io.ReadAll(assets.Kernel)
if err != nil {
return fmt.Errorf("failed to create kernel memfd: %w", err)
return fmt.Errorf("failed to read kernel: %w", err)
}
defer kernelFile.Close()
log.Printf("kernel size: %d bytes", len(kernelData))

// Create memfd for initramfs from reader
initrdFile, err := createMemfdFromReader("initramfs", assets.Initrd)
// Read initramfs into memory
initrdData, err := io.ReadAll(assets.Initrd)
if err != nil {
return fmt.Errorf("failed to create initramfs memfd: %w", err)
return fmt.Errorf("failed to read initramfs: %w", err)
}
defer initrdFile.Close()
initrdFD := int(initrdFile.Fd())
log.Printf("initramfs size: %d bytes", len(initrdData))

// Read cmdline from UKI
ukiCmdlineBytes, err := io.ReadAll(assets.Cmdline)
Expand All @@ -146,56 +152,65 @@ func kexecLoadFromUKI(ukiPath string, extraCmdline string) error {
if extraCmdline != "" {
cmdlineParts = append(cmdlineParts, extraCmdline)
}
cmdline := strings.Join(cmdlineParts, " ")

log.Printf("cmdline: %s", cmdline)

// Call kexec_file_load via syscall
// SYS_KEXEC_FILE_LOAD = 320 on x86_64
// long kexec_file_load(int kernel_fd, int initrd_fd, unsigned long cmdline_len, const char *cmdline, unsigned long flags)
const SYS_KEXEC_FILE_LOAD = 320
// KEXEC_FILE_LOAD_UNSAFE = 0x00000001 - skip signature verification (if lockdown is not enabled)
// KEXEC_FILE_LOAD_NO_VERIFY_SIG = 0x00000002 - skip signature verification
const KEXEC_FILE_LOAD_UNSAFE = 0x00000001
const KEXEC_FILE_LOAD_NO_VERIFY_SIG = 0x00000002

cmdline := strings.Join(cmdlineParts, " ") + "\x00" // null terminator
cmdlineBytes := []byte(cmdline)
if len(cmdlineBytes) > 0 {
cmdlineBytes = append(cmdlineBytes, 0) // null terminator
}

var cmdlinePtr uintptr
if len(cmdlineBytes) > 0 {
cmdlinePtr = uintptr(unsafe.Pointer(&cmdlineBytes[0]))
log.Printf("cmdline: %s", strings.TrimRight(cmdline, "\x00"))

// Prepare kexec segments
// For x86_64, we use a simplified approach:
// - Load entire bzImage at 2MB boundary (safe for modern systems)
// - Load initrd at a high address (below 4GB boundary)
// - Use zero entry point to let kernel auto-detect

const KERNEL_LOAD_ADDR = 0x200000 // 2MB - safe alignment for modern kernels
const INITRD_LOAD_ADDR = 0x7f000000 // ~2GB - safe for initrd
const CMDLINE_LOAD_ADDR = 0x20000 // 128KB - traditional cmdline location

segments := []kexecSegment{
// Kernel image (entire bzImage)
{
buf: uintptr(unsafe.Pointer(&kernelData[0])),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

This will cause a panic if kernelData is an empty slice. You should add a check after reading the kernel data (e.g., after line 130) to ensure len(kernelData) > 0 and return an error if it's empty, as an empty kernel is invalid.

bufsz: uint64(len(kernelData)),
mem: KERNEL_LOAD_ADDR,
memsz: uint64(len(kernelData)),
},
// Initramfs
{
buf: uintptr(unsafe.Pointer(&initrdData[0])),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

This will cause a panic if initrdData is an empty slice. An empty initramfs is a valid scenario. The code should handle this by conditionally adding the initramfs segment only if len(initrdData) > 0. The current implementation needs to be refactored to build the segments slice dynamically to support this.

bufsz: uint64(len(initrdData)),
mem: INITRD_LOAD_ADDR,
memsz: uint64(len(initrdData)),
},
// Command line
{
buf: uintptr(unsafe.Pointer(&cmdlineBytes[0])),
bufsz: uint64(len(cmdlineBytes)),
mem: CMDLINE_LOAD_ADDR,
memsz: uint64(len(cmdlineBytes)),
},
}

// Try first without flags (requires signed kernel)
var flags uintptr = 0
// Call kexec_load via syscall
// SYS_KEXEC_LOAD = 246 on x86_64, 283 on ARM64
// long kexec_load(unsigned long entry, unsigned long nr_segments, struct kexec_segment *segments, unsigned long flags)
const SYS_KEXEC_LOAD = 246
const KEXEC_ARCH_X86_64 = 62 << 16 // KEXEC_ARCH_X86_64

// Entry point 0 means auto-detect from kernel header
var entry uintptr = 0
var flags uintptr = KEXEC_ARCH_X86_64

_, _, errno := unix.Syscall6(
SYS_KEXEC_FILE_LOAD,
uintptr(kernelFile.Fd()), // kernel_fd
uintptr(initrdFD), // initrd_fd (-1 if none)
uintptr(len(cmdlineBytes)), // cmdline_len
cmdlinePtr, // cmdline
flags, // flags
0, // unused
SYS_KEXEC_LOAD,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For better portability and readability, use the unix.SYS_KEXEC_LOAD constant from the golang.org/x/sys/unix package instead of a locally defined constant with a hardcoded value. This also allows you to remove the SYS_KEXEC_LOAD constant definition on line 197.

Suggested change
SYS_KEXEC_LOAD,
unix.SYS_KEXEC_LOAD,

entry, // entry (0 = auto-detect)
uintptr(len(segments)), // nr_segments
uintptr(unsafe.Pointer(&segments[0])), // segments
flags, // flags
0, // unused
0, // unused
)

// If we got EPERM and it's not due to sysctl, try with flag to skip signature verification
if errno == unix.EPERM {
log.Printf("kexec_file_load failed with EPERM, trying with KEXEC_FILE_LOAD_UNSAFE flag (may require lockdown=off)")
flags = KEXEC_FILE_LOAD_UNSAFE
_, _, errno = unix.Syscall6(
SYS_KEXEC_FILE_LOAD,
uintptr(kernelFile.Fd()), // kernel_fd
uintptr(initrdFD), // initrd_fd (-1 if none)
uintptr(len(cmdlineBytes)), // cmdline_len
cmdlinePtr, // cmdline
flags, // flags
0, // unused
)
}

if errno != 0 {
switch errno {
case unix.ENOSYS:
Expand All @@ -204,23 +219,20 @@ func kexecLoadFromUKI(ukiPath string, extraCmdline string) error {
// EPERM can mean several things:
// 1. sysctl is disabled
// 2. lockdown mode is enabled
// 3. kernel signature is required
lockdownData, _ := os.ReadFile("/sys/kernel/security/lockdown")
lockdown := strings.TrimSpace(string(lockdownData))
if strings.Contains(lockdown, "[confidentiality]") || strings.Contains(lockdown, "[integrity]") {
return fmt.Errorf("kexec blocked: kernel is in lockdown mode (%s). Solutions:\n 1. Boot with 'lockdown=none' kernel parameter\n 2. Use signed UKI kernel\n 3. Disable Secure Boot", lockdown)
return fmt.Errorf("kexec blocked: kernel is in lockdown mode (%s). Solutions:\n 1. Boot with 'lockdown=none' kernel parameter\n 2. Disable Secure Boot", lockdown)
}
sysctlData, _ := os.ReadFile("/proc/sys/kernel/kexec_load_disabled")
if strings.TrimSpace(string(sysctlData)) == "1" {
return fmt.Errorf("kexec is disabled via sysctl. Run: sudo sysctl -w kernel.kexec_load_disabled=0")
}
return fmt.Errorf("kexec blocked: permission denied. Possible causes:\n 1. Kernel requires signed image (try booting with 'lockdown=none')\n 2. Secure Boot is enabled\n 3. Check /proc/sys/kernel/kexec_load_disabled")
return fmt.Errorf("kexec blocked: permission denied. Check /proc/sys/kernel/kexec_load_disabled")
case unix.EBUSY:
return fmt.Errorf("kexec is busy (another kexec may be in progress)")
case syscall.Errno(129): // EKEYREJECTED = 129
return fmt.Errorf("kernel signature verification failed (unsigned kernel with lockdown enabled)")
case syscall.Errno(95): // ENOTSUP = 95
return fmt.Errorf("kexec_file_load not supported (old kernel or missing CONFIG_KEXEC_FILE)")
case unix.EINVAL:
return fmt.Errorf("invalid kexec parameters. Check dmesg for details (may need different load addresses or kernel format)")
default:
return fmt.Errorf("error loading kernel for kexec: %v (errno: %d). Check dmesg for details", errno, errno)
}
Expand Down