Skip to content

Commit 46eb20b

Browse files
committed
Introduce boot/install mode
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
1 parent f226880 commit 46eb20b

4 files changed

Lines changed: 580 additions & 148 deletions

File tree

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,12 @@ curl -sSL https://github.com/cozystack/boot-to-talos/raw/refs/heads/main/hack/in
2222

2323
```console
2424
$ boot-to-talos
25-
Target disk [/dev/sda]:
25+
Mode:
26+
1. boot – extract the kernel and initrd from the Talos installer and boot them directly using the kexec mechanism.
27+
2. install – prepare the environment, run the Talos installer, and then overwrite the system disk with the installed image.
28+
Mode [1]: 2
2629
Talos installer image [ghcr.io/cozystack/cozystack/talos:v1.10.5]:
30+
Target disk [/dev/sda]:
2731
Add networking configuration? [yes]:
2832
Interface [eth0]:
2933
IP address [10.0.2.15]:

boot.go

Lines changed: 347 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,347 @@
1+
package main
2+
3+
import (
4+
"archive/tar"
5+
"debug/pe"
6+
"fmt"
7+
"io"
8+
"log"
9+
"os"
10+
"path/filepath"
11+
"strings"
12+
"syscall"
13+
"unsafe"
14+
15+
"github.com/google/go-containerregistry/pkg/crane"
16+
"golang.org/x/sys/unix"
17+
)
18+
19+
/* -------------------- UKI extraction helpers ------------------------------- */
20+
21+
// UKIAssetInfo contains kernel, initrd and cmdline from UKI file
22+
type UKIAssetInfo struct {
23+
io.Closer
24+
Kernel io.Reader
25+
Initrd io.Reader
26+
Cmdline io.Reader
27+
}
28+
29+
// ExtractUKI extracts kernel, initrd and cmdline from UKI file
30+
func ExtractUKI(ukiPath string) (*UKIAssetInfo, error) {
31+
peFile, err := pe.Open(ukiPath)
32+
if err != nil {
33+
return nil, fmt.Errorf("failed to open PE file: %w", err)
34+
}
35+
36+
assetInfo := &UKIAssetInfo{
37+
Closer: peFile,
38+
}
39+
40+
sectionMap := map[string]*io.Reader{
41+
".initrd": &assetInfo.Initrd,
42+
".cmdline": &assetInfo.Cmdline,
43+
".linux": &assetInfo.Kernel,
44+
}
45+
46+
for _, section := range peFile.Sections {
47+
// Remove null bytes from section name
48+
sectionName := ""
49+
for _, b := range section.Name {
50+
if b != 0 {
51+
sectionName += string(b)
52+
}
53+
}
54+
55+
if reader, exists := sectionMap[sectionName]; exists && *reader == nil {
56+
// Use VirtualSize instead of Size to exclude alignment
57+
*reader = io.LimitReader(section.Open(), int64(section.VirtualSize))
58+
}
59+
}
60+
61+
// Check that all required sections are found
62+
for name, reader := range sectionMap {
63+
if *reader == nil {
64+
peFile.Close()
65+
return nil, fmt.Errorf("%s not found in PE file", name)
66+
}
67+
}
68+
69+
return assetInfo, nil
70+
}
71+
72+
/* -------------------- kexec helpers ----------------------------------------- */
73+
74+
// createMemfdFromReader creates an anonymous file in memory via memfd_create and copies data from reader
75+
func createMemfdFromReader(name string, reader io.Reader) (*os.File, error) {
76+
// SYS_MEMFD_CREATE = 319 on x86_64
77+
// int memfd_create(const char *name, unsigned int flags);
78+
const SYS_MEMFD_CREATE = 319
79+
const MFD_CLOEXEC = 0x0001
80+
81+
nameBytes := []byte(name + "\x00")
82+
fd, _, errno := unix.Syscall(SYS_MEMFD_CREATE, uintptr(unsafe.Pointer(&nameBytes[0])), MFD_CLOEXEC, 0)
83+
if errno != 0 {
84+
return nil, fmt.Errorf("memfd_create failed: %v", errno)
85+
}
86+
87+
file := os.NewFile(fd, name)
88+
if file == nil {
89+
return nil, fmt.Errorf("failed to create file from fd")
90+
}
91+
92+
// Copy data from reader to memfd
93+
if _, err := io.Copy(file, reader); err != nil {
94+
file.Close()
95+
return nil, fmt.Errorf("failed to copy to memfd: %w", err)
96+
}
97+
98+
// Reset position to beginning
99+
if _, err := file.Seek(0, 0); err != nil {
100+
file.Close()
101+
return nil, fmt.Errorf("failed to seek memfd: %w", err)
102+
}
103+
104+
return file, nil
105+
}
106+
107+
func kexecLoadFromUKI(ukiPath string, extraCmdline string) error {
108+
// Use KexecFileLoad - it's simpler and more correct
109+
log.Printf("using KexecFileLoad")
110+
111+
// Extract assets from UKI
112+
assets, err := ExtractUKI(ukiPath)
113+
if err != nil {
114+
return fmt.Errorf("failed to extract UKI: %w", err)
115+
}
116+
defer assets.Close()
117+
118+
// Create memfd for kernel from reader
119+
kernelFile, err := createMemfdFromReader("kernel", assets.Kernel)
120+
if err != nil {
121+
return fmt.Errorf("failed to create kernel memfd: %w", err)
122+
}
123+
defer kernelFile.Close()
124+
125+
// Create memfd for initramfs from reader
126+
initrdFile, err := createMemfdFromReader("initramfs", assets.Initrd)
127+
if err != nil {
128+
return fmt.Errorf("failed to create initramfs memfd: %w", err)
129+
}
130+
defer initrdFile.Close()
131+
initrdFD := int(initrdFile.Fd())
132+
133+
// Read cmdline from UKI
134+
ukiCmdlineBytes, err := io.ReadAll(assets.Cmdline)
135+
if err != nil {
136+
return fmt.Errorf("failed to read cmdline from UKI: %w", err)
137+
}
138+
ukiCmdline := strings.TrimRight(string(ukiCmdlineBytes), "\x00")
139+
ukiCmdline = strings.TrimSpace(ukiCmdline)
140+
141+
// Combine cmdline from UKI with additional arguments
142+
cmdlineParts := []string{}
143+
if ukiCmdline != "" {
144+
cmdlineParts = append(cmdlineParts, ukiCmdline)
145+
}
146+
if extraCmdline != "" {
147+
cmdlineParts = append(cmdlineParts, extraCmdline)
148+
}
149+
cmdline := strings.Join(cmdlineParts, " ")
150+
151+
log.Printf("cmdline: %s", cmdline)
152+
153+
// Call kexec_file_load via syscall
154+
// SYS_KEXEC_FILE_LOAD = 320 on x86_64
155+
// long kexec_file_load(int kernel_fd, int initrd_fd, unsigned long cmdline_len, const char *cmdline, unsigned long flags)
156+
const SYS_KEXEC_FILE_LOAD = 320
157+
// KEXEC_FILE_LOAD_UNSAFE = 0x00000001 - skip signature verification (if lockdown is not enabled)
158+
// KEXEC_FILE_LOAD_NO_VERIFY_SIG = 0x00000002 - skip signature verification
159+
const KEXEC_FILE_LOAD_UNSAFE = 0x00000001
160+
const KEXEC_FILE_LOAD_NO_VERIFY_SIG = 0x00000002
161+
162+
cmdlineBytes := []byte(cmdline)
163+
if len(cmdlineBytes) > 0 {
164+
cmdlineBytes = append(cmdlineBytes, 0) // null terminator
165+
}
166+
167+
var cmdlinePtr uintptr
168+
if len(cmdlineBytes) > 0 {
169+
cmdlinePtr = uintptr(unsafe.Pointer(&cmdlineBytes[0]))
170+
}
171+
172+
// Try first without flags (requires signed kernel)
173+
var flags uintptr = 0
174+
_, _, errno := unix.Syscall6(
175+
SYS_KEXEC_FILE_LOAD,
176+
uintptr(kernelFile.Fd()), // kernel_fd
177+
uintptr(initrdFD), // initrd_fd (-1 if none)
178+
uintptr(len(cmdlineBytes)), // cmdline_len
179+
cmdlinePtr, // cmdline
180+
flags, // flags
181+
0, // unused
182+
)
183+
184+
// If we got EPERM and it's not due to sysctl, try with flag to skip signature verification
185+
if errno == unix.EPERM {
186+
log.Printf("kexec_file_load failed with EPERM, trying with KEXEC_FILE_LOAD_UNSAFE flag (may require lockdown=off)")
187+
flags = KEXEC_FILE_LOAD_UNSAFE
188+
_, _, errno = unix.Syscall6(
189+
SYS_KEXEC_FILE_LOAD,
190+
uintptr(kernelFile.Fd()), // kernel_fd
191+
uintptr(initrdFD), // initrd_fd (-1 if none)
192+
uintptr(len(cmdlineBytes)), // cmdline_len
193+
cmdlinePtr, // cmdline
194+
flags, // flags
195+
0, // unused
196+
)
197+
}
198+
199+
if errno != 0 {
200+
switch errno {
201+
case unix.ENOSYS:
202+
return fmt.Errorf("kexec support is disabled in the kernel (CONFIG_KEXEC not enabled)")
203+
case unix.EPERM:
204+
// EPERM can mean several things:
205+
// 1. sysctl is disabled
206+
// 2. lockdown mode is enabled
207+
// 3. kernel signature is required
208+
lockdownData, _ := os.ReadFile("/sys/kernel/security/lockdown")
209+
lockdown := strings.TrimSpace(string(lockdownData))
210+
if strings.Contains(lockdown, "[confidentiality]") || strings.Contains(lockdown, "[integrity]") {
211+
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)
212+
}
213+
sysctlData, _ := os.ReadFile("/proc/sys/kernel/kexec_load_disabled")
214+
if strings.TrimSpace(string(sysctlData)) == "1" {
215+
return fmt.Errorf("kexec is disabled via sysctl. Run: sudo sysctl -w kernel.kexec_load_disabled=0")
216+
}
217+
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")
218+
case unix.EBUSY:
219+
return fmt.Errorf("kexec is busy (another kexec may be in progress)")
220+
case syscall.Errno(129): // EKEYREJECTED = 129
221+
return fmt.Errorf("kernel signature verification failed (unsigned kernel with lockdown enabled)")
222+
case syscall.Errno(95): // ENOTSUP = 95
223+
return fmt.Errorf("kexec_file_load not supported (old kernel or missing CONFIG_KEXEC_FILE)")
224+
default:
225+
return fmt.Errorf("error loading kernel for kexec: %v (errno: %d). Check dmesg for details", errno, errno)
226+
}
227+
}
228+
229+
log.Printf("kexec loaded successfully, rebooting...")
230+
231+
// Call reboot with LINUX_REBOOT_CMD_KEXEC
232+
const LINUX_REBOOT_CMD_KEXEC = 0x45584543
233+
const LINUX_REBOOT_MAGIC1 = 0xfee1dead
234+
const LINUX_REBOOT_MAGIC2 = 672274793
235+
const SYS_REBOOT = 169
236+
_, _, errno2 := unix.Syscall6(
237+
SYS_REBOOT,
238+
LINUX_REBOOT_MAGIC1, // magic1
239+
LINUX_REBOOT_MAGIC2, // magic2
240+
LINUX_REBOOT_CMD_KEXEC, // cmd
241+
0, // arg (unused)
242+
0, // unused
243+
0, // unused
244+
)
245+
if errno2 != 0 {
246+
return fmt.Errorf("reboot with kexec failed: %v", errno2)
247+
}
248+
249+
// Code should not reach here, as reboot restarts the system
250+
return nil
251+
}
252+
253+
/* -------------------- boot mode ------------------------------------------- */
254+
255+
func runBootMode(image string, extra multiFlag) {
256+
// First show summary and ask for confirmation
257+
// (without loading the image)
258+
fmt.Println("\nBoot Summary:")
259+
fmt.Printf(" Image: %s\n", image)
260+
fmt.Printf(" Extra kernel args: %s\n",
261+
func() string {
262+
if len(extra) == 0 {
263+
return "(none)"
264+
}
265+
return strings.Join(extra, " ")
266+
}())
267+
fmt.Println()
268+
269+
if !askYesNo("Continue with boot?", true) {
270+
log.Fatal("aborted by user")
271+
}
272+
fmt.Println()
273+
274+
// Only after confirmation start loading the image
275+
log.Printf("boot mode: extracting kernel and initramfs from image")
276+
277+
tmpDir, _ := os.MkdirTemp("", "boot-*")
278+
log.Printf("created temporary directory %s", tmpDir)
279+
defer os.RemoveAll(tmpDir)
280+
281+
transport := setupTransportWithProxy()
282+
opts := crane.WithTransport(transport)
283+
284+
log.Printf("pulling image %s", image)
285+
img, err := crane.Pull(image, opts)
286+
must("pull image", err)
287+
288+
log.Print("extracting image layers")
289+
instDir := filepath.Join(tmpDir, "installer")
290+
os.MkdirAll(instDir, 0o755)
291+
292+
var ukiPath string
293+
layers, _ := img.Layers()
294+
for _, l := range layers {
295+
r, _ := l.Uncompressed()
296+
defer r.Close()
297+
tr := tar.NewReader(r)
298+
for {
299+
h, err := tr.Next()
300+
if err == io.EOF {
301+
break
302+
}
303+
must("tar", err)
304+
if strings.HasPrefix(filepath.Base(h.Name), ".wh.") {
305+
continue
306+
}
307+
target := filepath.Join(instDir, h.Name)
308+
name := strings.ToLower(h.Name)
309+
// Look for UKI kernel
310+
if strings.Contains(name, "install") && strings.Contains(name, "vmlinuz.efi") {
311+
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
312+
log.Fatalf("failed to create directory for UKI kernel: %v (check available disk space)", err)
313+
}
314+
f, err := os.Create(target)
315+
if err != nil {
316+
log.Fatalf("failed to create UKI kernel file: %v (check available disk space)", err)
317+
}
318+
if _, err := io.Copy(f, tr); err != nil {
319+
f.Close()
320+
os.Remove(target)
321+
log.Fatalf("failed to extract UKI kernel: %v (check available disk space)", err)
322+
}
323+
if err := f.Close(); err != nil {
324+
os.Remove(target)
325+
log.Fatalf("failed to close UKI kernel file: %v", err)
326+
}
327+
if err := os.Chmod(target, os.FileMode(h.Mode)); err != nil {
328+
log.Fatalf("failed to set permissions on UKI kernel file: %v", err)
329+
}
330+
ukiPath = target
331+
}
332+
}
333+
}
334+
335+
if ukiPath == "" {
336+
log.Fatal("UKI kernel (vmlinuz.efi) not found in image")
337+
}
338+
339+
log.Printf("found UKI kernel: %s", ukiPath)
340+
341+
// Collect additional kernel arguments into a string
342+
extraCmdline := strings.Join(extra, " ")
343+
344+
log.Print("loading kernel with kexec from UKI")
345+
must("kexec", kexecLoadFromUKI(ukiPath, extraCmdline))
346+
}
347+

0 commit comments

Comments
 (0)