Preserve cleanup after daemon restarts and harden OCI and tar imports against filenames that debugfs cannot encode safely. Mirror tap, loop, and dm teardown identity onto VM.Runtime, teach cleanup and reconcile to fall back to those persisted fields when handles.json is missing or corrupt, and clear the recovery state on stop, error, and delete paths. Reject debugfs-hostile entry names during flattening and in ApplyOwnership itself, then add regression coverage for corrupt handles.json recovery and unsafe import paths. Verified with targeted go tests, make lint-go, make lint-shell, and make build.
182 lines
5.2 KiB
Go
182 lines
5.2 KiB
Go
package daemon
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"banger/internal/daemon/fcproc"
|
|
"banger/internal/model"
|
|
"banger/internal/namegen"
|
|
"banger/internal/system"
|
|
"banger/internal/vmdns"
|
|
)
|
|
|
|
// Cross-service constants. Kept in vm.go because both lifecycle
|
|
// (VMService) and networking (HostNetwork) reference them; moving
|
|
// them to either owner would read as a layering violation.
|
|
var (
|
|
errWaitForExitTimeout = fcproc.ErrWaitForExitTimeout
|
|
gracefulShutdownWait = 10 * time.Second
|
|
vsockReadyWait = 30 * time.Second
|
|
vsockReadyPoll = 200 * time.Millisecond
|
|
)
|
|
|
|
// rebuildDNS enumerates live VMs and republishes the DNS record set.
|
|
// Lives on VMService because "alive" is a VM-state concern that
|
|
// HostNetwork shouldn't need to reach into. VMService orchestrates:
|
|
// VM list from the store, alive filter, hand the resulting map to
|
|
// HostNetwork.replaceDNS.
|
|
func (s *VMService) rebuildDNS(ctx context.Context) error {
|
|
if s.net == nil {
|
|
return nil
|
|
}
|
|
vms, err := s.store.ListVMs(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
records := make(map[string]string)
|
|
for _, vm := range vms {
|
|
if !s.vmAlive(vm) {
|
|
continue
|
|
}
|
|
if strings.TrimSpace(vm.Runtime.GuestIP) == "" {
|
|
continue
|
|
}
|
|
records[vmdns.RecordName(vm.Name)] = vm.Runtime.GuestIP
|
|
}
|
|
return s.net.replaceDNS(records)
|
|
}
|
|
|
|
func persistRuntimeTeardownState(vm *model.VMRecord, h model.VMHandles) {
|
|
if vm == nil {
|
|
return
|
|
}
|
|
vm.Runtime.TapDevice = h.TapDevice
|
|
vm.Runtime.BaseLoop = h.BaseLoop
|
|
vm.Runtime.COWLoop = h.COWLoop
|
|
vm.Runtime.DMName = h.DMName
|
|
vm.Runtime.DMDev = h.DMDev
|
|
}
|
|
|
|
func clearRuntimeTeardownState(vm *model.VMRecord) {
|
|
if vm == nil {
|
|
return
|
|
}
|
|
vm.Runtime.TapDevice = ""
|
|
vm.Runtime.BaseLoop = ""
|
|
vm.Runtime.COWLoop = ""
|
|
vm.Runtime.DMName = ""
|
|
vm.Runtime.DMDev = ""
|
|
}
|
|
|
|
func teardownHandlesForCleanup(vm model.VMRecord, live model.VMHandles) model.VMHandles {
|
|
recovered := live
|
|
if strings.TrimSpace(recovered.TapDevice) == "" {
|
|
recovered.TapDevice = strings.TrimSpace(vm.Runtime.TapDevice)
|
|
}
|
|
if strings.TrimSpace(recovered.BaseLoop) == "" {
|
|
recovered.BaseLoop = strings.TrimSpace(vm.Runtime.BaseLoop)
|
|
}
|
|
if strings.TrimSpace(recovered.COWLoop) == "" {
|
|
recovered.COWLoop = strings.TrimSpace(vm.Runtime.COWLoop)
|
|
}
|
|
if strings.TrimSpace(recovered.DMName) == "" {
|
|
recovered.DMName = strings.TrimSpace(vm.Runtime.DMName)
|
|
}
|
|
if strings.TrimSpace(recovered.DMDev) == "" {
|
|
recovered.DMDev = strings.TrimSpace(vm.Runtime.DMDev)
|
|
}
|
|
return recovered
|
|
}
|
|
|
|
// cleanupRuntime tears down the host-side state for a VM: firecracker
|
|
// process, DM snapshot, capabilities, tap, sockets. Lives on VMService
|
|
// because it reaches into handles (VMService-owned); the capability
|
|
// teardown goes through the capHooks seam to keep Daemon out of the
|
|
// dependency chain.
|
|
func (s *VMService) cleanupRuntime(ctx context.Context, vm model.VMRecord, preserveDisks bool) error {
|
|
if s.logger != nil {
|
|
s.logger.Debug("cleanup runtime", append(vmLogAttrs(vm), "preserve_disks", preserveDisks)...)
|
|
}
|
|
h := s.vmHandles(vm.ID)
|
|
cleanupPID := h.PID
|
|
if vm.Runtime.APISockPath != "" {
|
|
if pid, err := s.net.findFirecrackerPID(ctx, vm.Runtime.APISockPath); err == nil && pid > 0 {
|
|
cleanupPID = pid
|
|
}
|
|
}
|
|
if cleanupPID > 0 && system.ProcessRunning(cleanupPID, vm.Runtime.APISockPath) {
|
|
_ = s.net.killVMProcess(ctx, cleanupPID)
|
|
if err := s.net.waitForExit(ctx, cleanupPID, vm.Runtime.APISockPath, 30*time.Second); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
handles := teardownHandlesForCleanup(vm, h)
|
|
snapshotErr := s.net.cleanupDMSnapshot(ctx, dmSnapshotHandles{
|
|
BaseLoop: handles.BaseLoop,
|
|
COWLoop: handles.COWLoop,
|
|
DMName: handles.DMName,
|
|
DMDev: handles.DMDev,
|
|
})
|
|
featureErr := s.capHooks.cleanupState(ctx, vm)
|
|
var tapErr error
|
|
// Prefer the handle cache (fresh from startVMLocked), but fall
|
|
// back to the VMRuntime mirrors so restart-time cleanup still works
|
|
// when handles.json is missing or corrupt.
|
|
tap := handles.TapDevice
|
|
if tap != "" {
|
|
tapErr = s.net.releaseTap(ctx, tap)
|
|
}
|
|
if vm.Runtime.APISockPath != "" {
|
|
_ = os.Remove(vm.Runtime.APISockPath)
|
|
}
|
|
if vm.Runtime.VSockPath != "" {
|
|
_ = os.Remove(vm.Runtime.VSockPath)
|
|
}
|
|
// The handles are only meaningful while the kernel objects exist;
|
|
// dropping them here keeps the cache in sync with reality even
|
|
// when the caller forgets to call clearVMHandles explicitly.
|
|
s.clearVMHandles(vm)
|
|
if !preserveDisks && vm.Runtime.VMDir != "" {
|
|
return errors.Join(snapshotErr, featureErr, tapErr, os.RemoveAll(vm.Runtime.VMDir))
|
|
}
|
|
return errors.Join(snapshotErr, featureErr, tapErr)
|
|
}
|
|
|
|
func (s *VMService) generateName(ctx context.Context) (string, error) {
|
|
_ = ctx
|
|
if name := strings.TrimSpace(namegen.Generate()); name != "" {
|
|
return name, nil
|
|
}
|
|
return "vm-" + strconv.FormatInt(time.Now().Unix(), 10), nil
|
|
}
|
|
|
|
func bridgePrefix(bridgeIP string) string {
|
|
parts := strings.Split(bridgeIP, ".")
|
|
if len(parts) < 3 {
|
|
return bridgeIP
|
|
}
|
|
return strings.Join(parts[:3], ".")
|
|
}
|
|
|
|
func optionalIntOrDefault(value *int, fallback int) int {
|
|
if value != nil {
|
|
return *value
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
func validateOptionalPositiveSetting(label string, value *int) error {
|
|
if value == nil {
|
|
return nil
|
|
}
|
|
if *value <= 0 {
|
|
return fmt.Errorf("%s must be a positive integer", label)
|
|
}
|
|
return nil
|
|
}
|