Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,5 @@ site/site/
.github/copilot-instructions.md
template_variables_report.json
src/file_management/donor_info.json
message.txt
BUILD_LOG_ANALYSIS.md
4 changes: 4 additions & 0 deletions Containerfile
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ RUN pip3 install --no-cache-dir -r requirements.txt -r requirements-tui.txt
COPY src ./src
COPY configs ./configs
COPY pcileech.py .
COPY pyproject.toml setup.py setup.cfg ./

# Install the package itself so `from pcileechfwgenerator.xxx` imports work
RUN pip3 install --no-cache-dir -e . || echo "Warning: Editable install failed, trying regular install" && pip3 install --no-cache-dir .
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The || operator in this RUN command will mask installation failures. If the editable install (pip3 install -e .) fails, the echo statement will succeed (exit code 0), and the subsequent regular install command will never execute because the || operator short-circuits on success. This should use || followed by the actual install command without the echo, or better yet, use ; if [ $? -ne 0 ]; then to properly chain the fallback installation.

Suggested change
RUN pip3 install --no-cache-dir -e . || echo "Warning: Editable install failed, trying regular install" && pip3 install --no-cache-dir .
RUN pip3 install --no-cache-dir -e . || (echo "Warning: Editable install failed, trying regular install" >&2 && pip3 install --no-cache-dir .)

Copilot uses AI. Check for mistakes.

# Copy voltcyclone-fpga from build stage (cloned during build)
COPY --from=build /src/lib/voltcyclone-fpga ./lib/voltcyclone-fpga
Expand Down
13 changes: 13 additions & 0 deletions configs/fallbacks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,19 @@ critical_variables:
- device.class_code # PCI Class Code (e.g., 020000 for network devices)
- device.subsys_vendor_id # PCI Subsystem Vendor ID
- device.subsys_device_id # PCI Subsystem Device ID

# Timing configuration (MUST come from device profiling)
# Using fallbacks would create fingerprintable patterns
- timing_config.min_latency_cycles
- timing_config.max_latency_cycles
- timing_config.avg_latency_cycles
- timing_config.min_read_latency
- timing_config.max_read_latency
- timing_config.avg_read_latency

Comment on lines +26 to +34
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This configuration has a logical inconsistency. The file declares timing_config values as critical variables (lines 28-33) that MUST NOT have fallbacks, yet unified_context.py provides DEFAULT_TIMING_CONFIG with default values for these same parameters (lines 70-74), and the template in tlp_latency_emulator.sv.j2 uses these values with the default() filter (lines 57, 60).

Either:

  1. Remove these timing values from the critical_variables list if defaults are acceptable
  2. Remove the defaults from DEFAULT_TIMING_CONFIG and update templates to not use the default() filter for these values

The current state sends mixed signals about whether these values require hardware profiling or can use defaults.

Suggested change
# Timing configuration (MUST come from device profiling)
# Using fallbacks would create fingerprintable patterns
- timing_config.min_latency_cycles
- timing_config.max_latency_cycles
- timing_config.avg_latency_cycles
- timing_config.min_read_latency
- timing_config.max_read_latency
- timing_config.avg_read_latency

Copilot uses AI. Check for mistakes.
# Template context device identifiers
- vendor_id_int # Integer form of vendor ID
- device_id_int # Integer form of device ID

