"""Runtime executor adapters — subprocess wrappers for each supported backend. Security note (F-01 / F-02 remediation): All adapters now invoke ``subprocess.run([...])`` with explicit argument lists instead of building a shell string via f-string interpolation. This eliminates the shell-injection surface from untrusted ``container`` / ``vm`` / ``command`` fields. Supports: - host: run locally via subprocess (default) - lxc: run inside an LXC container via ``lxc exec`` - libvirt: run inside a libvirt VM via ``virsh domexec`` - tmux: run in a detached tmux session - podman: run inside a rootless Podman container - firecracker: run inside a Firecracker microVM via SSH """ from __future__ import annotations import os import shlex import subprocess from typing import Dict, Mapping, Optional # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _shlex_split(command: str) -> list[str]: """Tokenize a shell command string into argv. Used when the caller hands us a single ``command`` string that the underlying tool will exec directly (no shell). We split it with :func:`shlex.split` so subprocess receives a proper argv list and no metacharacter injection is possible. """ return shlex.split(command) def _sanitize_path(path: Optional[str]) -> Optional[str]: """Reject path values that look like they are trying to escape argv. A path is allowed if it does not contain a NUL byte and does not start with ``-`` (which would be interpreted as a flag by most tools). Returns the path unchanged, or ``None`` if rejected. """ if path is None: return None if "\x00" in path: return None if path.startswith("-"): return None return path # --------------------------------------------------------------------------- # Host # --------------------------------------------------------------------------- def run_host(command: str, cwd: Optional[str], env: Optional[Mapping[str, str]]) -> int: """Execute a command on the host. The command is tokenized with :func:`shlex.split` and passed to ``subprocess.run`` as an argv list — never via ``shell=True``. """ argv = _shlex_split(command) if not argv: return 2 # ENOENT-ish — nothing to run return subprocess.run(argv, cwd=cwd, env=dict(env) if env else None).returncode # --------------------------------------------------------------------------- # LXC # --------------------------------------------------------------------------- def run_lxc(container: str, command: str) -> int: """Execute a command inside an LXC container.""" container = _sanitize_path(container) if not container: return 2 # Pass the command through `bash -lc` *inside* the container by giving # lxc exec a literal argv — lxc itself does the right thing here. argv = ["lxc", "exec", container, "--", "bash", "-lc", command] return subprocess.run(argv).returncode # --------------------------------------------------------------------------- # libvirt # --------------------------------------------------------------------------- def run_libvirt(vm_name: str, command: str) -> int: """Execute a command inside a libvirt VM via qemu-agent.""" vm_name = _sanitize_path(vm_name) if not vm_name: return 2 argv = ["virsh", "domexec", vm_name, "--", "bash", "-c", command] return subprocess.run(argv).returncode # --------------------------------------------------------------------------- # Podman # --------------------------------------------------------------------------- def run_podman(container: str, command: str, cwd: Optional[str] = None) -> int: """Execute a command inside a Podman container.""" container = _sanitize_path(container) if not container: return 2 argv = ["podman", "exec"] if cwd: cwd_clean = _sanitize_path(cwd) if cwd_clean: argv += ["--workdir", cwd_clean] argv += [container, "bash", "-c", command] return subprocess.run(argv).returncode # --------------------------------------------------------------------------- # Firecracker (SSH) # --------------------------------------------------------------------------- def run_firecracker( host: str, ssh_key: str, ssh_port: int, command: str, cwd: Optional[str] = None, ) -> int: """Execute a command inside a Firecracker microVM via SSH. Builds the SSH argv explicitly — no f-string shell interpolation. The remote ``cd`` + command is delivered as a single argument to ``ssh``, which then runs it through the remote user's shell. This is the standard ssh idiom and is safe because the *local* subprocess layer sees a clean argv list. """ host = _sanitize_path(host) ssh_key = _sanitize_path(ssh_key) if not host or not ssh_key: return 2 try: port = int(ssh_port) except (TypeError, ValueError): return 2 remote = f"cd {cwd or '/root'} && {command}" argv = [ "ssh", "-i", ssh_key, "-p", str(port), "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", "-o", "ConnectTimeout=5", f"root@{host}", remote, ] return subprocess.run(argv).returncode