package roothelper import ( "bufio" "context" "encoding/json" "errors" "fmt" "log/slog" "net" "os" "path/filepath" "strconv" "strings" "syscall" "time" "golang.org/x/sys/unix" "banger/internal/daemon/dmsnap" "banger/internal/daemon/fcproc" "banger/internal/firecracker" "banger/internal/hostnat" "banger/internal/installmeta" "banger/internal/paths" "banger/internal/rpc" "banger/internal/system" ) const ( methodEnsureBridge = "priv.ensure_bridge" methodCreateTap = "priv.create_tap" methodDeleteTap = "priv.delete_tap" methodSyncResolverRouting = "priv.sync_resolver_routing" methodClearResolverRouting = "priv.clear_resolver_routing" methodEnsureNAT = "priv.ensure_nat" methodCreateDMSnapshot = "priv.create_dm_snapshot" methodCleanupDMSnapshot = "priv.cleanup_dm_snapshot" methodRemoveDMSnapshot = "priv.remove_dm_snapshot" methodFsckSnapshot = "priv.fsck_snapshot" methodReadExt4File = "priv.read_ext4_file" methodWriteExt4Files = "priv.write_ext4_files" methodResolveFirecrackerBin = "priv.resolve_firecracker_binary" methodLaunchFirecracker = "priv.launch_firecracker" methodEnsureSocketAccess = "priv.ensure_socket_access" methodFindFirecrackerPID = "priv.find_firecracker_pid" methodKillProcess = "priv.kill_process" methodSignalProcess = "priv.signal_process" methodProcessRunning = "priv.process_running" rootfsDMNamePrefix = "fc-rootfs-" vmTapPrefix = "tap-fc-" tapPoolPrefix = "tap-pool-" vmResolverRouteDomain = "~vm" defaultFirecrackerBinaryName = "firecracker" ) type NetworkConfig struct { BridgeName string `json:"bridge_name"` BridgeIP string `json:"bridge_ip"` CIDR string `json:"cidr"` } type Ext4Write struct { GuestPath string `json:"guest_path"` Data []byte `json:"data"` Mode uint32 `json:"mode"` } type FirecrackerLaunchRequest struct { BinaryPath string `json:"binary_path"` VMID string `json:"vm_id"` SocketPath string `json:"socket_path"` LogPath string `json:"log_path"` MetricsPath string `json:"metrics_path"` KernelImagePath string `json:"kernel_image_path"` InitrdPath string `json:"initrd_path,omitempty"` KernelArgs string `json:"kernel_args"` Drives []firecracker.DriveConfig `json:"drives"` TapDevice string `json:"tap_device"` VSockPath string `json:"vsock_path"` VSockCID uint32 `json:"vsock_cid"` VCPUCount int `json:"vcpu_count"` MemoryMiB int `json:"memory_mib"` Network NetworkConfig `json:"network"` } type findPIDResult struct { PID int `json:"pid"` } type processRunningResult struct { Running bool `json:"running"` } type readExt4FileResult struct { Data []byte `json:"data"` } type resolveFirecrackerResult struct { Path string `json:"path"` } type launchFirecrackerResult struct { PID int `json:"pid"` } type Client struct { socketPath string } func NewClient(socketPath string) *Client { return &Client{socketPath: strings.TrimSpace(socketPath)} } func (c *Client) EnsureBridge(ctx context.Context, cfg NetworkConfig) error { _, err := rpc.Call[struct{}](ctx, c.socketPath, methodEnsureBridge, cfg) return err } func (c *Client) CreateTap(ctx context.Context, cfg NetworkConfig, tapName string) error { _, err := rpc.Call[struct{}](ctx, c.socketPath, methodCreateTap, struct { NetworkConfig TapName string `json:"tap_name"` }{NetworkConfig: cfg, TapName: tapName}) return err } func (c *Client) DeleteTap(ctx context.Context, tapName string) error { _, err := rpc.Call[struct{}](ctx, c.socketPath, methodDeleteTap, struct { TapName string `json:"tap_name"` }{TapName: tapName}) return err } func (c *Client) SyncResolverRouting(ctx context.Context, bridgeName, serverAddr string) error { _, err := rpc.Call[struct{}](ctx, c.socketPath, methodSyncResolverRouting, struct { BridgeName string `json:"bridge_name"` ServerAddr string `json:"server_addr"` }{BridgeName: bridgeName, ServerAddr: serverAddr}) return err } func (c *Client) ClearResolverRouting(ctx context.Context, bridgeName string) error { _, err := rpc.Call[struct{}](ctx, c.socketPath, methodClearResolverRouting, struct { BridgeName string `json:"bridge_name"` }{BridgeName: bridgeName}) return err } func (c *Client) EnsureNAT(ctx context.Context, guestIP, tap string, enable bool) error { _, err := rpc.Call[struct{}](ctx, c.socketPath, methodEnsureNAT, struct { GuestIP string `json:"guest_ip"` Tap string `json:"tap"` Enable bool `json:"enable"` }{GuestIP: guestIP, Tap: tap, Enable: enable}) return err } func (c *Client) CreateDMSnapshot(ctx context.Context, rootfsPath, cowPath, dmName string) (dmsnap.Handles, error) { return rpc.Call[dmsnap.Handles](ctx, c.socketPath, methodCreateDMSnapshot, struct { RootfsPath string `json:"rootfs_path"` COWPath string `json:"cow_path"` DMName string `json:"dm_name"` }{RootfsPath: rootfsPath, COWPath: cowPath, DMName: dmName}) } func (c *Client) CleanupDMSnapshot(ctx context.Context, handles dmsnap.Handles) error { _, err := rpc.Call[struct{}](ctx, c.socketPath, methodCleanupDMSnapshot, handles) return err } func (c *Client) RemoveDMSnapshot(ctx context.Context, target string) error { _, err := rpc.Call[struct{}](ctx, c.socketPath, methodRemoveDMSnapshot, struct { Target string `json:"target"` }{Target: target}) return err } func (c *Client) FsckSnapshot(ctx context.Context, dmDev string) error { _, err := rpc.Call[struct{}](ctx, c.socketPath, methodFsckSnapshot, struct { DMDev string `json:"dm_dev"` }{DMDev: dmDev}) return err } func (c *Client) ReadExt4File(ctx context.Context, imagePath, guestPath string) ([]byte, error) { result, err := rpc.Call[readExt4FileResult](ctx, c.socketPath, methodReadExt4File, struct { ImagePath string `json:"image_path"` GuestPath string `json:"guest_path"` }{ImagePath: imagePath, GuestPath: guestPath}) if err != nil { return nil, err } return result.Data, nil } func (c *Client) WriteExt4Files(ctx context.Context, imagePath string, files []Ext4Write) error { _, err := rpc.Call[struct{}](ctx, c.socketPath, methodWriteExt4Files, struct { ImagePath string `json:"image_path"` Files []Ext4Write `json:"files"` }{ImagePath: imagePath, Files: files}) return err } func (c *Client) ResolveFirecrackerBinary(ctx context.Context, requested string) (string, error) { result, err := rpc.Call[resolveFirecrackerResult](ctx, c.socketPath, methodResolveFirecrackerBin, struct { Requested string `json:"requested"` }{Requested: requested}) if err != nil { return "", err } return result.Path, nil } func (c *Client) LaunchFirecracker(ctx context.Context, req FirecrackerLaunchRequest) (int, error) { result, err := rpc.Call[launchFirecrackerResult](ctx, c.socketPath, methodLaunchFirecracker, req) if err != nil { return 0, err } return result.PID, nil } func (c *Client) EnsureSocketAccess(ctx context.Context, socketPath, label string) error { _, err := rpc.Call[struct{}](ctx, c.socketPath, methodEnsureSocketAccess, struct { SocketPath string `json:"socket_path"` Label string `json:"label"` }{SocketPath: socketPath, Label: label}) return err } func (c *Client) FindFirecrackerPID(ctx context.Context, apiSock string) (int, error) { result, err := rpc.Call[findPIDResult](ctx, c.socketPath, methodFindFirecrackerPID, struct { APISock string `json:"api_sock"` }{APISock: apiSock}) if err != nil { return 0, err } return result.PID, nil } func (c *Client) KillProcess(ctx context.Context, pid int) error { _, err := rpc.Call[struct{}](ctx, c.socketPath, methodKillProcess, struct { PID int `json:"pid"` }{PID: pid}) return err } func (c *Client) SignalProcess(ctx context.Context, pid int, signal string) error { _, err := rpc.Call[struct{}](ctx, c.socketPath, methodSignalProcess, struct { PID int `json:"pid"` Signal string `json:"signal"` }{PID: pid, Signal: signal}) return err } func (c *Client) ProcessRunning(ctx context.Context, pid int, apiSock string) (bool, error) { result, err := rpc.Call[processRunningResult](ctx, c.socketPath, methodProcessRunning, struct { PID int `json:"pid"` APISock string `json:"api_sock"` }{PID: pid, APISock: apiSock}) if err != nil { return false, err } return result.Running, nil } type Server struct { meta installmeta.Metadata runner system.CommandRunner logger *slog.Logger listener net.Listener } func Open() (*Server, error) { meta, err := installmeta.Load(installmeta.DefaultPath) if err != nil { return nil, err } if err := os.MkdirAll(installmeta.DefaultRootHelperRuntimeDir, 0o711); err != nil { return nil, err } if err := os.Chmod(installmeta.DefaultRootHelperRuntimeDir, 0o711); err != nil { return nil, err } return &Server{ meta: meta, runner: system.NewRunner(), logger: slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo})), }, nil } func (s *Server) Close() error { if s == nil || s.listener == nil { return nil } return s.listener.Close() } func (s *Server) Serve(ctx context.Context) error { _ = os.Remove(installmeta.DefaultRootHelperSocketPath) listener, err := net.Listen("unix", installmeta.DefaultRootHelperSocketPath) if err != nil { return err } s.listener = listener defer listener.Close() defer os.Remove(installmeta.DefaultRootHelperSocketPath) if err := os.Chmod(installmeta.DefaultRootHelperSocketPath, 0o600); err != nil { return err } if err := os.Chown(installmeta.DefaultRootHelperSocketPath, s.meta.OwnerUID, s.meta.OwnerGID); err != nil { return err } done := make(chan struct{}) defer close(done) go func() { select { case <-ctx.Done(): _ = listener.Close() case <-done: } }() for { conn, err := listener.Accept() if err != nil { select { case <-ctx.Done(): return nil default: } var netErr net.Error if errors.As(err, &netErr) && netErr.Temporary() { time.Sleep(100 * time.Millisecond) continue } return err } go s.handleConn(conn) } } func (s *Server) handleConn(conn net.Conn) { defer conn.Close() if err := s.authorizeConn(conn); err != nil { _ = json.NewEncoder(conn).Encode(rpc.NewError("unauthorized", err.Error())) return } var req rpc.Request if err := json.NewDecoder(bufio.NewReader(conn)).Decode(&req); err != nil { _ = json.NewEncoder(conn).Encode(rpc.NewError("bad_request", err.Error())) return } resp := s.dispatch(context.Background(), req) _ = json.NewEncoder(conn).Encode(resp) } func (s *Server) authorizeConn(conn net.Conn) error { unixConn, ok := conn.(*net.UnixConn) if !ok { return errors.New("root helper requires unix connections") } rawConn, err := unixConn.SyscallConn() if err != nil { return err } var cred *unix.Ucred var controlErr error if err := rawConn.Control(func(fd uintptr) { cred, controlErr = unix.GetsockoptUcred(int(fd), unix.SOL_SOCKET, unix.SO_PEERCRED) }); err != nil { return err } if controlErr != nil { return controlErr } if cred == nil { return errors.New("missing peer credentials") } if int(cred.Uid) == 0 || int(cred.Uid) == s.meta.OwnerUID { return nil } return fmt.Errorf("uid %d is not allowed to use the root helper", cred.Uid) } func (s *Server) dispatch(ctx context.Context, req rpc.Request) rpc.Response { switch req.Method { case methodEnsureBridge: params, err := rpc.DecodeParams[NetworkConfig](req) if err != nil { return rpc.NewError("bad_params", err.Error()) } return marshalResultOrError(struct{}{}, s.ensureBridge(ctx, params)) case methodCreateTap: params, err := rpc.DecodeParams[struct { NetworkConfig TapName string `json:"tap_name"` }](req) if err != nil { return rpc.NewError("bad_params", err.Error()) } return marshalResultOrError(struct{}{}, s.createTap(ctx, params.NetworkConfig, params.TapName)) case methodDeleteTap: params, err := rpc.DecodeParams[struct { TapName string `json:"tap_name"` }](req) if err != nil { return rpc.NewError("bad_params", err.Error()) } return marshalResultOrError(struct{}{}, s.deleteTap(ctx, params.TapName)) case methodSyncResolverRouting: params, err := rpc.DecodeParams[struct { BridgeName string `json:"bridge_name"` ServerAddr string `json:"server_addr"` }](req) if err != nil { return rpc.NewError("bad_params", err.Error()) } return marshalResultOrError(struct{}{}, s.syncResolverRouting(ctx, params.BridgeName, params.ServerAddr)) case methodClearResolverRouting: params, err := rpc.DecodeParams[struct { BridgeName string `json:"bridge_name"` }](req) if err != nil { return rpc.NewError("bad_params", err.Error()) } return marshalResultOrError(struct{}{}, s.clearResolverRouting(ctx, params.BridgeName)) case methodEnsureNAT: params, err := rpc.DecodeParams[struct { GuestIP string `json:"guest_ip"` Tap string `json:"tap"` Enable bool `json:"enable"` }](req) if err != nil { return rpc.NewError("bad_params", err.Error()) } return marshalResultOrError(struct{}{}, hostnat.Ensure(ctx, s.runner, params.GuestIP, params.Tap, params.Enable)) case methodCreateDMSnapshot: params, err := rpc.DecodeParams[struct { RootfsPath string `json:"rootfs_path"` COWPath string `json:"cow_path"` DMName string `json:"dm_name"` }](req) if err != nil { return rpc.NewError("bad_params", err.Error()) } if err := s.validateManagedPath(params.RootfsPath, paths.ResolveSystem().StateDir); err != nil { return rpc.NewError("bad_params", err.Error()) } if err := s.validateManagedPath(params.COWPath, paths.ResolveSystem().StateDir); err != nil { return rpc.NewError("bad_params", err.Error()) } if err := validateDMName(params.DMName); err != nil { return rpc.NewError("bad_params", err.Error()) } result, err := dmsnap.Create(ctx, s.runner, params.RootfsPath, params.COWPath, params.DMName) return marshalResultOrError(result, err) case methodCleanupDMSnapshot: params, err := rpc.DecodeParams[dmsnap.Handles](req) if err != nil { return rpc.NewError("bad_params", err.Error()) } return marshalResultOrError(struct{}{}, dmsnap.Cleanup(ctx, s.runner, params)) case methodRemoveDMSnapshot: params, err := rpc.DecodeParams[struct { Target string `json:"target"` }](req) if err != nil { return rpc.NewError("bad_params", err.Error()) } return marshalResultOrError(struct{}{}, dmsnap.Remove(ctx, s.runner, params.Target)) case methodFsckSnapshot: params, err := rpc.DecodeParams[struct { DMDev string `json:"dm_dev"` }](req) if err != nil { return rpc.NewError("bad_params", err.Error()) } return marshalResultOrError(struct{}{}, s.fsckSnapshot(ctx, params.DMDev)) case methodReadExt4File: params, err := rpc.DecodeParams[struct { ImagePath string `json:"image_path"` GuestPath string `json:"guest_path"` }](req) if err != nil { return rpc.NewError("bad_params", err.Error()) } data, readErr := system.ReadExt4File(ctx, s.runner, params.ImagePath, params.GuestPath) return marshalResultOrError(readExt4FileResult{Data: data}, readErr) case methodWriteExt4Files: params, err := rpc.DecodeParams[struct { ImagePath string `json:"image_path"` Files []Ext4Write `json:"files"` }](req) if err != nil { return rpc.NewError("bad_params", err.Error()) } return marshalResultOrError(struct{}{}, s.writeExt4Files(ctx, params.ImagePath, params.Files)) case methodResolveFirecrackerBin: params, err := rpc.DecodeParams[struct { Requested string `json:"requested"` }](req) if err != nil { return rpc.NewError("bad_params", err.Error()) } path, resolveErr := s.resolveFirecrackerBinary(params.Requested) return marshalResultOrError(resolveFirecrackerResult{Path: path}, resolveErr) case methodLaunchFirecracker: params, err := rpc.DecodeParams[FirecrackerLaunchRequest](req) if err != nil { return rpc.NewError("bad_params", err.Error()) } pid, launchErr := s.launchFirecracker(ctx, params) return marshalResultOrError(launchFirecrackerResult{PID: pid}, launchErr) case methodEnsureSocketAccess: params, err := rpc.DecodeParams[struct { SocketPath string `json:"socket_path"` Label string `json:"label"` }](req) if err != nil { return rpc.NewError("bad_params", err.Error()) } return marshalResultOrError(struct{}{}, s.ensureSocketAccess(ctx, params.SocketPath, params.Label)) case methodFindFirecrackerPID: params, err := rpc.DecodeParams[struct { APISock string `json:"api_sock"` }](req) if err != nil { return rpc.NewError("bad_params", err.Error()) } pid, findErr := fcproc.New(s.runner, fcproc.Config{}, s.logger).FindPID(ctx, params.APISock) return marshalResultOrError(findPIDResult{PID: pid}, findErr) case methodKillProcess: params, err := rpc.DecodeParams[struct { PID int `json:"pid"` }](req) if err != nil { return rpc.NewError("bad_params", err.Error()) } _, killErr := s.runner.Run(ctx, "kill", "-KILL", strconv.Itoa(params.PID)) return marshalResultOrError(struct{}{}, killErr) case methodSignalProcess: params, err := rpc.DecodeParams[struct { PID int `json:"pid"` Signal string `json:"signal"` }](req) if err != nil { return rpc.NewError("bad_params", err.Error()) } signal := strings.TrimSpace(params.Signal) if signal == "" { signal = "TERM" } _, signalErr := s.runner.Run(ctx, "kill", "-"+signal, strconv.Itoa(params.PID)) return marshalResultOrError(struct{}{}, signalErr) case methodProcessRunning: params, err := rpc.DecodeParams[struct { PID int `json:"pid"` APISock string `json:"api_sock"` }](req) if err != nil { return rpc.NewError("bad_params", err.Error()) } return marshalResultOrError(processRunningResult{Running: system.ProcessRunning(params.PID, params.APISock)}, nil) default: return rpc.NewError("unknown_method", req.Method) } } func (s *Server) ensureBridge(ctx context.Context, cfg NetworkConfig) error { return fcproc.New(s.runner, fcproc.Config{ BridgeName: cfg.BridgeName, BridgeIP: cfg.BridgeIP, CIDR: cfg.CIDR, }, s.logger).EnsureBridge(ctx) } func (s *Server) createTap(ctx context.Context, cfg NetworkConfig, tapName string) error { if err := validateTapName(tapName); err != nil { return err } return fcproc.New(s.runner, fcproc.Config{ BridgeName: cfg.BridgeName, BridgeIP: cfg.BridgeIP, CIDR: cfg.CIDR, }, s.logger).CreateTapOwned(ctx, tapName, s.meta.OwnerUID, s.meta.OwnerGID) } func (s *Server) deleteTap(ctx context.Context, tapName string) error { if err := validateTapName(tapName); err != nil { return err } _, err := s.runner.Run(ctx, "ip", "link", "del", tapName) return err } func (s *Server) syncResolverRouting(ctx context.Context, bridgeName, serverAddr string) error { if strings.TrimSpace(bridgeName) == "" || strings.TrimSpace(serverAddr) == "" { return nil } if _, err := system.LookupExecutable("resolvectl"); err != nil { return nil } if _, err := s.runner.Run(ctx, "resolvectl", "dns", bridgeName, serverAddr); err != nil { return err } if _, err := s.runner.Run(ctx, "resolvectl", "domain", bridgeName, vmResolverRouteDomain); err != nil { return err } _, err := s.runner.Run(ctx, "resolvectl", "default-route", bridgeName, "no") return err } func (s *Server) clearResolverRouting(ctx context.Context, bridgeName string) error { if strings.TrimSpace(bridgeName) == "" { return nil } if _, err := system.LookupExecutable("resolvectl"); err != nil { return nil } _, err := s.runner.Run(ctx, "resolvectl", "revert", bridgeName) return err } func (s *Server) fsckSnapshot(ctx context.Context, dmDev string) error { if strings.TrimSpace(dmDev) == "" { return errors.New("dm device is required") } if _, err := s.runner.Run(ctx, "e2fsck", "-fy", dmDev); err != nil { if code := system.ExitCode(err); code < 0 || code > 1 { return fmt.Errorf("fsck snapshot: %w", err) } } return nil } func (s *Server) writeExt4Files(ctx context.Context, imagePath string, files []Ext4Write) error { for _, file := range files { mode := os.FileMode(file.Mode) if mode == 0 { mode = 0o644 } if err := system.WriteExt4FileOwned(ctx, s.runner, imagePath, file.GuestPath, mode, 0, 0, file.Data); err != nil { return err } } return nil } func (s *Server) resolveFirecrackerBinary(requested string) (string, error) { requested = strings.TrimSpace(requested) if requested == "" { requested = defaultFirecrackerBinaryName } cfg := fcproc.Config{FirecrackerBin: requested} resolved, err := fcproc.New(s.runner, cfg, s.logger).ResolveBinary() if err != nil { return "", err } if err := validateRootExecutable(resolved); err != nil { return "", err } return resolved, nil } func (s *Server) launchFirecracker(ctx context.Context, req FirecrackerLaunchRequest) (int, error) { systemLayout := paths.ResolveSystem() for _, path := range []string{req.SocketPath, req.VSockPath} { if err := s.validateManagedPath(path, systemLayout.RuntimeDir); err != nil { return 0, err } } for _, path := range []string{req.LogPath, req.MetricsPath, req.KernelImagePath} { if err := s.validateManagedPath(path, systemLayout.StateDir); err != nil { return 0, err } } if strings.TrimSpace(req.InitrdPath) != "" { if err := s.validateManagedPath(req.InitrdPath, systemLayout.StateDir); err != nil { return 0, err } } if err := validateTapName(req.TapDevice); err != nil { return 0, err } if err := validateRootExecutable(req.BinaryPath); err != nil { return 0, err } for _, drive := range req.Drives { if err := s.validateLaunchDrivePath(drive, systemLayout.StateDir); err != nil { return 0, err } } machine, err := firecracker.NewMachine(ctx, firecracker.MachineConfig{ BinaryPath: req.BinaryPath, VMID: req.VMID, SocketPath: req.SocketPath, LogPath: req.LogPath, MetricsPath: req.MetricsPath, KernelImagePath: req.KernelImagePath, InitrdPath: req.InitrdPath, KernelArgs: req.KernelArgs, Drives: req.Drives, TapDevice: req.TapDevice, VSockPath: req.VSockPath, VSockCID: req.VSockCID, VCPUCount: req.VCPUCount, MemoryMiB: req.MemoryMiB, Logger: s.logger, }) if err != nil { return 0, err } if err := machine.Start(ctx); err != nil { manager := fcproc.New(s.runner, fcproc.Config{BridgeName: req.Network.BridgeName, BridgeIP: req.Network.BridgeIP, CIDR: req.Network.CIDR}, s.logger) if pid := manager.ResolvePID(context.Background(), machine, req.SocketPath); pid > 0 { _, _ = s.runner.Run(context.Background(), "kill", "-KILL", strconv.Itoa(pid)) } return 0, err } manager := fcproc.New(s.runner, fcproc.Config{BridgeName: req.Network.BridgeName, BridgeIP: req.Network.BridgeIP, CIDR: req.Network.CIDR}, s.logger) if err := manager.EnsureSocketAccessFor(ctx, req.SocketPath, "firecracker api socket", s.meta.OwnerUID, s.meta.OwnerGID); err != nil { return 0, err } if strings.TrimSpace(req.VSockPath) != "" { if err := manager.EnsureSocketAccessFor(ctx, req.VSockPath, "firecracker vsock socket", s.meta.OwnerUID, s.meta.OwnerGID); err != nil { return 0, err } } pid := manager.ResolvePID(context.Background(), machine, req.SocketPath) if pid <= 0 { return 0, errors.New("firecracker started but pid could not be resolved") } return pid, nil } func (s *Server) validateLaunchDrivePath(drive firecracker.DriveConfig, stateDir string) error { if err := s.validateManagedPath(drive.Path, stateDir); err == nil { return nil } if drive.IsRoot { if err := validateDMDevicePath(drive.Path); err == nil { return nil } } return fmt.Errorf("path %q is outside banger-managed directories", drive.Path) } func (s *Server) ensureSocketAccess(ctx context.Context, socketPath, label string) error { return fcproc.New(s.runner, fcproc.Config{}, s.logger).EnsureSocketAccessFor(ctx, socketPath, label, s.meta.OwnerUID, s.meta.OwnerGID) } func (s *Server) validateManagedPath(path string, roots ...string) error { path = strings.TrimSpace(path) if path == "" { return errors.New("path is required") } if !filepath.IsAbs(path) { return fmt.Errorf("path %q must be absolute", path) } cleaned := filepath.Clean(path) for _, root := range roots { root = strings.TrimSpace(root) if root == "" { continue } root = filepath.Clean(root) if cleaned == root || strings.HasPrefix(cleaned, root+string(os.PathSeparator)) { return nil } } return fmt.Errorf("path %q is outside banger-managed directories", path) } func validateTapName(tapName string) error { tapName = strings.TrimSpace(tapName) if strings.HasPrefix(tapName, vmTapPrefix) || strings.HasPrefix(tapName, tapPoolPrefix) { return nil } return fmt.Errorf("tap %q is outside banger-managed naming", tapName) } func validateDMName(dmName string) error { dmName = strings.TrimSpace(dmName) if strings.HasPrefix(dmName, rootfsDMNamePrefix) { return nil } return fmt.Errorf("dm target %q is outside banger-managed naming", dmName) } func validateDMDevicePath(path string) error { path = strings.TrimSpace(path) if path == "" { return errors.New("dm device path is required") } if !filepath.IsAbs(path) { return fmt.Errorf("dm device path %q must be absolute", path) } cleaned := filepath.Clean(path) if filepath.Dir(cleaned) != "/dev/mapper" { return fmt.Errorf("dm device path %q is outside /dev/mapper", path) } return validateDMName(filepath.Base(cleaned)) } func validateRootExecutable(path string) error { info, err := os.Stat(path) if err != nil { return err } if !info.Mode().IsRegular() { return fmt.Errorf("firecracker binary %q is not a regular file", path) } if info.Mode().Perm()&0o111 == 0 { return fmt.Errorf("firecracker binary %q is not executable", path) } if info.Mode().Perm()&0o022 != 0 { return fmt.Errorf("firecracker binary %q must not be group/world writable", path) } stat, ok := info.Sys().(*syscall.Stat_t) if !ok { return fmt.Errorf("inspect owner for %q: unsupported file metadata", path) } if stat.Uid != 0 { return fmt.Errorf("firecracker binary %q must be root-owned in system mode", path) } return nil } func marshalResultOrError(v any, err error) rpc.Response { if err != nil { return rpc.NewError("operation_failed", err.Error()) } resp, marshalErr := rpc.NewResult(v) if marshalErr != nil { return rpc.NewError("marshal_failed", marshalErr.Error()) } return resp }