# Safe fallback values for non-critical variables
# These will be used only if the variable is missing
Expand Down
26 changes: 15 additions & 11 deletions install-sudo-wrapper.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ log_error() {
check_files() {
local missing_files=()

if [ ! -f "pcileech-build-sudo" ]; then
missing_files+=("pcileech-build-sudo")
if [ ! -f "pcileech-sudo" ]; then
missing_files+=("pcileech-sudo")
fi

if [ ${#missing_files[@]} -ne 0 ]; then
Expand Down Expand Up @@ -59,14 +59,18 @@ if ! check_files; then
fi

# Copy the wrapper script to the installation directory
log_info "Installing pcileech-build-sudo..."
cp pcileech-build-sudo "$INSTALL_DIR/"
log_info "Installing pcileech-sudo..."
cp pcileech-sudo "$INSTALL_DIR/"
chmod +x "$INSTALL_DIR/pcileech-sudo"

# Also install as pcileech-build-sudo for backwards compatibility
ln -sf "$INSTALL_DIR/pcileech-sudo" "$INSTALL_DIR/pcileech-build-sudo" 2>/dev/null || \
cp pcileech-sudo "$INSTALL_DIR/pcileech-build-sudo"
Comment on lines +67 to +68
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The symlink command on line 67 has an issue. When using ln -sf, the source argument should be relative to the target location or an absolute path. Currently, it's using $INSTALL_DIR/pcileech-sudo as the source, which means the symlink will point to itself (since the target is also in $INSTALL_DIR). The correct source should be just pcileech-sudo (relative to the target directory) or it should be created with ln -sf pcileech-sudo "$INSTALL_DIR/pcileech-build-sudo" after changing to $INSTALL_DIR.

Copilot uses AI. Check for mistakes.
chmod +x "$INSTALL_DIR/pcileech-build-sudo"

log_info "Installed pcileech sudo wrapper to $INSTALL_DIR"
log_info "You can now run builds with sudo using: pcileech-build-sudo build --bdf <device> --board <board>"
log_warning "Note: The wrapper now uses the unified pcileech.py entrypoint"
log_warning "Legacy usage is supported but consider using: sudo python3 pcileech.py build ..."
log_info "You can now run commands with sudo using: pcileech-sudo <command> [args]"
log_warning "Note: The wrapper uses the unified pcileech.py entrypoint"

# Add the directory to PATH if it's not already there
if [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then
Expand Down Expand Up @@ -106,9 +110,9 @@ fi
log_info "Installation complete!"
log_info ""
log_info "Usage examples:"
log_info " pcileech-build-sudo build --bdf 0000:03:00.0 --board pcileech_35t325_x1"
log_info " pcileech-build-sudo tui"
log_info " pcileech-build-sudo check --device 0000:03:00.0"
log_info " pcileech-sudo build --bdf 0000:03:00.0 --board pcileech_35t325_x1"
log_info " pcileech-sudo check --device 0000:03:00.0"
log_info " pcileech-sudo tui"
log_info ""
log_info "Alternative (recommended):"
log_info "Alternative (from project directory):"
log_info " sudo python3 pcileech.py build --bdf 0000:03:00.0 --board pcileech_35t325_x1"
57 changes: 57 additions & 0 deletions pcileech-sudo
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#!/bin/bash
# pcileech-sudo - Run pcileech commands with elevated privileges
#
# This wrapper script handles:
# 1. Automatic virtual environment detection
# 2. Running pcileech.py with sudo while preserving the Python environment
#
# Usage: pcileech-sudo <command> [args...]
# Example: pcileech-sudo build --bdf 0000:03:00.0 --board pcileech_35t325_x1
# pcileech-sudo check --device 0000:03:00.0
# pcileech-sudo tui

set -euo pipefail

# Find the script directory (where pcileech.py is located)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# Check if pcileech.py exists in the script directory
if [ ! -f "$SCRIPT_DIR/pcileech.py" ]; then
# Try to find it relative to installed location
if [ -f "/app/pcileech.py" ]; then
SCRIPT_DIR="/app"
else
echo "Error: Could not find pcileech.py in $SCRIPT_DIR or /app"
echo "Please run this script from the PCILeechFWGenerator directory"
exit 1
fi
fi

# Determine the Python interpreter to use
PYTHON_CMD=""

# Check for active virtual environment first
if [ -n "${VIRTUAL_ENV:-}" ]; then
PYTHON_CMD="$VIRTUAL_ENV/bin/python3"
elif [ -f "$SCRIPT_DIR/.venv/bin/python3" ]; then
# Check for local .venv
PYTHON_CMD="$SCRIPT_DIR/.venv/bin/python3"
elif [ -f "$HOME/.pcileech-venv/bin/python3" ]; then
# Check for user-level venv
PYTHON_CMD="$HOME/.pcileech-venv/bin/python3"
else
# Fall back to system Python
PYTHON_CMD="python3"
fi

# Verify the Python interpreter exists
if [ ! -x "$PYTHON_CMD" ] && ! command -v "$PYTHON_CMD" &> /dev/null; then
echo "Error: Python interpreter not found: $PYTHON_CMD"
echo "Please ensure Python 3 is installed and accessible"
exit 1
fi

# Execute pcileech.py with sudo, preserving the Python environment
# Using 'sudo -E' to preserve environment variables
cd "$SCRIPT_DIR"
exec sudo -E "$PYTHON_CMD" "$SCRIPT_DIR/pcileech.py" "$@"
18 changes: 18 additions & 0 deletions pcileech.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,24 @@
sys.path.insert(0, str(project_root))
sys.path.insert(0, str(project_root / "src"))

# Create pcileechfwgenerator namespace mapping for direct script execution
# This is needed because pyproject.toml maps pcileechfwgenerator -> src/
# but that only works when the package is installed via pip
_src_path = project_root / "src"
if _src_path.exists() and "pcileechfwgenerator" not in sys.modules:
import importlib.util

# Check if package is already installed (editable or regular install)
_spec = importlib.util.find_spec("pcileechfwgenerator")
if _spec is None:
# Package not installed, create the namespace mapping manually
# This allows "from pcileechfwgenerator.x import y" to work as "from src.x import y"
import types
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Module 'types' is imported with both 'import' and 'import from'.

Copilot uses AI. Check for mistakes.
_pkg = types.ModuleType("pcileechfwgenerator")
_pkg.__path__ = [str(_src_path)]
_pkg.__file__ = str(_src_path / "__init__.py")
sys.modules["pcileechfwgenerator"] = _pkg


def get_version():
"""Get the current version from the centralized version resolver."""
Expand Down
1 change: 1 addition & 0 deletions scripts/vfio_container_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
sys.path.insert(0, str(Path(__file__).parent.parent))

from src.cli.vfio_handler import (
DeviceInfo,
VFIOBinder,
VFIOBindError,
VFIOPermissionError,
Expand Down
57 changes: 45 additions & 12 deletions src/cli/vfio_diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,9 @@ class Diagnostics:
def __init__(self, device_bdf: Optional[str] = None):
self.device_bdf = device_bdf
self.checks: List[Check] = []
# Store device vendor/device IDs for remediation commands
self._vendor_id: Optional[str] = None
self._device_id: Optional[str] = None

# Public API --------------------------------------------------------------
def run(self) -> Report:
Expand Down Expand Up @@ -591,6 +594,9 @@ def _device_exists(self):
path_manager = VFIOPathManager(self.device_bdf)
if path_manager.device_path.exists():
vendor_id, device_id = path_manager.get_vendor_device_id()
# Store IDs for use in remediation commands (without 0x prefix)
self._vendor_id = vendor_id
self._device_id = device_id
# Add 0x prefix for display consistency with existing behavior
vendor = f"0x{vendor_id}"
device = f"0x{device_id}"
Expand Down Expand Up @@ -828,22 +834,49 @@ def _device_driver_binding(self):
commands=self._bind_commands(self.device_bdf, None),
)

@staticmethod
def _bind_commands(bdf: str, current: Optional[str]) -> List[str]:
cmds: list[str] = [
(
def _bind_commands(self, bdf: str, current: Optional[str]) -> List[str]:
"""Generate commands to bind device to vfio-pci driver.

Uses the new_id approach which is more robust:
1. Unbind from current driver (if any)
2. Register device IDs with vfio-pci via new_id (this auto-binds)

The new_id approach is preferred because:
- It works even when the device has no current driver
- It doesn't require the device to be pre-registered with vfio-pci
- It handles the binding automatically after registration
"""
cmds: list[str] = []

# Step 1: Unbind from current driver if one is bound
if current:
cmds.append(
safe_format(
"echo '{bdf}' | sudo tee /sys/bus/pci/devices/{bdf}/driver/unbind",
bdf=bdf,
)
if current
else ""
),
safe_format(
"echo '{bdf}' | sudo tee /sys/bus/pci/drivers/vfio-pci/bind", bdf=bdf
),
]
return [c for c in cmds if c]
)

# Step 2: Use new_id to register and auto-bind to vfio-pci
# This is the recommended approach and works regardless of prior driver state
if self._vendor_id and self._device_id:
cmds.append(
safe_format(
"echo '{vid} {did}' | sudo tee /sys/bus/pci/drivers/vfio-pci/new_id",
vid=self._vendor_id,
did=self._device_id,
)
)
else:
# Fallback to direct bind if we don't have device IDs
# This may fail if the device IDs aren't pre-registered
cmds.append(
safe_format(
"echo '{bdf}' | sudo tee /sys/bus/pci/drivers/vfio-pci/bind", bdf=bdf
)
)

return cmds

def _device_node(self):
link = Path(
Expand Down
3 changes: 3 additions & 0 deletions src/device_clone/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
TimingPattern,
)

# Import the module itself for patching purposes
from pcileechfwgenerator.device_clone import board_config

Comment on lines +25 to +27
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This import is problematic. Importing the module itself (from pcileechfwgenerator.device_clone import board_config) for "patching purposes" creates a circular dependency and is an anti-pattern. The module is already being imported on line 29 where specific functions are imported from it. If the intent is to allow mocking the module in tests, this should be done in the test files themselves, not in the production code. This import should be removed.

Suggested change
# Import the module itself for patching purposes
from pcileechfwgenerator.device_clone import board_config

Copilot uses AI. Check for mistakes.
# Core device cloning functionality
from pcileechfwgenerator.device_clone.board_config import (
get_board_info,
Expand Down
2 changes: 1 addition & 1 deletion src/templates/sv/pcileech_bar_impl_device.sv.j2
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ module pcileech_bar_impl_device #(

// Device-specific timing seed for jitter
// Uses vendor_id XOR device_id for deterministic but unique timing
localparam [15:0] INT_TIMING_SEED = {{ "16'h%04X" % ((vendor_id | 0x8086) ^ (device_id | 0x1234)) }};
localparam [15:0] INT_TIMING_SEED = {{ "16'h%04X" % ((vendor_id|bitor(0x8086))|bitxor(device_id|bitor(0x1234))) }};

// LFSR for interrupt timing jitter (matches tlp_latency_emulator approach)
reg [15:0] int_lfsr;
Expand Down
17 changes: 13 additions & 4 deletions src/templates/sv/pcileech_tlps128_bar_controller.sv.j2
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,17 @@ module pcileech_tlps128_bar_controller(
{% if timing_config is defined and timing_config and timing_config.get('enable_latency_jitter', False) %}
// ========================================================================
// TLP Latency Emulation (enabled via behavior profiling)
// SECURITY: All timing parameters must come from device profiling
// ========================================================================
{%- if not timing_config.get('min_read_latency') %}
{% error "timing_config.min_read_latency must be provided from device profiling" %}
{%- endif %}
{%- if not timing_config.get('max_read_latency') %}
{% error "timing_config.max_read_latency must be provided from device profiling" %}
{%- endif %}
{%- if not timing_config.get('avg_read_latency') %}
{% error "timing_config.avg_read_latency must be provided from device profiling" %}
{%- endif %}
wire [87:0] bar_latency_rsp_ctx[7];
wire [31:0] bar_latency_rsp_data[7];
wire bar_latency_rsp_valid[7];
Expand All @@ -103,10 +113,9 @@ module pcileech_tlps128_bar_controller(
generate
for (bar_idx = 0; bar_idx < 7; bar_idx = bar_idx + 1) begin : gen_latency_emulators
tlp_latency_emulator #(
.MIN_LATENCY_CYCLES({{ timing_config.get('min_read_latency', 10) }}),
.MAX_LATENCY_CYCLES({{ timing_config.get('max_read_latency', 50) }}),
.AVG_LATENCY_CYCLES({{ timing_config.get('avg_read_latency', 30) }}),
.LFSR_SEED({{ timing_config.get('latency_lfsr_seed', 0xACE1) }}),
.MIN_LATENCY_CYCLES({{ timing_config.get('min_read_latency') }}),
.MAX_LATENCY_CYCLES({{ timing_config.get('max_read_latency') }}),
.AVG_LATENCY_CYCLES({{ timing_config.get('avg_read_latency') }}),
.ENABLE_JITTER(1'b1)
) i_tlp_latency_emulator (
.rst ( rst ),
Expand Down
49 changes: 38 additions & 11 deletions src/templates/sv/tlp_latency_emulator.sv.j2
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,47 @@
`timescale 1ns / 1ps

module tlp_latency_emulator #(
parameter MIN_LATENCY_CYCLES = {{ timing_config.min_latency_cycles | default(10) }},
parameter MAX_LATENCY_CYCLES = {{ timing_config.max_latency_cycles | default(50) }},
parameter AVG_LATENCY_CYCLES = {{ timing_config.avg_latency_cycles | default(25) }},
// Timing parameters - MUST be provided from device profiling, no fallbacks
// Using hardcoded defaults would create fingerprintable patterns
{%- if not timing_config or timing_config.min_latency_cycles is not defined %}
{% error "timing_config.min_latency_cycles must be provided from device profiling - no fallback allowed" %}
{%- endif %}
{%- if not timing_config or timing_config.max_latency_cycles is not defined %}
{% error "timing_config.max_latency_cycles must be provided from device profiling - no fallback allowed" %}
{%- endif %}
{%- if not timing_config or timing_config.avg_latency_cycles is not defined %}
{% error "timing_config.avg_latency_cycles must be provided from device profiling - no fallback allowed" %}
{%- endif %}
parameter MIN_LATENCY_CYCLES = {{ timing_config.min_latency_cycles }},
parameter MAX_LATENCY_CYCLES = {{ timing_config.max_latency_cycles }},
parameter AVG_LATENCY_CYCLES = {{ timing_config.avg_latency_cycles }},
parameter ENABLE_JITTER = 1,
// Device-specific seed derived from vendor_id XOR device_id XOR timestamp hash
// Device-specific seed derived from vendor_id XOR device_id XOR build entropy
// This ensures each build has unique timing characteristics
parameter [31:0] PRNG_SEED_0 = 32'h{{ "%08X" % ((vendor_id_int | default(0x8086)) ^ (device_id_int | default(0x1234)) ^ 0xDEADBEEF) }},
parameter [31:0] PRNG_SEED_1 = 32'h{{ "%08X" % (((vendor_id_int | default(0x8086)) << 16) | (device_id_int | default(0x1234)) ^ 0xCAFEBABE) }},
parameter [31:0] PRNG_SEED_2 = 32'h{{ "%08X" % ((device_id_int | default(0x1234)) ^ (subsystem_id_int | default(0x0000)) ^ 0xFEEDFACE) }},
parameter [31:0] PRNG_SEED_3 = 32'h{{ "%08X" % ((vendor_id_int | default(0x8086)) ^ 0xBAADF00D) }},
// SECURITY: These values MUST come from actual hardware - no fallbacks allowed
{%- if vendor_id_int is not defined %}
{% error "vendor_id_int must be provided from hardware - no fallback allowed to prevent fingerprinting" %}
{%- endif %}
{%- if device_id_int is not defined %}
{% error "device_id_int must be provided from hardware - no fallback allowed to prevent fingerprinting" %}
{%- endif %}
{%- set vid = vendor_id_int %}
{%- set did = device_id_int %}
{%- set ssid = subsystem_id_int | default(0x0000) %}
// Build entropy from timestamp/random source (provided by context builder)
{%- set build_entropy = build_entropy_seed | default(range(1, 2147483647) | random) %}
// PRNG seeds use device-specific values mixed with build entropy
// No well-known magic constants - each combination is unique
parameter [31:0] PRNG_SEED_0 = 32'h{{ "%08X" % ((vid | bitxor(did)) | bitxor(build_entropy)) }},
parameter [31:0] PRNG_SEED_1 = 32'h{{ "%08X" % (((vid * 65536) | bitor(did)) | bitxor((build_entropy * 31) % 0xFFFFFFFF)) }},
parameter [31:0] PRNG_SEED_2 = 32'h{{ "%08X" % ((did | bitxor(ssid)) | bitxor((build_entropy * 127) % 0xFFFFFFFF)) }},
parameter [31:0] PRNG_SEED_3 = 32'h{{ "%08X" % (vid | bitxor((build_entropy * 8191) % 0xFFFFFFFF)) }},
// Burst correlation factor (0-255, higher = more correlated sequential timing)
parameter BURST_CORRELATION = {{ timing_config.burst_correlation | default(180) }},
// Derived from device ID to avoid fingerprinting with fixed default
parameter BURST_CORRELATION = {{ timing_config.burst_correlation | default(128 + (vid % 128)) }},
// Thermal drift rate (cycles between baseline adjustments)
parameter THERMAL_DRIFT_PERIOD = {{ timing_config.thermal_drift_period | default(65536) }}
// Device-specific to avoid fingerprinting
parameter THERMAL_DRIFT_PERIOD = {{ timing_config.thermal_drift_period | default(32768 + (did % 65536)) }}
)(
input wire clk,
input wire rst,
Expand Down Expand Up @@ -167,7 +194,7 @@ module tlp_latency_emulator #(
assign base_jitter = is_sequential ? correlated_latency : (prng_out[7:0] % latency_range);

// Apply thermal drift
assign thermal_adjusted = base_jitter + {{4{thermal_offset[3]}}, thermal_offset};
assign thermal_adjusted = base_jitter + {% raw %}{{4{thermal_offset[3]}}, thermal_offset}{% endraw %};

// Apply region-based adjustment
assign region_adjusted = thermal_adjusted + region_latency_adjust;
Expand Down
Loading
Loading