// Firecracker runtime adapter for Sorcery-Go Sanctums. // // Firecracker is an AWS open-source microVM that provides strong VM-level // isolation with sub-millisecond boot times and a minimal attack surface. // Each Sanctum runs in its own Firecracker microVM with dedicated kernel // and rootfs — providing hardware-level isolation that containers cannot match. // // This adapter communicates with the Firecracker VMM process via its // Unix socket API (the "Machine Controller"). Each microVM is managed as: // 1. Allocate a VMM Unix socket // 2. Configure boot source (kernel + initrd or rootfs) // 3. Configure root drive // 4. Configure network interface (tap device) // 5. Start the instance // // Note: Firecracker does NOT use cgroups for process management, so the // eBPF cgroup filter attachment is not applicable. The Tomb Guard LSM // hook still protects the host's Tomb from any process, including the // Firecracker VMM process itself. Network isolation is handled by the // Firecracker jailer's chroot + seccomp + network namespace. package runtime import ( "bytes" "context" "encoding/json" "fmt" "io" "log" "net" "net/http" "os" "os/exec" "path/filepath" "strconv" "strings" "sync" "time" ) // DefaultFirecrackerKernelPath is the fallback kernel image path when // no per-VM KernelPath is specified via CreateOpts. const DefaultFirecrackerKernelPath = "/var/lib/sorcery-go/vmlinux" // DefaultFirecrackerBootArgs are the base kernel command-line arguments. // IP configuration is appended when the VM has a network interface. const DefaultFirecrackerBootArgs = "console=ttyS0 reboot=k panic=1 pci=off" // FirecrackerRuntime manages Firecracker microVMs. type FirecrackerRuntime struct { mu sync.Mutex binPath string // path to firecracker binary jailerPath string // path to jailer binary socketDir string // directory for VMM sockets vms map[string]*fcVM // active VMs by sanctum ID } // fcVM tracks the state of a running Firecracker microVM. type fcVM struct { ID string SocketPath string PID int RootFSPath string KernelPath string DrivePath string TapDevice string IP string Started time.Time } // Firecracker API request/response types. type fcBootSource struct { KernelImage string `json:"kernel_image_path"` BootArgs string `json:"boot_args,omitempty"` InitrdPath string `json:"initrd_path,omitempty"` } type fcDrive struct { DriveID string `json:"drive_id"` PathOnHost string `json:"path_on_host"` IsRootDevice bool `json:"is_root_device"` IsReadOnly bool `json:"is_read_only"` Partuuid string `json:"partuuid,omitempty"` } type fcInterface struct { IfaceID string `json:"iface_id"` GuestMac string `json:"guest_mac"` HostDevName string `json:"host_dev_name"` } type fcInstanceAction struct { ActionType string `json:"action_type"` } // NewFirecrackerRuntime creates a Firecracker runtime adapter. func NewFirecrackerRuntime() *FirecrackerRuntime { return &FirecrackerRuntime{ binPath: "firecracker", socketDir: "/run/sorcery-go/firecracker", vms: make(map[string]*fcVM), } } func (r *FirecrackerRuntime) Type() Type { return RuntimeFirecracker } func (r *FirecrackerRuntime) Name() string { return "Firecracker microVMs" } // Probe checks that firecracker is available. func (r *FirecrackerRuntime) Probe() error { if _, err := exec.LookPath("firecracker"); err != nil { return fmt.Errorf("firecracker: binary not found in PATH: %w", err) } // Jailer is optional but recommended. if _, err := exec.LookPath("jailer"); err == nil { r.jailerPath = "jailer" } return nil } // Create provisions a new Firecracker microVM. // This sets up the socket and configuration but does not start the VM. func (r *FirecrackerRuntime) Create(ctx context.Context, opts CreateOpts) (string, error) { r.mu.Lock() defer r.mu.Unlock() if err := os.MkdirAll(r.socketDir, 0755); err != nil { return "", fmt.Errorf("firecracker: mkdir %s: %w", r.socketDir, err) } socketPath := filepath.Join(r.socketDir, opts.Name+".sock") vm := &fcVM{ ID: opts.Name, SocketPath: socketPath, KernelPath: opts.KernelPath, DrivePath: opts.RootDrivePath, RootFSPath: opts.RootFS, Started: time.Now(), } // Generate a MAC address for the VM. if opts.NetworkConfig != nil && opts.NetworkConfig.Type != "none" { vm.TapDevice = "tap-" + opts.Name vm.IP = opts.NetworkConfig.IP } r.vms[opts.Name] = vm return opts.Name, nil } // Start boots a Firecracker microVM. func (r *FirecrackerRuntime) Start(ctx context.Context, sanctumID string) error { r.mu.Lock() vm, ok := r.vms[sanctumID] r.mu.Unlock() if !ok { return fmt.Errorf("firecracker: unknown VM %s", sanctumID) } // Clean up old socket if present. if err := os.Remove(vm.SocketPath); err != nil { log.Printf("firecracker: remove old socket %s: %v\n", vm.SocketPath, err) } // Start the Firecracker VMM process. args := []string{"--api-sock", vm.SocketPath} cmd := exec.CommandContext(ctx, r.binPath, args...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Start(); err != nil { return fmt.Errorf("firecracker: start VMM %s: %w", sanctumID, err) } vm.PID = cmd.Process.Pid // Wait for the socket to appear (channel-based, no busy-wait). if err := waitForFile(vm.SocketPath, 5*time.Second); err != nil { return fmt.Errorf("firecracker: VMM socket not ready: %w", err) } client := newFCClient(vm.SocketPath) // Set boot source. if vm.KernelPath == "" { vm.KernelPath = DefaultFirecrackerKernelPath } bootArgs := DefaultFirecrackerBootArgs if vm.IP != "" { bootArgs += " ip=" + vm.IP } if err := client.putBootSource(fcBootSource{ KernelImage: vm.KernelPath, BootArgs: bootArgs, }); err != nil { return fmt.Errorf("firecracker: configure boot source: %w", err) } // Set root drive. if vm.DrivePath != "" { if err := client.putDrive(fcDrive{ DriveID: "rootfs", PathOnHost: vm.DrivePath, IsRootDevice: true, IsReadOnly: false, }); err != nil { return fmt.Errorf("firecracker: configure root drive: %w", err) } } // Configure network interface. if vm.TapDevice != "" { if err := client.putInterface(fcInterface{ IfaceID: "eth0", GuestMac: generateMAC(sanctumID), HostDevName: vm.TapDevice, }); err != nil { return fmt.Errorf("firecracker: configure network: %w", err) } // Create the tap device on the host. if out, err := exec.Command("ip", "tuntap", "add", "dev", vm.TapDevice, "mode", "tap").CombinedOutput(); err != nil { return fmt.Errorf("firecracker: create tap %s: %w (%s)", vm.TapDevice, err, string(out)) } if out, err := exec.Command("ip", "link", "set", vm.TapDevice, "up").CombinedOutput(); err != nil { return fmt.Errorf("firecracker: bring up tap %s: %w (%s)", vm.TapDevice, err, string(out)) } } // Start the instance (the actual boot). if err := client.putInstanceAction(fcInstanceAction{ActionType: "InstanceStart"}); err != nil { return fmt.Errorf("firecracker: start instance: %w", err) } return nil } // Stop sends an ACPI shutdown to the Firecracker VM. func (r *FirecrackerRuntime) Stop(ctx context.Context, sanctumID string) error { r.mu.Lock() vm, ok := r.vms[sanctumID] r.mu.Unlock() if !ok { return fmt.Errorf("firecracker: unknown VM %s", sanctumID) } client := newFCClient(vm.SocketPath) if err := client.putInstanceAction(fcInstanceAction{ActionType: "SendCtrlAltDel"}); err != nil { // VM may already be stopped — log but continue cleanup. } // Wait for the VMM process to exit with a context-aware wait. if vm.PID > 0 { waitCtx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() if err := waitForPID(waitCtx, vm.PID); err != nil { // Force-kill if graceful shutdown timed out. _ = syscall.Kill(vm.PID, syscall.SIGKILL) } } // Clean up tap device. if vm.TapDevice != "" { exec.Command("ip", "link", "set", vm.TapDevice, "down").Run() exec.Command("ip", "tuntap", "del", "dev", vm.TapDevice, "mode", "tap").Run() } return nil } // Freeze is a no-op for Firecracker VMs. MicroVMs can be paused // at the VMM level but this requires snapshot support which is // a more advanced feature. For now, we report that freeze is // not supported for Firecracker and suggest using Stop instead. func (r *FirecrackerRuntime) Freeze(ctx context.Context, sanctumID string) error { return fmt.Errorf("firecracker: freeze not supported for microVMs — use Stop instead") } // Thaw is a no-op for Firecracker VMs. func (r *FirecrackerRuntime) Thaw(ctx context.Context, sanctumID string) error { return fmt.Errorf("firecracker: thaw not supported for microVMs — use Start instead") } // Destroy stops and removes a Firecracker microVM. func (r *FirecrackerRuntime) Destroy(ctx context.Context, sanctumID string) error { r.Stop(ctx, sanctumID) r.mu.Lock() defer r.mu.Unlock() vm, ok := r.vms[sanctumID] if !ok { return nil // already destroyed } os.Remove(vm.SocketPath) delete(r.vms, sanctumID) return nil } // Exec runs a command inside a Firecracker VM via serial console. // Note: This is a simplified implementation that uses the serial console. // A production implementation would use an SSH connection or a virtio-serial // channel with a guest agent. func (r *FirecrackerRuntime) Exec(ctx context.Context, sanctumID string, command []string, stdin []byte) (*ExecResult, error) { // Firecracker doesn't have a built-in exec mechanism. // In production, this would use: // 1. mmds (MicroVM Metadata Service) for command dispatch // 2. SSH into the VM's IP address // 3. virtio-serial guest agent // For now, return an error indicating this is not available. return nil, fmt.Errorf("firecracker: exec not supported — use SSH to %s (IP: %s)", sanctumID, r.getVMIP(sanctumID)) } // Status returns the state of a Firecracker microVM. func (r *FirecrackerRuntime) Status(ctx context.Context, sanctumID string) (*SanctumInfo, error) { r.mu.Lock() vm, ok := r.vms[sanctumID] r.mu.Unlock() if !ok { return nil, fmt.Errorf("firecracker: unknown VM %s", sanctumID) } status := StatusStopped if vm.PID > 0 { // Check if the VMM process is still running. if _, err := os.FindProcess(vm.PID); err == nil { // Send signal 0 to check if process exists. if exec.Command("kill", "-0", strconv.Itoa(vm.PID)).Run() == nil { status = StatusRunning } } } return &SanctumInfo{ ID: vm.ID, Name: vm.ID, Runtime: RuntimeFirecracker, Status: status, Arch: "x86_64", // Firecracker is x86_64 only (aarch64 experimental) IP: vm.IP, PID: uint32(vm.PID), RootFS: vm.DrivePath, Created: vm.Started, Cgroup: "", // Firecracker doesn't use cgroups for the guest Metadata: map[string]string{ "socket": vm.SocketPath, "tap_device": vm.TapDevice, "kernel": vm.KernelPath, }, }, nil } // List returns all Firecracker microVMs managed by this runtime. func (r *FirecrackerRuntime) List(ctx context.Context) ([]*SanctumInfo, error) { r.mu.Lock() defer r.mu.Unlock() var infos []*SanctumInfo for _, vm := range r.vms { info, _ := r.Status(ctx, vm.ID) if info != nil { infos = append(infos, info) } } return infos, nil } // CgroupPath returns "" for Firecracker since it doesn't use cgroups. func (r *FirecrackerRuntime) CgroupPath(sanctumID string) string { // Firecracker VMs are not managed via cgroups on the host. // The VMM process itself runs in the host's cgroup, but the // guest processes are isolated in their own kernel. // The eBPF LSM hook still protects the host Tomb regardless. return "" } func (r *FirecrackerRuntime) getVMIP(id string) string { r.mu.Lock() defer r.mu.Unlock() if vm, ok := r.vms[id]; ok { return vm.IP } return "" } // waitForPID polls until the given PID exits or the context expires. func waitForPID(ctx context.Context, pid int) error { ticker := time.NewTicker(100 * time.Millisecond) defer ticker.Stop() for { select { case <-ctx.Done(): return ctx.Err() case <-ticker.C: if err := syscall.Kill(pid, 0); err != nil { return nil // process gone } } } } // --- Firecracker API client --- type fcAPIClient struct { socketPath string httpClient *http.Client baseURL string } func newFCClient(socketPath string) *fcAPIClient { dialer := func(proto, addr string) (net.Conn, error) { return net.Dial("unix", socketPath) } transport := &http.Transport{ Dial: dialer, } return &fcAPIClient{ socketPath: socketPath, httpClient: &http.Client{Transport: transport, Timeout: 10 * time.Second}, baseURL: "http://localhost", } } func (c *fcAPIClient) putBootSource(bs fcBootSource) error { return c.put("/boot-source", bs) } func (c *fcAPIClient) putDrive(d fcDrive) error { return c.put("/drives/"+d.DriveID, d) } func (c *fcAPIClient) putInterface(i fcInterface) error { return c.put("/network-interfaces/"+i.IfaceID, i) } func (c *fcAPIClient) putInstanceAction(a fcInstanceAction) error { return c.put("/actions", a) } func (c *fcAPIClient) put(path string, payload interface{}) error { data, err := json.Marshal(payload) if err != nil { return err } req, err := http.NewRequest("PUT", c.baseURL+path, bytes.NewReader(data)) if err != nil { return err } req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json") resp, err := c.httpClient.Do(req) if err != nil { return fmt.Errorf("firecracker API %s: %w", path, err) } defer resp.Body.Close() if resp.StatusCode >= 400 { body, _ := io.ReadAll(resp.Body) return fmt.Errorf("firecracker API %s: %s (body: %s)", path, resp.Status, string(body)) } return nil } // --- helpers --- // waitForFile waits for a file to appear within a timeout using // a channel-based ticker (SEI CERT: no busy-wait / sleep-in-loop). func waitForFile(path string, timeout time.Duration) error { ticker := time.NewTicker(25 * time.Millisecond) defer ticker.Stop() timer := time.NewTimer(timeout) defer timer.Stop() for { select { case <-ticker.C: if _, err := os.Stat(path); err == nil { return nil } case <-timer.C: return fmt.Errorf("timeout waiting for %s", path) } } } // generateMAC produces a deterministic, locally-administered MAC (second bit // of the first octet set) from the sanctum ID. Uses FNV-1a for better // distribution than the former integer-overflow hash (SEI CERT EXP00-J). func generateMAC(id string) string { h := uint32(2166136261) for _, c := range id { h ^= uint32(c) h *= 16777619 } return fmt.Sprintf("02:FC:%02X:%02X:%02X:%02X", (h>>24)&0xFF, (h>>16)&0xFF, (h>>8)&0xFF, h&0xFF) }