/* Kernel Manager - Download, verify, and manage kernels for Volt hybrid runtime. Provides kernel lifecycle operations: - Download kernels to /var/lib/volt/kernels/ - Verify SHA-256 checksums - List available (local) kernels - Default kernel selection (host kernel fallback) - Kernel config validation (namespaces, cgroups, Landlock) Copyright (c) Armored Gates LLC. All rights reserved. */ package kernel import ( "crypto/sha256" "encoding/hex" "fmt" "io" "net/http" "os" "os/exec" "path/filepath" "sort" "strings" "time" ) const ( // DefaultKernelDir is where kernels are stored on disk. DefaultKernelDir = "/var/lib/volt/kernels" // HostKernelPath is the default host kernel image location. HostKernelPath = "/boot/vmlinuz" // configGzPath is the compressed kernel config inside /proc. configGzPath = "/proc/config.gz" ) // KernelInfo describes a locally available kernel. type KernelInfo struct { Version string // e.g. "6.1.0-42-amd64" Path string // absolute path to vmlinuz Size int64 // bytes SHA256 string // hex-encoded checksum Source string // "host", "downloaded", "custom" AddedAt time.Time // when the kernel was registered IsDefault bool // whether this is the active default } // RequiredFeature is a kernel config option that must be present. type RequiredFeature struct { Config string // e.g. "CONFIG_NAMESPACES" Description string // human-readable explanation } // RequiredFeatures lists kernel config options needed for Volt hybrid mode. var RequiredFeatures = []RequiredFeature{ {Config: "CONFIG_NAMESPACES", Description: "Namespace support (PID, NET, MNT, UTS, IPC)"}, {Config: "CONFIG_PID_NS", Description: "PID namespace isolation"}, {Config: "CONFIG_NET_NS", Description: "Network namespace isolation"}, {Config: "CONFIG_USER_NS", Description: "User namespace isolation"}, {Config: "CONFIG_UTS_NS", Description: "UTS namespace isolation"}, {Config: "CONFIG_IPC_NS", Description: "IPC namespace isolation"}, {Config: "CONFIG_CGROUPS", Description: "Control groups support"}, {Config: "CONFIG_CGROUP_V2", Description: "Cgroups v2 unified hierarchy"}, {Config: "CONFIG_SECURITY_LANDLOCK", Description: "Landlock LSM filesystem sandboxing"}, {Config: "CONFIG_SECCOMP", Description: "Seccomp syscall filtering"}, {Config: "CONFIG_SECCOMP_FILTER", Description: "Seccomp BPF filter programs"}, } // Manager handles kernel downloads, verification, and selection. type Manager struct { kernelDir string } // NewManager creates a new kernel manager rooted at the given directory. // If kernelDir is empty, DefaultKernelDir is used. func NewManager(kernelDir string) *Manager { if kernelDir == "" { kernelDir = DefaultKernelDir } return &Manager{kernelDir: kernelDir} } // Init ensures the kernel directory exists. func (m *Manager) Init() error { return os.MkdirAll(m.kernelDir, 0755) } // KernelDir returns the base directory for kernel storage. func (m *Manager) KernelDir() string { return m.kernelDir } // ── Download & Verify ──────────────────────────────────────────────────────── // Download fetches a kernel image from url into the kernel directory under the // given version name. If expectedSHA256 is non-empty the download is verified // against it; a mismatch causes the file to be removed and an error returned. func (m *Manager) Download(version, url, expectedSHA256 string) (*KernelInfo, error) { if err := m.Init(); err != nil { return nil, fmt.Errorf("kernel dir init: %w", err) } destDir := filepath.Join(m.kernelDir, version) if err := os.MkdirAll(destDir, 0755); err != nil { return nil, fmt.Errorf("create version dir: %w", err) } destPath := filepath.Join(destDir, "vmlinuz") // Download to temp file first, then rename. tmpPath := destPath + ".tmp" out, err := os.Create(tmpPath) if err != nil { return nil, fmt.Errorf("create temp file: %w", err) } defer func() { out.Close() os.Remove(tmpPath) // clean up on any failure path }() client := &http.Client{Timeout: 10 * time.Minute} resp, err := client.Get(url) if err != nil { return nil, fmt.Errorf("download failed: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("download returned HTTP %d", resp.StatusCode) } hasher := sha256.New() writer := io.MultiWriter(out, hasher) if _, err := io.Copy(writer, resp.Body); err != nil { return nil, fmt.Errorf("download interrupted: %w", err) } if err := out.Close(); err != nil { return nil, fmt.Errorf("close temp file: %w", err) } checksum := hex.EncodeToString(hasher.Sum(nil)) if expectedSHA256 != "" && !strings.EqualFold(checksum, expectedSHA256) { os.Remove(tmpPath) return nil, fmt.Errorf("checksum mismatch: got %s, expected %s", checksum, expectedSHA256) } if err := os.Rename(tmpPath, destPath); err != nil { return nil, fmt.Errorf("rename to final path: %w", err) } // Write checksum sidecar. checksumPath := filepath.Join(destDir, "sha256") os.WriteFile(checksumPath, []byte(checksum+"\n"), 0644) fi, _ := os.Stat(destPath) return &KernelInfo{ Version: version, Path: destPath, Size: fi.Size(), SHA256: checksum, Source: "downloaded", AddedAt: time.Now(), }, nil } // VerifyChecksum checks that the kernel at path matches the expected SHA-256 // hex digest. Returns nil on match. func VerifyChecksum(path, expectedSHA256 string) error { f, err := os.Open(path) if err != nil { return fmt.Errorf("open kernel: %w", err) } defer f.Close() h := sha256.New() if _, err := io.Copy(h, f); err != nil { return fmt.Errorf("read kernel: %w", err) } got := hex.EncodeToString(h.Sum(nil)) if !strings.EqualFold(got, expectedSHA256) { return fmt.Errorf("checksum mismatch: got %s, expected %s", got, expectedSHA256) } return nil } // Checksum computes and returns the SHA-256 hex digest of the file at path. func Checksum(path string) (string, error) { f, err := os.Open(path) if err != nil { return "", fmt.Errorf("open: %w", err) } defer f.Close() h := sha256.New() if _, err := io.Copy(h, f); err != nil { return "", fmt.Errorf("read: %w", err) } return hex.EncodeToString(h.Sum(nil)), nil } // ── List ───────────────────────────────────────────────────────────────────── // List returns all locally available kernels sorted by version name. func (m *Manager) List() ([]KernelInfo, error) { entries, err := os.ReadDir(m.kernelDir) if err != nil { if os.IsNotExist(err) { return nil, nil } return nil, fmt.Errorf("read kernel dir: %w", err) } var kernels []KernelInfo for _, entry := range entries { if !entry.IsDir() { continue } version := entry.Name() vmlinuz := filepath.Join(m.kernelDir, version, "vmlinuz") fi, err := os.Stat(vmlinuz) if err != nil { continue // not a valid kernel directory } ki := KernelInfo{ Version: version, Path: vmlinuz, Size: fi.Size(), Source: "downloaded", } // Read checksum sidecar if present. if data, err := os.ReadFile(filepath.Join(m.kernelDir, version, "sha256")); err == nil { ki.SHA256 = strings.TrimSpace(string(data)) } kernels = append(kernels, ki) } sort.Slice(kernels, func(i, j int) bool { return kernels[i].Version < kernels[j].Version }) return kernels, nil } // ── Default Kernel Selection ───────────────────────────────────────────────── // DefaultKernel returns the best kernel to use: // 1. The host kernel at /boot/vmlinuz-$(uname -r). // 2. Generic /boot/vmlinuz fallback. // 3. The latest locally downloaded kernel. // // Returns the absolute path to the kernel image. func (m *Manager) DefaultKernel() (string, error) { // Prefer the host kernel matching the running version. uname := currentKernelVersion() hostPath := "/boot/vmlinuz-" + uname if fileExists(hostPath) { return hostPath, nil } // Generic fallback. if fileExists(HostKernelPath) { return HostKernelPath, nil } // Check locally downloaded kernels — pick the latest. kernels, err := m.List() if err == nil && len(kernels) > 0 { return kernels[len(kernels)-1].Path, nil } return "", fmt.Errorf("no kernel found (checked %s, %s, %s)", hostPath, HostKernelPath, m.kernelDir) } // ResolveKernel resolves a kernel reference to an absolute path. // If kernelRef is an absolute path and exists, it is returned directly. // Otherwise, it is treated as a version name under kernelDir. // If empty, DefaultKernel() is used. func (m *Manager) ResolveKernel(kernelRef string) (string, error) { if kernelRef == "" { return m.DefaultKernel() } // Absolute path — use directly. if filepath.IsAbs(kernelRef) { if !fileExists(kernelRef) { return "", fmt.Errorf("kernel not found: %s", kernelRef) } return kernelRef, nil } // Treat as version name. path := filepath.Join(m.kernelDir, kernelRef, "vmlinuz") if fileExists(path) { return path, nil } return "", fmt.Errorf("kernel version %q not found in %s", kernelRef, m.kernelDir) } // ── Kernel Config Validation ───────────────────────────────────────────────── // ValidationResult holds the outcome of a kernel config check. type ValidationResult struct { Feature RequiredFeature Present bool Value string // "y", "m", or empty } // ValidateHostKernel checks the running host kernel's config for required // features. It reads from /boot/config-$(uname -r) or /proc/config.gz. func ValidateHostKernel() ([]ValidationResult, error) { uname := currentKernelVersion() configPath := "/boot/config-" + uname configData, err := os.ReadFile(configPath) if err != nil { // Try /proc/config.gz via zcat configData, err = readProcConfigGz() if err != nil { return nil, fmt.Errorf("cannot read kernel config (tried %s and %s): %w", configPath, configGzPath, err) } } return validateConfig(string(configData)), nil } // ValidateConfigFile checks a kernel config file at the given path for // required features. func ValidateConfigFile(path string) ([]ValidationResult, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("read config file: %w", err) } return validateConfig(string(data)), nil } // validateConfig parses a kernel .config text and checks for required features. func validateConfig(configText string) []ValidationResult { configMap := make(map[string]string) for _, line := range strings.Split(configText, "\n") { line = strings.TrimSpace(line) if line == "" { continue } // Check for "# CONFIG_FOO is not set" pattern. if strings.HasPrefix(line, "# ") && strings.HasSuffix(line, " is not set") { key := strings.TrimPrefix(line, "# ") key = strings.TrimSuffix(key, " is not set") configMap[key] = "n" continue } if strings.HasPrefix(line, "#") { continue } parts := strings.SplitN(line, "=", 2) if len(parts) == 2 { configMap[parts[0]] = parts[1] } } var results []ValidationResult for _, feat := range RequiredFeatures { val := configMap[feat.Config] r := ValidationResult{Feature: feat} if val == "y" || val == "m" { r.Present = true r.Value = val } results = append(results, r) } return results } // AllFeaturesPresent returns true if every validation result is present. func AllFeaturesPresent(results []ValidationResult) bool { for _, r := range results { if !r.Present { return false } } return true } // MissingFeatures returns only the features that are not present. func MissingFeatures(results []ValidationResult) []ValidationResult { var missing []ValidationResult for _, r := range results { if !r.Present { missing = append(missing, r) } } return missing } // ── Helpers ────────────────────────────────────────────────────────────────── // currentKernelVersion returns the running kernel version string (uname -r). func currentKernelVersion() string { data, err := os.ReadFile("/proc/sys/kernel/osrelease") if err == nil { return strings.TrimSpace(string(data)) } // Fallback: shell out to uname. out, err := exec.Command("uname", "-r").Output() if err == nil { return strings.TrimSpace(string(out)) } return "unknown" } // readProcConfigGz reads kernel config from /proc/config.gz using zcat. func readProcConfigGz() ([]byte, error) { if !fileExists(configGzPath) { return nil, fmt.Errorf("%s not found (try: modprobe configs)", configGzPath) } return exec.Command("zcat", configGzPath).Output() } // fileExists returns true if the path exists and is not a directory. func fileExists(path string) bool { fi, err := os.Stat(path) if err != nil { return false } return !fi.IsDir() }