summaryrefslogtreecommitdiffstats
path: root/tools/ghidra/diff_gsp_firmware.py
diff options
context:
space:
mode:
authorOpen-IOV <dev@open-iov.org>2026-09-23 05:44:08 -0400
committerOpen-IOV <dev@open-iov.org>2026-09-23 05:46:43 -0400
commit2302fc09080bad51c73f293294e19fd4db464eb0 (patch)
treec722d186347fdef5a34131e9e143b1549b1daf8e /tools/ghidra/diff_gsp_firmware.py
downloadopen-iov-2302fc09080bad51c73f293294e19fd4db464eb0.tar.gz
open-iov-2302fc09080bad51c73f293294e19fd4db464eb0.zip
Open-IOV: initial public release
Open-IOV virtualizes NVIDIA GPUs so that unmodified guest drivers run with real GPU functionality - CUDA first, 3D and video to follow - on unmodified hosts, without a proprietary vGPU license and without patching any driver, firmware or hypervisor. Core principle: emulate the interface, pass through the work. kernel/ open-iov-vfio.ko - host VFIO mdev driver: virtual GPU device (PCI config space, BAR shadows, MSI-X), GSP boot emulation, GSP-RM message-queue/RPC emulation, memory model (GPU-VA to guest-PA discovery) and the semaphore-completion service that advances guest UVM progress. userspace/ libiov.so management API and examples. tools/ guest diagnostics, firmware analysis wrappers, RM probes. scripts/ stack manager, VM harness, build/load/test automation. config/ hardware/version profiles (the portability surface). docs/ documentation tree - start at docs/README.md. wiki/ long-form firmware and protocol deep dives. Status: initialization works end to end for a stock 570.144 Linux guest on Turing (TU104): driver probe, nvidia-smi, the full GSP-RM control battery, object creation and the early CUDA/UVM start-up path. GPU work forwarding - the step that makes CUDA kernels execute on real hardware - is the active work item; see docs/04-implementation/01-known-knowns-and-unknowns.md. License: BSD-3-Clause for the project; GPL-2.0 for kernel modules that link GPL-only kernel interfaces. See LICENSE and docs/09-reference/legal-and-licensing.md.
Diffstat (limited to 'tools/ghidra/diff_gsp_firmware.py')
-rw-r--r--tools/ghidra/diff_gsp_firmware.py220
1 files changed, 220 insertions, 0 deletions
diff --git a/tools/ghidra/diff_gsp_firmware.py b/tools/ghidra/diff_gsp_firmware.py
new file mode 100644
index 0000000..8145982
--- /dev/null
+++ b/tools/ghidra/diff_gsp_firmware.py
@@ -0,0 +1,220 @@
+#!/usr/bin/env python3
+"""
+Diff two GSP firmware versions.
+Usage: python3 diff_gsp_firmware.py <old_firmware> <new_firmware> <output_report>
+"""
+
+import sys
+import os
+import subprocess
+import json
+import hashlib
+
+def extract_fwimage(firmware_path, output_path):
+ """Extract .fwimage section from GSP firmware ELF."""
+ cmd = ["readelf", "-x", ".fwimage", firmware_path]
+ result = subprocess.run(cmd, capture_output=True, text=True)
+ # This is a hex dump, we need to parse it
+ # Better to use objcopy
+ cmd = ["objcopy", "-O", "binary", "-j", ".fwimage", firmware_path, output_path]
+ result = subprocess.run(cmd, capture_output=True)
+ return result.returncode == 0
+
+def extract_version(firmware_path):
+ """Extract version from .fwversion section."""
+ cmd = ["readelf", "-x", ".fwversion", firmware_path]
+ result = subprocess.run(cmd, capture_output=True, text=True)
+ # Parse hex dump
+ for line in result.stdout.split('\n'):
+ if '0x' in line and len(line.strip()) > 10:
+ parts = line.split()
+ if len(parts) >= 5:
+ hex_str = ''.join(parts[1:5])
+ try:
+ version = bytes.fromhex(hex_str).decode('ascii').strip('\x00')
+ return version
+ except:
+ pass
+ return "unknown"
+
+def run_ghidra_analysis(fwimage_path, project_dir, project_name):
+ """Run Ghidra analysis on fwimage."""
+ analyze_headless = "/snap/ghidra/47/ghidra/support/analyzeHeadless"
+ if not os.path.exists(analyze_headless):
+ analyze_headless = "/opt/ghidra/support/analyzeHeadless"
+
+ script_path = "/home/rmcore/Git/integdev_gpu_drv/open-iov/tools/ghidra/gsp-tu104-570.144/scripts"
+
+ cmd = [
+ analyze_headless,
+ project_dir,
+ project_name,
+ "-import", fwimage_path,
+ "-processor", "RISCV:LE:64:default",
+ "-cspec", "gcc",
+ "-overwrite",
+ "-scriptPath", script_path,
+ "-postScript", "analyze_gsp_fwimage.py"
+ ]
+
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=7200)
+ return result.returncode == 0
+
+def load_analysis_results(output_dir):
+ """Load analysis results from JSON files."""
+ results = {}
+ for fname in ["functions.json", "rpc_functions.json", "vgpu_strings.json",
+ "wpr_functions.json", "mem_functions.json", "irq_functions.json",
+ "analysis_summary.json"]:
+ path = os.path.join(output_dir, fname)
+ if os.path.exists(path):
+ with open(path, 'r') as f:
+ results[fname.replace('.json', '')] = json.load(f)
+ return results
+
+def diff_functions(old_funcs, new_funcs):
+ """Compare function lists."""
+ old_names = {f['name']: f for f in old_funcs}
+ new_names = {f['name']: f for f in new_funcs}
+
+ added = [f for name, f in new_names.items() if name not in old_names]
+ removed = [f for name, f in old_names.items() if name not in new_names]
+ modified = []
+
+ for name in set(old_names.keys()) & set(new_names.keys()):
+ if old_names[name]['size'] != new_names[name]['size'] or \
+ old_names[name]['entry'] != new_names[name]['entry']:
+ modified.append({
+ 'name': name,
+ 'old': old_names[name],
+ 'new': new_names[name]
+ })
+
+ return added, removed, modified
+
+def generate_diff_report(old_firmware, new_firmware, old_results, new_results, output_path):
+ """Generate markdown diff report."""
+ old_version = extract_version(old_firmware)
+ new_version = extract_version(new_firmware)
+
+ old_funcs = old_results.get('functions', [])
+ new_funcs = new_results.get('functions', [])
+
+ added, removed, modified = diff_functions(old_funcs, new_funcs)
+
+ old_rpc = {f['name'] for f in old_results.get('rpc_functions', [])}
+ new_rpc = {f['name'] for f in new_results.get('rpc_functions', [])}
+
+ rpc_added = new_rpc - old_rpc
+ rpc_removed = old_rpc - new_rpc
+
+ report = f"""# GSP Firmware Diff Report
+
+## Versions
+- **Old**: {old_version} ({os.path.basename(old_firmware)})
+- **New**: {new_version} ({os.path.basename(new_firmware)})
+
+## Function Changes
+- **Total Functions**: {len(old_funcs)} → {len(new_funcs)} ({len(new_funcs) - len(old_funcs):+d})
+- **Added**: {len(added)}
+- **Removed**: {len(removed)}
+- **Modified**: {len(modified)}
+
+## RPC Handler Changes
+- **Added RPC Functions**: {len(rpc_added)}
+- **Removed RPC Functions**: {len(rpc_removed)}
+
+## Added Functions
+"""
+ for f in added[:50]:
+ report += f"- `{f['name']}` at {f['entry']} (size: {f['size']})\n"
+
+ if len(added) > 50:
+ report += f"\n... and {len(added) - 50} more\n"
+
+ report += "\n## Removed Functions\n"
+ for f in removed[:50]:
+ report += f"- `{f['name']}` at {f['entry']} (size: {f['size']})\n"
+
+ if len(removed) > 50:
+ report += f"\n... and {len(removed) - 50} more\n"
+
+ report += "\n## Modified Functions\n"
+ for f in modified[:50]:
+ report += f"- `{f['name']}`: {f['old']['entry']}→{f['new']['entry']}, size {f['old']['size']}→{f['new']['size']}\n"
+
+ if len(modified) > 50:
+ report += f"\n... and {len(modified) - 50} more\n"
+
+ report += "\n## Added RPC Functions\n"
+ for name in sorted(rpc_added):
+ report += f"- `{name}`\n"
+
+ report += "\n## Removed RPC Functions\n"
+ for name in sorted(rpc_removed):
+ report += f"- `{name}`\n"
+
+ # vGPU strings comparison
+ old_strings = {s['value'] for s in old_results.get('vgpu_strings', [])}
+ new_strings = {s['value'] for s in new_results.get('vgpu_strings', [])}
+
+ report += "\n## vGPU String Changes\n"
+ report += f"- **Added**: {len(new_strings - old_strings)}\n"
+ report += f"- **Removed**: {len(old_strings - new_strings)}\n"
+
+ for s in sorted(new_strings - old_strings)[:20]:
+ report += f" + `{s}`\n"
+ for s in sorted(old_strings - new_strings)[:20]:
+ report += f" - `{s}`\n"
+
+ with open(output_path, 'w') as f:
+ f.write(report)
+
+ print(f"Diff report saved to {output_path}")
+
+def main():
+ if len(sys.argv) < 4:
+ print("Usage: diff_gsp_firmware.py <old_firmware> <new_firmware> <output_report>")
+ sys.exit(1)
+
+ old_firmware = sys.argv[1]
+ new_firmware = sys.argv[2]
+ output_report = sys.argv[3]
+
+ # Extract .fwimage sections
+ old_fwimage = "/tmp/old_fwimage.bin"
+ new_fwimage = "/tmp/new_fwimage.bin"
+
+ print("Extracting .fwimage sections...")
+ if not extract_fwimage(old_firmware, old_fwimage):
+ print("Failed to extract old .fwimage")
+ sys.exit(1)
+ if not extract_fwimage(new_firmware, new_fwimage):
+ print("Failed to extract new .fwimage")
+ sys.exit(1)
+
+ # Run Ghidra analysis on both
+ print("Analyzing old firmware...")
+ old_project_dir = "/tmp/ghidra_old"
+ if not run_ghidra_analysis(old_fwimage, old_project_dir, "gsp_old"):
+ print("Failed to analyze old firmware")
+ sys.exit(1)
+
+ print("Analyzing new firmware...")
+ new_project_dir = "/tmp/ghidra_new"
+ if not run_ghidra_analysis(new_fwimage, new_project_dir, "gsp_new"):
+ print("Failed to analyze new firmware")
+ sys.exit(1)
+
+ # Load results
+ old_results = load_analysis_results("/home/rmcore/Git/integdev_gpu_drv/open-iov/firmware/570-GSP")
+ # For new firmware, we'd need a separate output dir
+ new_results = load_analysis_results("/home/rmcore/Git/integdev_gpu_drv/open-iov/firmware/NEW-GSP")
+
+ # Generate diff report
+ generate_diff_report(old_firmware, new_firmware, old_results, new_results, output_report)
+
+ print("Done!")
+
+if __name__ == "__main__":
+ main() \ No newline at end of file