Pcileech/generate.py

1511 lines
57 KiB
Python

#!/usr/bin/env python3
"""
Host-side orchestrator for DMA firmware generation.
This script:
• Enumerates PCIe devices
• Allows user to select a donor device
• Re-binds donor to vfio-pci driver
• Launches Podman container (image: pcileech-fw-generator) that runs build.py
• Optionally flashes output/firmware.bin with usbloader
• Restores original driver afterwards
Requires root privileges (sudo) for driver rebinding and VFIO operations.
"""
import argparse
import datetime
import logging
import os
import pathlib
import platform
import re
import shutil
import subprocess
import sys
import textwrap
import time
from typing import Dict, List, Optional, Tuple
# Import donor dump manager
try:
from src.donor_dump_manager import DonorDumpError, DonorDumpManager
except ImportError:
DonorDumpManager = None
DonorDumpError = Exception
# Git repository information
PCILEECH_FPGA_REPO = "https://github.com/ufrisk/pcileech-fpga.git"
REPO_CACHE_DIR = os.path.expanduser("~/.cache/pcileech-fw-generator/repos")
def clear_python_cache():
"""Clear Python bytecode cache files to ensure updated code is used."""
import glob
cache_patterns = [
"__pycache__",
"src/__pycache__",
"tests/__pycache__",
"tests/tui/__pycache__",
"src/tui/__pycache__",
"src/tui/core/__pycache__",
"src/tui/models/__pycache__",
"src/scripts/__pycache__",
]
for pattern in cache_patterns:
for cache_dir in glob.glob(pattern):
if os.path.exists(cache_dir):
try:
shutil.rmtree(cache_dir)
print(f"[*] Cleared Python cache: {cache_dir}")
except Exception as e:
print(f"[!] Warning: Could not clear cache {cache_dir}: {e}")
# Clear Python cache at startup to ensure updated code is used
clear_python_cache()
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s",
handlers=[
logging.StreamHandler(sys.stdout),
logging.FileHandler("generate.log", mode="a"),
],
)
logger = logging.getLogger(__name__)
def validate_bdf_format(bdf: str) -> bool:
"""
Validate BDF (Bus:Device.Function) format.
Expected format: DDDD:BB:DD.F where D=hex digit, B=hex digit, F=0-7
Example: 0000:03:00.0
"""
bdf_pattern = re.compile(r"^[0-9a-fA-F]{4}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}\.[0-7]$")
return bool(bdf_pattern.match(bdf))
def run_command(cmd: str, timeout: int = 30, **kwargs) -> str:
"""Execute a shell command and return stripped output with timeout and better error handling."""
try:
return subprocess.check_output(
cmd, shell=True, text=True, timeout=timeout, **kwargs
).strip()
except subprocess.TimeoutExpired as e:
logger.error(f"Command timed out after {timeout}s: {cmd}")
raise RuntimeError(f"Command timed out: {cmd}") from e
except subprocess.CalledProcessError as e:
logger.error(f"Command failed (exit code {e.returncode}): {cmd}")
logger.error(f"Command stderr: {e.stderr}")
raise
def is_linux() -> bool:
"""Check if running on Linux."""
import platform
return platform.system().lower() == "linux"
def check_linux_requirement(operation: str) -> None:
"""Check if operation requires Linux and raise error if not available."""
if not is_linux():
raise RuntimeError(
f"{operation} requires Linux. "
f"Current platform: {platform.system()}. "
f"Please run this on a Linux system with VFIO support."
)
def list_pci_devices() -> List[Dict[str, str]]:
"""List all PCIe devices with their details."""
check_linux_requirement("PCIe device enumeration")
pattern = re.compile(
r"(?P<bdf>[0-9a-fA-F:.]+) .*?\["
r"(?P<class>[0-9a-fA-F]{4})\]: .*?\["
r"(?P<ven>[0-9a-fA-F]{4}):(?P<dev>[0-9a-fA-F]{4})\]"
)
devices = []
for line in run_command("lspci -Dnn").splitlines():
match = pattern.match(line)
if match:
device_info = match.groupdict()
device_info["pretty"] = line
devices.append(device_info)
return devices
def choose_device(devices: List[Dict[str, str]]) -> Dict[str, str]:
"""Interactive device selection from the list of PCIe devices."""
print("\nSelect donor PCIe device:")
for i, device in enumerate(devices):
print(f" [{i}] {device['pretty']}")
while True:
try:
selection = input("Enter number: ")
index = int(selection)
return devices[index]
except (ValueError, IndexError):
print(" Invalid selection — please try again.")
def get_current_driver(bdf: str) -> Optional[str]:
"""Get the current driver bound to a PCIe device."""
check_linux_requirement("Driver detection")
if not validate_bdf_format(bdf):
raise ValueError(
f"Invalid BDF format: {bdf}. Expected format: DDDD:BB:DD.F (e.g., 0000:03:00.0)"
)
driver_path = f"/sys/bus/pci/devices/{bdf}/driver"
if os.path.exists(driver_path):
return os.path.basename(os.path.realpath(driver_path))
return None
def get_iommu_group(bdf: str) -> str:
"""Get the IOMMU group for a PCIe device."""
check_linux_requirement("IOMMU group detection")
if not validate_bdf_format(bdf):
raise ValueError(
f"Invalid BDF format: {bdf}. Expected format: DDDD:BB:DD.F (e.g., 0000:03:00.0)"
)
iommu_link = f"/sys/bus/pci/devices/{bdf}/iommu_group"
return os.path.basename(os.path.realpath(iommu_link))
def list_usb_devices() -> List[Tuple[str, str]]:
"""Return list of USB devices as (vid:pid, description) tuples."""
try:
output = subprocess.check_output("lsusb", shell=True, text=True).splitlines()
except subprocess.CalledProcessError:
return []
devices = []
for line in output:
match = re.match(
r"Bus.*Device.*ID\s+([0-9a-fA-F]{4}:[0-9a-fA-F]{4})\s+(.*)", line
)
if match:
vid_pid = match.group(1)
description = match.group(2).strip() # Strip whitespace from description
devices.append((vid_pid, description))
return devices
def select_usb_device() -> str:
"""Interactive USB device selection for flashing."""
devices = list_usb_devices()
if not devices:
error_msg = "No USB devices found"
logger.error(error_msg)
raise RuntimeError(error_msg)
print("\nSelect FPGA board / USB programmer:")
for i, (vid_pid, description) in enumerate(devices):
print(f" [{i}] {vid_pid} {description}")
while True:
try:
selection = input("Enter number: ")
index = int(selection)
return devices[index][0] # Return VID:PID
except (ValueError, IndexError):
print(" Invalid selection — please try again.")
except KeyboardInterrupt:
logger.warning("USB device selection interrupted by user")
raise
def flash_firmware(bitfile: pathlib.Path) -> None:
"""Flash firmware to FPGA board using usbloader."""
logger.info("Starting firmware flash process")
if shutil.which("usbloader") is None:
error_msg = "usbloader not found in PATH — install λConcept usbloader first"
logger.error(error_msg)
raise RuntimeError(error_msg)
try:
vid_pid = select_usb_device()
logger.info(f"Selected USB device: {vid_pid}")
print(f"[*] Flashing firmware using VID:PID {vid_pid}")
subprocess.run(
f"usbloader --vidpid {vid_pid} -f {bitfile}", shell=True, check=True
)
logger.info("Firmware flashed successfully")
print("[✓] Firmware flashed successfully")
except subprocess.CalledProcessError as e:
error_msg = f"Flash failed: {e}"
logger.error(error_msg)
raise RuntimeError(error_msg) from e
except Exception as e:
error_msg = f"Unexpected error during firmware flash: {e}"
logger.error(error_msg)
raise RuntimeError(error_msg) from e
def _validate_vfio_prerequisites() -> None:
"""Validate VFIO prerequisites and system configuration."""
# Check if VFIO modules are loaded
vfio_modules = ["/sys/module/vfio", "/sys/module/vfio_pci"]
loaded_modules = [mod for mod in vfio_modules if os.path.exists(mod)]
if not loaded_modules:
error_msg = (
"VFIO modules not loaded. Please load VFIO modules:\n"
" sudo modprobe vfio\n"
" sudo modprobe vfio-pci"
)
logger.error(error_msg)
raise RuntimeError(error_msg)
# Check if vfio-pci driver is available
if not os.path.exists("/sys/bus/pci/drivers/vfio-pci"):
error_msg = (
"vfio-pci driver not available. Ensure VFIO is enabled in kernel.\n"
"Check: cat /boot/config-$(uname -r) | grep -i vfio"
)
logger.error(error_msg)
raise RuntimeError(error_msg)
# Check if IOMMU is enabled
try:
dmesg_output = run_command("dmesg | grep -i iommu", timeout=10)
if (
"IOMMU enabled" not in dmesg_output
and "AMD-Vi" not in dmesg_output
and "Intel-IOMMU" not in dmesg_output
):
logger.warning(
"IOMMU may not be enabled. Check BIOS settings and kernel parameters."
)
except Exception:
logger.debug("Could not check IOMMU status from dmesg")
def _check_device_in_use(bdf: str) -> bool:
"""Check if device is currently in use by checking for open file descriptors."""
try:
# Check if device has any open file descriptors
lsof_output = run_command(
f"lsof /dev/vfio/* 2>/dev/null | grep -v COMMAND || true", timeout=5
)
if bdf in lsof_output:
logger.warning(f"Device {bdf} may be in use by another process")
return True
except Exception:
logger.debug("Could not check device usage with lsof")
return False
def _wait_for_device_state(
bdf: str, expected_driver: Optional[str], max_retries: int = 5
) -> bool:
"""Wait for device to reach expected driver state with retries."""
import time
for attempt in range(max_retries):
try:
current_driver = get_current_driver(bdf)
if current_driver == expected_driver:
return True
if attempt < max_retries - 1:
logger.debug(
f"Device {bdf} not in expected state (current: {current_driver}, expected: {expected_driver}), retrying in 1s..."
)
time.sleep(1)
except Exception as e:
logger.debug(f"Error checking device state (attempt {attempt + 1}): {e}")
if attempt < max_retries - 1:
time.sleep(1)
return False
def bind_to_vfio(
bdf: str, vendor: str, device: str, original_driver: Optional[str]
) -> None:
"""Bind PCIe device to vfio-pci driver with enhanced error handling and validation."""
check_linux_requirement("VFIO device binding")
if not validate_bdf_format(bdf):
raise ValueError(
f"Invalid BDF format: {bdf}. Expected format: DDDD:BB:DD.F (e.g., 0000:03:00.0)"
)
# Validate vendor and device IDs
if not re.match(r"^[0-9a-fA-F]{4}$", vendor):
raise ValueError(f"Invalid vendor ID format: {vendor}. Expected 4-digit hex.")
if not re.match(r"^[0-9a-fA-F]{4}$", device):
raise ValueError(f"Invalid device ID format: {device}. Expected 4-digit hex.")
logger.info(
f"Binding device {bdf} (vendor:{vendor} device:{device}) to vfio-pci driver (current driver: {original_driver or 'none'})"
)
# Early exit if already bound to vfio-pci
if original_driver == "vfio-pci":
print(
"[*] Device already bound to vfio-pci driver, skipping binding process..."
)
logger.info(f"Device {bdf} already bound to vfio-pci, skipping binding process")
return
print("[*] Binding device to vfio-pci driver...")
try:
# Validate VFIO prerequisites
_validate_vfio_prerequisites()
# Check if device exists
device_path = f"/sys/bus/pci/devices/{bdf}"
if not os.path.exists(device_path):
error_msg = f"PCIe device {bdf} not found in sysfs"
logger.error(error_msg)
raise RuntimeError(error_msg)
# Check if device is in use
if _check_device_in_use(bdf):
logger.warning(
f"Device {bdf} appears to be in use, proceeding with caution"
)
# Check if device ID is already registered with vfio-pci
device_id_registered = False
try:
ids_file = "/sys/bus/pci/drivers/vfio-pci/ids"
if os.path.exists(ids_file):
with open(ids_file, "r") as f:
registered_ids = f.read()
if f"{vendor} {device}" in registered_ids:
logger.info(
f"Device ID {vendor}:{device} already registered with vfio-pci"
)
device_id_registered = True
except (OSError, IOError) as e:
logger.debug(f"Error checking registered device IDs: {e}")
# Continue with normal flow if we can't check
# Register device ID with vfio-pci if not already registered
if not device_id_registered:
logger.debug(f"Registering device ID {vendor}:{device} with vfio-pci")
max_retries = 3
for attempt in range(max_retries):
try:
# Use direct file writing instead of shell redirection to avoid I/O errors
new_id_path = "/sys/bus/pci/drivers/vfio-pci/new_id"
with open(new_id_path, "w") as f:
f.write(f"{vendor} {device}\n")
logger.info(
f"Successfully registered device ID {vendor}:{device} with vfio-pci"
)
break
except (OSError, IOError) as e:
if (
"File exists" in str(e)
or "Invalid argument" in str(e)
or "Device or resource busy" in str(e)
):
logger.info(
f"Device ID {vendor}:{device} already registered with vfio-pci or busy"
)
break
elif attempt < max_retries - 1:
logger.warning(
f"Failed to register device ID (attempt {attempt + 1}): {e}, retrying..."
)
import time
time.sleep(1)
else:
logger.error(
f"Failed to register device ID after {max_retries} attempts: {e}"
)
raise RuntimeError(f"Failed to register device ID: {e}")
# Unbind from current driver if present
if original_driver:
logger.debug(f"Unbinding from current driver: {original_driver}")
max_retries = 3
for attempt in range(max_retries):
try:
# Use direct file writing instead of shell redirection
unbind_path = f"/sys/bus/pci/devices/{bdf}/driver/unbind"
with open(unbind_path, "w") as f:
f.write(f"{bdf}\n")
logger.info(f"Successfully unbound {bdf} from {original_driver}")
# Wait for unbind to complete
if _wait_for_device_state(bdf, None, max_retries=3):
break
elif attempt < max_retries - 1:
logger.warning(
f"Device still bound after unbind (attempt {attempt + 1}), retrying..."
)
import time
time.sleep(1)
else:
logger.warning(
"Device may still be bound to original driver, continuing..."
)
break
except (OSError, IOError) as e:
if "No such device" in str(e) or "No such file or directory" in str(
e
):
logger.info(f"Device {bdf} already unbound")
break
elif attempt < max_retries - 1:
logger.warning(
f"Failed to unbind from current driver (attempt {attempt + 1}): {e}, retrying..."
)
import time
time.sleep(1)
else:
logger.warning(
f"Failed to unbind from current driver after {max_retries} attempts: {e}"
)
# Continue anyway, as the bind might still work
# Bind to vfio-pci with retries
logger.debug(f"Binding {bdf} to vfio-pci")
max_retries = 3
bind_successful = False
for attempt in range(max_retries):
try:
# Use direct file writing instead of shell redirection
bind_path = "/sys/bus/pci/drivers/vfio-pci/bind"
with open(bind_path, "w") as f:
f.write(f"{bdf}\n")
# Verify binding was successful
if _wait_for_device_state(bdf, "vfio-pci", max_retries=3):
logger.info(f"Successfully bound {bdf} to vfio-pci")
print("[✓] Device successfully bound to vfio-pci driver")
bind_successful = True
break
elif attempt < max_retries - 1:
logger.warning(
f"Bind command succeeded but device not bound to vfio-pci (attempt {attempt + 1}), retrying..."
)
import time
time.sleep(1)
except (OSError, IOError) as e:
if "Device or resource busy" in str(e):
logger.warning(f"Device {bdf} is busy (attempt {attempt + 1})")
if attempt < max_retries - 1:
import time
time.sleep(2) # Longer wait for busy devices
continue
elif "No such device" in str(e) or "No such file or directory" in str(
e
):
logger.error(f"Device {bdf} disappeared during binding")
raise RuntimeError(f"Device {bdf} not found during binding")
elif attempt < max_retries - 1:
logger.warning(
f"Failed to bind to vfio-pci (attempt {attempt + 1}): {e}, retrying..."
)
import time
time.sleep(1)
else:
# Final attempt failed, check if device is actually bound
current_driver = get_current_driver(bdf)
if current_driver == "vfio-pci":
logger.info(
f"Device {bdf} is bound to vfio-pci despite bind command error"
)
print("[✓] Device is bound to vfio-pci driver")
bind_successful = True
break
else:
logger.error(
f"Failed to bind to vfio-pci after {max_retries} attempts: {e}"
)
raise RuntimeError(f"Failed to bind to vfio-pci: {e}")
if not bind_successful:
error_msg = (
f"Failed to bind device {bdf} to vfio-pci after {max_retries} attempts"
)
logger.error(error_msg)
raise RuntimeError(error_msg)
# Final verification
final_driver = get_current_driver(bdf)
if final_driver != "vfio-pci":
error_msg = (
f"Device {bdf} not bound to vfio-pci (current driver: {final_driver})"
)
logger.error(error_msg)
raise RuntimeError(error_msg)
except (OSError, IOError) as e:
error_msg = f"Failed to bind device to vfio-pci: {e}"
logger.error(error_msg)
raise RuntimeError(error_msg) from e
except Exception as e:
error_msg = f"Unexpected error during vfio binding: {e}"
logger.error(error_msg)
raise RuntimeError(error_msg) from e
def restore_original_driver(bdf: str, original_driver: Optional[str]) -> None:
"""Restore the original driver binding for the PCIe device with enhanced error handling."""
if not validate_bdf_format(bdf):
logger.warning(f"Invalid BDF format during restore: {bdf}")
return
logger.info(
f"Restoring original driver binding for {bdf} (target driver: {original_driver or 'none'})"
)
print("[*] Restoring original driver binding...")
try:
# Check if device exists
device_path = f"/sys/bus/pci/devices/{bdf}"
if not os.path.exists(device_path):
logger.warning(
f"Device {bdf} not found during restore, may have been removed"
)
return
# Check current driver state
current_driver = get_current_driver(bdf)
logger.debug(f"Current driver for {bdf}: {current_driver or 'none'}")
# Unbind from vfio-pci if currently bound
if current_driver == "vfio-pci":
logger.debug(f"Unbinding {bdf} from vfio-pci")
max_retries = 3
unbind_successful = False
for attempt in range(max_retries):
try:
# Use direct file writing instead of shell redirection
unbind_path = "/sys/bus/pci/drivers/vfio-pci/unbind"
with open(unbind_path, "w") as f:
f.write(f"{bdf}\n")
# Wait for unbind to complete
if _wait_for_device_state(bdf, None, max_retries=3):
logger.info(f"Successfully unbound {bdf} from vfio-pci")
unbind_successful = True
break
elif attempt < max_retries - 1:
logger.warning(
f"Device still bound to vfio-pci (attempt {attempt + 1}), retrying..."
)
import time
time.sleep(1)
except (OSError, IOError) as e:
if "No such device" in str(e) or "No such file or directory" in str(
e
):
logger.info(f"Device {bdf} already unbound from vfio-pci")
unbind_successful = True
break
elif attempt < max_retries - 1:
logger.warning(
f"Failed to unbind from vfio-pci (attempt {attempt + 1}): {e}, retrying..."
)
import time
time.sleep(1)
else:
logger.warning(
f"Failed to unbind from vfio-pci after {max_retries} attempts: {e}"
)
# Continue with restore attempt anyway
break
if not unbind_successful and get_current_driver(bdf) == "vfio-pci":
logger.warning(
f"Device {bdf} still bound to vfio-pci, restore may fail"
)
else:
logger.info(
f"Device {bdf} not bound to vfio-pci (current: {current_driver or 'none'})"
)
# Bind back to original driver if it existed
if original_driver:
# Check if original driver is available
driver_path = f"/sys/bus/pci/drivers/{original_driver}"
if not os.path.exists(driver_path):
logger.warning(
f"Original driver {original_driver} not available for restore"
)
print(f"Warning: Original driver {original_driver} not available")
return
logger.debug(f"Binding {bdf} back to {original_driver}")
max_retries = 3
restore_successful = False
for attempt in range(max_retries):
try:
# Use direct file writing instead of shell redirection
bind_path = f"/sys/bus/pci/drivers/{original_driver}/bind"
with open(bind_path, "w") as f:
f.write(f"{bdf}\n")
# Verify restore was successful
if _wait_for_device_state(bdf, original_driver, max_retries=3):
logger.info(f"Successfully restored {bdf} to {original_driver}")
print(f"[✓] Device restored to {original_driver} driver")
restore_successful = True
break
elif attempt < max_retries - 1:
logger.warning(
f"Restore command succeeded but device not bound to {original_driver} (attempt {attempt + 1}), retrying..."
)
import time
time.sleep(1)
except (OSError, IOError) as e:
if "Device or resource busy" in str(e):
logger.warning(
f"Device {bdf} is busy during restore (attempt {attempt + 1})"
)
if attempt < max_retries - 1:
import time
time.sleep(2)
continue
elif "No such device" in str(
e
) or "No such file or directory" in str(e):
logger.warning(f"Device {bdf} not found during restore")
break
elif attempt < max_retries - 1:
logger.warning(
f"Failed to restore to {original_driver} (attempt {attempt + 1}): {e}, retrying..."
)
import time
time.sleep(1)
else:
logger.warning(
f"Failed to restore to {original_driver} after {max_retries} attempts: {e}"
)
break
if not restore_successful:
final_driver = get_current_driver(bdf)
if final_driver == original_driver:
logger.info(
f"Device {bdf} is bound to {original_driver} despite restore errors"
)
print(f"[✓] Device is bound to {original_driver} driver")
else:
logger.warning(
f"Failed to restore {bdf} to {original_driver}, current driver: {final_driver or 'none'}"
)
print(
f"Warning: Failed to restore to {original_driver}, current driver: {final_driver or 'none'}"
)
else:
logger.info(f"No original driver to restore for {bdf}")
print("[*] No original driver to restore")
except (OSError, IOError) as e:
logger.warning(f"Failed to restore original driver for {bdf}: {e}")
print(f"Warning: Failed to restore original driver: {e}")
except Exception as e:
logger.warning(f"Unexpected error during driver restore for {bdf}: {e}")
print(f"Warning: Unexpected error during driver restore: {e}")
def _validate_vfio_device_access(vfio_device: str, bdf: str) -> None:
"""Validate VFIO device access and permissions."""
# Check if VFIO device exists
if not os.path.exists(vfio_device):
error_msg = f"VFIO device {vfio_device} not found"
logger.error(error_msg)
raise RuntimeError(error_msg)
# Check if /dev/vfio/vfio exists (VFIO container device)
vfio_container = "/dev/vfio/vfio"
if not os.path.exists(vfio_container):
error_msg = f"VFIO container device {vfio_container} not found"
logger.error(error_msg)
raise RuntimeError(error_msg)
# Check device permissions
try:
import stat
vfio_stat = os.stat(vfio_device)
if not (vfio_stat.st_mode & stat.S_IRGRP) or not (
vfio_stat.st_mode & stat.S_IWGRP
):
logger.warning(
f"VFIO device {vfio_device} may not have proper group permissions"
)
except OSError as e:
logger.warning(f"Could not check VFIO device permissions: {e}")
# Verify device is actually bound to vfio-pci
current_driver = get_current_driver(bdf)
if current_driver != "vfio-pci":
error_msg = (
f"Device {bdf} not bound to vfio-pci (current: {current_driver or 'none'})"
)
logger.error(error_msg)
raise RuntimeError(error_msg)
def _validate_container_environment() -> None:
"""Validate container runtime environment."""
# Check if podman is available
if shutil.which("podman") is None:
error_msg = "Podman not found in PATH. Please install Podman container runtime."
logger.error(error_msg)
raise RuntimeError(error_msg)
# Check if container image exists
try:
result = run_command(
"podman images --format '{{.Repository}}:{{.Tag}}' | grep '^pcileech-fw-generator:'",
timeout=10,
)
if not result:
# Container image not found, try to build it automatically
logger.info(
"Container image 'pcileech-fw-generator' not found. Building it now..."
)
print(
"[*] Container image 'pcileech-fw-generator' not found. Building it now..."
)
try:
# Use the proper build script which handles the container building correctly
build_script_path = "scripts/build_container.sh"
if os.path.exists(build_script_path):
logger.info("Using build script for container creation")
build_result = subprocess.run(
f"bash {build_script_path} --tag pcileech-fw-generator:latest",
shell=True,
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
else:
# Fallback to direct container build
build_result = subprocess.run(
"podman build -t pcileech-fw-generator:latest -f Containerfile .",
shell=True,
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
logger.info("Container image built successfully")
print("[✓] Container image built successfully")
except subprocess.CalledProcessError as e:
error_msg = f"Failed to build container image automatically: {e.stderr}\nPlease build manually with: make container"
logger.error(error_msg)
raise RuntimeError(error_msg)
except subprocess.CalledProcessError:
# If we can't check, try to build anyway
logger.info("Could not check container image status. Attempting to build...")
print("[*] Could not check container image status. Attempting to build...")
try:
build_script_path = "scripts/build_container.sh"
if os.path.exists(build_script_path):
logger.info("Using build script for container creation")
build_result = subprocess.run(
f"bash {build_script_path} --tag pcileech-fw-generator:latest",
shell=True,
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
else:
# Fallback to direct container build
build_result = subprocess.run(
"podman build -t pcileech-fw-generator:latest -f Containerfile .",
shell=True,
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
logger.info("Container image built successfully")
print("[✓] Container image built successfully")
except subprocess.CalledProcessError as e:
error_msg = f"Failed to build container image: {e.stderr}\nPlease build manually with: make container"
logger.error(error_msg)
raise RuntimeError(error_msg)
def run_build_container(
bdf: str, board: str, vfio_device: str, args: argparse.Namespace
) -> None:
"""Run the firmware build in a Podman container with enhanced validation and error handling."""
if not validate_bdf_format(bdf):
raise ValueError(
f"Invalid BDF format: {bdf}. Expected format: DDDD:BB:DD.F (e.g., 0000:03:00.0)"
)
# Log advanced features being used
advanced_features = []
if args.advanced_sv:
advanced_features.append("Advanced SystemVerilog Generation")
if args.enable_variance:
advanced_features.append("Manufacturing Variance Simulation")
if args.device_type != "generic":
advanced_features.append(f"Device-specific optimizations ({args.device_type})")
if advanced_features:
logger.info(f"Advanced features enabled: {', '.join(advanced_features)}")
print(f"[*] Advanced features: {', '.join(advanced_features)}")
logger.info(f"Starting container build for device {bdf} on board {board}")
# Validate container environment
_validate_container_environment()
# Ensure output directory exists with proper permissions
output_dir = "output"
os.makedirs(output_dir, exist_ok=True)
# Check output directory permissions
if not os.access(output_dir, os.W_OK):
error_msg = f"Output directory {output_dir} is not writable"
logger.error(error_msg)
raise RuntimeError(error_msg)
# Validate VFIO device access
_validate_vfio_device_access(vfio_device, bdf)
# Build the build.py command with all arguments - use modular build system if available
build_cmd_parts = [f"sudo python3 /app/src/build.py --bdf {bdf} --board {board}"]
# Add advanced features arguments
if args.advanced_sv:
build_cmd_parts.append("--advanced-sv")
if args.device_type != "generic":
build_cmd_parts.append(f"--device-type {args.device_type}")
if args.enable_variance:
build_cmd_parts.append("--enable-variance")
if args.disable_power_management:
build_cmd_parts.append("--disable-power-management")
if args.disable_error_handling:
build_cmd_parts.append("--disable-error-handling")
if args.disable_performance_counters:
build_cmd_parts.append("--disable-performance-counters")
if args.behavior_profile_duration != 30:
build_cmd_parts.append(
f"--behavior-profile-duration {args.behavior_profile_duration}"
)
build_cmd = " ".join(build_cmd_parts)
# Construct Podman command
container_cmd = textwrap.dedent(
f"""
podman run --rm -it --privileged \
--device={vfio_device} \
--device=/dev/vfio/vfio \
-v {os.getcwd()}/output:/app/output \
pcileech-fw-generator:latest \
{build_cmd}
"""
).strip()
logger.debug(f"Container command: {container_cmd}")
print("[*] Launching build container...")
start_time = time.time()
try:
subprocess.run(container_cmd, shell=True, check=True)
elapsed_time = time.time() - start_time
logger.info(f"Build completed successfully in {elapsed_time:.1f} seconds")
print(f"[✓] Build completed in {elapsed_time:.1f} seconds")
except subprocess.CalledProcessError as e:
elapsed_time = time.time() - start_time
error_msg = f"Container build failed after {elapsed_time:.1f} seconds: {e}"
logger.error(error_msg)
raise RuntimeError(error_msg) from e
except Exception as e:
elapsed_time = time.time() - start_time
error_msg = f"Unexpected error during container build after {elapsed_time:.1f} seconds: {e}"
logger.error(error_msg)
raise RuntimeError(error_msg) from e
def ensure_git_repo(repo_url: str, local_dir: str, update: bool = False) -> str:
"""
Ensure that the git repository is available locally.
Args:
repo_url (str): URL of the git repository
local_dir (str): Local directory to clone/pull the repository
update (bool): Whether to update the repository if it already exists
Returns:
str: Path to the local repository
"""
# Create cache directory if it doesn't exist
os.makedirs(os.path.dirname(local_dir), exist_ok=True)
# Check if repository already exists as a valid git repository
if os.path.exists(os.path.join(local_dir, ".git")):
logger.info(f"Repository already exists at {local_dir}")
# Update repository if requested
if update:
try:
logger.info(f"Updating repository at {local_dir}")
print(f"[*] Updating repository at {local_dir}")
# Get current directory
current_dir = os.getcwd()
# Change to repository directory
os.chdir(local_dir)
# Pull latest changes
result = subprocess.run(
"git pull",
shell=True,
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
# Change back to original directory
os.chdir(current_dir)
logger.info(f"Repository updated successfully: {result.stdout.strip()}")
print(f"[✓] Repository updated successfully")
except subprocess.CalledProcessError as e:
logger.warning(f"Failed to update repository: {e.stderr}")
print(f"[!] Warning: Failed to update repository: {e.stderr}")
else:
# Check if directory exists but is not a git repository
if os.path.exists(local_dir):
logger.info(f"Directory exists but is not a git repository: {local_dir}")
print(f"[*] Removing existing directory: {local_dir}")
# Remove the directory to allow fresh clone
import shutil
try:
shutil.rmtree(local_dir)
except Exception as e:
logger.warning(f"Failed to remove directory: {e}")
print(f"[!] Warning: Failed to remove directory: {e}")
# Continue anyway, git clone might still work
# Clone repository
try:
logger.info(f"Cloning repository {repo_url} to {local_dir}")
print(f"[*] Cloning repository {repo_url} to {local_dir}")
result = subprocess.run(
f"git clone {repo_url} {local_dir}",
shell=True,
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
logger.info(f"Repository cloned successfully")
print(f"[✓] Repository cloned successfully")
except subprocess.CalledProcessError as e:
error_msg = f"Failed to clone repository: {e.stderr}"
logger.error(error_msg)
raise RuntimeError(error_msg)
# Return path to repository
return local_dir
def validate_environment() -> None:
"""Validate that the environment is properly set up."""
if os.geteuid() != 0:
error_msg = "This script requires root privileges. Run with sudo."
logger.error(error_msg)
raise RuntimeError(error_msg)
# Check if git is available
if shutil.which("git") is None:
error_msg = "Git not found in PATH. Please install Git first."
logger.error(error_msg)
raise RuntimeError(error_msg)
# Check if Podman is available
if shutil.which("podman") is None:
error_msg = "Podman not found in PATH. Please install Podman first."
logger.error(error_msg)
raise RuntimeError(error_msg)
# Check if Vivado is available
try:
# Import vivado_utils from src directory
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent / "src"))
from src.vivado_utils import find_vivado_installation, get_vivado_search_paths
vivado_info = find_vivado_installation()
if vivado_info:
logger.info(
f"Found Vivado {vivado_info['version']} at {vivado_info['path']}"
)
print(f"[✓] Vivado {vivado_info['version']} detected")
else:
# Show what paths were checked using the utility function
checked_paths = get_vivado_search_paths()
error_msg = f"Vivado not found. Checked paths:\n" + "\n".join(
f"{path}" for path in checked_paths
)
logger.error(error_msg)
print(f"[✗] {error_msg}")
# Allow user to skip
try:
response = (
input("\nWould you like to continue without Vivado? (y/N): ")
.strip()
.lower()
)
if response in ["y", "yes"]:
print("[!] Continuing without Vivado - some features may not work")
logger.warning("User chose to continue without Vivado")
else:
raise RuntimeError(
"Vivado is required. Please install Vivado and try again."
)
except (KeyboardInterrupt, EOFError):
raise RuntimeError(
"Vivado is required. Please install Vivado and try again."
)
except ImportError:
error_msg = (
"Could not import vivado_utils. Please ensure Vivado is properly installed."
)
logger.error(error_msg)
print(f"[✗] {error_msg}")
# Allow user to skip
try:
response = (
input("\nWould you like to continue without Vivado? (y/N): ")
.strip()
.lower()
)
if response in ["y", "yes"]:
print("[!] Continuing without Vivado - some features may not work")
logger.warning("User chose to continue without Vivado")
else:
raise RuntimeError(
"Vivado is required. Please install Vivado and try again."
)
except (KeyboardInterrupt, EOFError):
raise RuntimeError(
"Vivado is required. Please install Vivado and try again."
)
# Check if container image exists
try:
result = run_command(
"podman images pcileech-fw-generator --format '{{.Repository}}'"
)
if "pcileech-fw-generator" not in result:
# Container image not found, try to build it
logger.info(
"Container image 'pcileech-fw-generator' not found. Building it now..."
)
print(
"[*] Container image 'pcileech-fw-generator' not found. Building it now..."
)
try:
# Use the proper build script which handles the container building correctly
build_script_path = "scripts/build_container.sh"
if os.path.exists(build_script_path):
logger.info("Using build script for container creation")
build_result = subprocess.run(
f"bash {build_script_path} --tag pcileech-fw-generator:latest",
shell=True,
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
else:
# Fallback to direct container build
build_result = subprocess.run(
"podman build -t pcileech-fw-generator:latest -f Containerfile .",
shell=True,
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
logger.info("Container image built successfully")
print("[✓] Container image built successfully")
except subprocess.CalledProcessError as e:
error_msg = f"Failed to build container image: {e.stderr}"
logger.error(error_msg)
raise RuntimeError(error_msg)
except Exception as e:
error_msg = f"Error checking container image: {str(e)}"
logger.error(error_msg)
raise RuntimeError(error_msg)
def main() -> int:
"""Main entry point for the firmware generator"""
bdf = None
original_driver = None
try:
logger.info("Starting PCILeech firmware generation process")
validate_environment()
# Ensure pcileech-fpga repository is available
repo_dir = os.path.join(REPO_CACHE_DIR, "pcileech-fpga")
pcileech_fpga_dir = ensure_git_repo(PCILEECH_FPGA_REPO, repo_dir, update=False)
logger.info(f"Using pcileech-fpga repository at {pcileech_fpga_dir}")
# Parse command line arguments
parser = argparse.ArgumentParser(
description="Generate DMA firmware from donor PCIe device",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=textwrap.dedent(
"""
Examples:
# Basic usage
sudo python3 generate.py --board 75t
sudo python3 generate.py --board 100t --flash
# Advanced SystemVerilog generation
sudo python3 generate.py --board 75t --advanced-sv --device-type network
# Manufacturing variance simulation
sudo python3 generate.py --board 100t --enable-variance --behavior-profile-duration 60
# Advanced features with selective disabling
sudo python3 generate.py --board 75t --advanced-sv --disable-power-management --disable-error-handling
# Full advanced configuration
sudo python3 generate.py --board 100t --advanced-sv --device-type storage --enable-variance --behavior-profile-duration 45 --flash
"""
),
)
# Basic options
parser.add_argument(
"--tui",
action="store_true",
help="Launch TUI (Text User Interface) mode",
)
parser.add_argument(
"--flash",
action="store_true",
help="Flash output/firmware.bin with usbloader after build",
)
parser.add_argument(
"--board",
choices=[
# Original boards
"35t",
"75t",
"100t",
# CaptainDMA boards
"pcileech_75t484_x1",
"pcileech_35t484_x1",
"pcileech_35t325_x4",
"pcileech_35t325_x1",
"pcileech_100t484_x1",
# Other boards
"pcileech_enigma_x1",
"pcileech_squirrel",
"pcileech_pciescreamer_xc7a35",
],
default="35t",
help="Target FPGA board type (default: 35t/Squirrel)",
)
# Advanced SystemVerilog Generation
parser.add_argument(
"--advanced-sv",
action="store_true",
help="Enable advanced SystemVerilog generation with enhanced features",
)
parser.add_argument(
"--device-type",
choices=["network", "storage", "graphics", "audio", "generic"],
default="generic",
help="Device type for specialized optimizations (default: generic)",
)
# Manufacturing Variance Simulation
parser.add_argument(
"--enable-variance",
action="store_true",
help="Enable manufacturing variance simulation for realistic timing",
)
# Feature Control
parser.add_argument(
"--disable-power-management",
action="store_true",
help="Disable power management features in advanced generation",
)
parser.add_argument(
"--disable-error-handling",
action="store_true",
help="Disable error handling features in advanced generation",
)
parser.add_argument(
"--disable-performance-counters",
action="store_true",
help="Disable performance counter features in advanced generation",
)
# Behavior Profiling
parser.add_argument(
"--behavior-profile-duration",
type=int,
default=30,
help="Duration for behavior profiling in seconds (default: 30)",
)
# Donor dump functionality
parser.add_argument(
"--donor-dump",
action="store_true",
help="Extract donor device parameters using kernel module before generation",
)
parser.add_argument(
"--auto-install-headers",
action="store_true",
help="Automatically install kernel headers if missing (for donor dump)",
)
args = parser.parse_args()
# Check if TUI mode is requested
if args.tui:
try:
# Import and launch TUI
from src.tui.main import PCILeechTUI
app = PCILeechTUI()
app.run()
return 0
except ImportError:
error_msg = "TUI dependencies not installed. Install with: pip install textual rich psutil"
logger.error(error_msg)
print(f"[✗] {error_msg}")
return 1
except Exception as e:
error_msg = f"Failed to launch TUI: {e}"
logger.error(error_msg)
print(f"[✗] {error_msg}")
return 1
# Enhanced logging with advanced features
config_info = [f"board={args.board}", f"flash={args.flash}"]
if args.advanced_sv:
config_info.append("advanced_sv=True")
if args.device_type != "generic":
config_info.append(f"device_type={args.device_type}")
if args.enable_variance:
config_info.append("variance=True")
if args.disable_power_management:
config_info.append("no_power_mgmt=True")
if args.disable_error_handling:
config_info.append("no_error_handling=True")
if args.disable_performance_counters:
config_info.append("no_perf_counters=True")
if args.behavior_profile_duration != 30:
config_info.append(f"profile_duration={args.behavior_profile_duration}s")
logger.info(f"Configuration: {', '.join(config_info)}")
# List and select PCIe device
devices = list_pci_devices()
if not devices:
error_msg = "No PCIe devices found"
logger.error(error_msg)
raise RuntimeError(error_msg)
selected_device = choose_device(devices)
bdf = selected_device["bdf"]
vendor = selected_device["ven"]
device = selected_device["dev"]
logger.info(f"Selected device: {bdf} (VID:{vendor} DID:{device})")
print(f"\nSelected device: {bdf} (VID:{vendor} DID:{device})")
# Get device information
iommu_group = get_iommu_group(bdf)
vfio_device = f"/dev/vfio/{iommu_group}"
original_driver = get_current_driver(bdf)
logger.info(
f"Device info - IOMMU group: {iommu_group}, Current driver: {original_driver or 'none'}"
)
print(f"IOMMU group: {iommu_group}")
print(f"Current driver: {original_driver or 'none'}")
# Extract donor device parameters if requested
donor_info = None
if args.donor_dump:
if DonorDumpManager is None:
error_msg = "Donor dump functionality not available. Check src/donor_dump_manager.py"
logger.error(error_msg)
print(f"[✗] {error_msg}")
return 1
try:
print(f"\n[•] Extracting donor device parameters for {bdf}...")
logger.info(f"Starting donor dump extraction for {bdf}")
dump_manager = DonorDumpManager()
donor_info = dump_manager.setup_module(
bdf, auto_install_headers=args.auto_install_headers
)
print("[✓] Donor device parameters extracted successfully")
logger.info("Donor dump extraction completed successfully")
# Log key parameters
key_params = [
"vendor_id",
"device_id",
"class_code",
"bar_size",
"mpc",
"mpr",
]
for param in key_params:
if param in donor_info:
logger.info(f" {param}: {donor_info[param]}")
# Save donor info to file for container use
import json
donor_info_path = pathlib.Path("output/donor_info.json")
donor_info_path.parent.mkdir(exist_ok=True)
with open(donor_info_path, "w") as f:
json.dump(donor_info, f, indent=2)
logger.info(f"Donor info saved to {donor_info_path}")
except DonorDumpError as e:
error_msg = f"Donor dump failed: {e}"
logger.error(error_msg)
print(f"[✗] {error_msg}")
# Ask user if they want to continue without donor dump
response = input("Continue without donor dump? [y/N]: ").strip().lower()
if response not in ["y", "yes"]:
return 1
print("[•] Continuing without donor dump...")
except Exception as e:
error_msg = f"Unexpected error during donor dump: {e}"
logger.error(error_msg)
print(f"[✗] {error_msg}")
return 1
# Bind device to vfio-pci
bind_to_vfio(bdf, vendor, device, original_driver)
# Run the build container
run_build_container(bdf, args.board, vfio_device, args)
# Flash firmware if requested
if args.flash:
firmware_path = pathlib.Path("output/firmware.bin")
if not firmware_path.exists():
error_msg = "ERROR: firmware.bin not found in ./output directory"
logger.error(error_msg)
raise RuntimeError(error_msg)
flash_firmware(firmware_path)
logger.info("Firmware generation process completed successfully")
print("[✓] Process completed successfully")
return 0
except KeyboardInterrupt:
logger.warning("Process interrupted by user")
print("\n[!] Process interrupted by user")
# Don't use sys.exit() - let finally block handle cleanup
return 1
except Exception as e:
logger.error(f"Fatal error during firmware generation: {e}")
print(f"\n[✗] Fatal error: {e}")
# Don't use sys.exit() - let finally block handle cleanup
return 1
finally:
# Always attempt to restore the original driver if we have the info
if bdf and original_driver is not None:
try:
restore_original_driver(bdf, original_driver)
logger.info("Driver restoration completed successfully")
except Exception as e:
logger.error(f"Failed to restore driver during cleanup: {e}")
print(f"[!] Warning: Failed to restore driver during cleanup: {e}")
# Ensure any temporary files are cleaned up
try:
# Clean up any temporary VFIO state if needed
logger.debug("Cleanup completed")
except Exception as e:
logger.warning(f"Minor cleanup issue: {e}")
if __name__ == "__main__":
sys.exit(main())