amd-psp-re
Reverse engineering, emulation and vulnerability research on the AMD Platform Security Processor (PSP) as shipped on Navi 31 (Radeon RX 7900 XTX), plus the neighbouring on-die co-processors (SMU, MEC).
Split out of a tinygrad working tree; tinygrad remains a dependency for
everything that talks to real hardware.
Start here
psp_out/INDEX.md is the map: which documents are authoritative, which are
retracted, which questions are settled negatives that should not be re-run, and
the current priority list. Several filenames outlived their conclusions, so
check a document's row in the index before citing it.
The four governing documents are CONSTRAINTS.md (standing rules, read before
touching hardware), VULN-AUDIT-WHAT-WE-ACTUALLY-HAVE.md (the vulnerability
register), ATTACK-SURFACE.md (the surface map) and INDEX.md itself.
What is actually in here
Two things, and it is worth keeping them apart:
1. Static reversing output (psp_out/) — all six PSP firmware modules fully
named and structured, a cross-version diffing pipeline over 34+ firmware builds,
per-handler annotated analysis, and ~125 findings documents.
| module | functions named |
|---|---|
asd |
1562 |
sys_drv |
1141 |
sos |
527 |
ras |
119 |
hdcp |
109 |
dtm |
14 |
| SMU (Xtensa) | 1033 |
2. A working PSP emulator (psp_emu/) — boots the real signed firmware
under Unicorn, from the ARM reset vector, with a CCP (crypto co-processor)
device model faithful enough that the firmware's own drivers run against it.
It currently runs the whole system in a single boot (harness/full_system.py):
sOS → SYS_DRV → HDCP, DTM and RAS, plus the ASD driver arriving over the host
ring, with every module's execution attributed by address space and live
physical page rather than by a shared virtual address. A
simulated host can drive the real mailbox/ring command path end to end:
host writes mailbox header + ring frame
-> IRQ 80 (doorbell)
-> bl_legacy_mailbox_loop -> tee_ring_drain -> tee_cmd_dispatch
-> tee_load_ta -> bl_load_ta_image (full RSA-PSS/SHA-256 on the CCP model)
-> session slot -> tee_ta_command -> TA_InvokeCommandEntryPoint
Honest status of the security work
No host-reachable memory-corruption primitive has been found, and the reason appears to be structural rather than "we did not look hard enough": the host-invokable TA surface is confined to three syscalls, and an exhaustive audit of all 37 host-reachable ASD opcodes plus a four-class write-shape scan over a 988-function closure came back bounded.
What was found:
- One proven heap overflow —
gp_operation_copy_state(GP opcode 0x37), byte-exact reproduction in-emulator. TA→kernel only, with no emitter, so it is not host-reachable. - One real firmware bug found by emulation —
ccp_hmac_sha2computes HMAC-SHA512 incorrectly (the opad IV memcpy clobbers the last 16 bytes of the inner digest). Dead code in-tree: both callers pass alg=4. - Four latent unbounded-index / uncapped-counter defects, all currently inert.
- An HDCP subcommand-12 overflow proven to be a DoS only.
Several earlier conclusions were retracted after better evidence; retractions are recorded in place rather than deleted. If a document contradicts a hardware-confirmed note, the hardware note wins.
Layout
psp_emu/ emulator
pspemu/ core: CPU/bus/MMU (armmmu.py), CCP model (ccp.py), SVCs
harness/ runnable experiments (boot, fuzzers, PoCs, self-tests)
tests/ test_smoke.py
symbols/ symbol maps consumed by the emulator
psp_out/ static analysis
*.md findings, audits, vulnerability write-ups
*.py analysis tools + hardware probes
handlers/ per-command annotated analysis (hand-written)
phase3/ vdiff/ sigs/ cross-version symbol maps and diff output
handover/ importable symbol sets + import_symbols.py
data/MANIFEST.sha256 SHA-256 of every firmware blob results were derived from
Data policy — what is and is not in the tree
Tracked: the firmware images themselves (*.bin, *.elf, 301 files,
~33 MB). The PSP and SMU module images come from linux-firmware and publicly
mirrored VBIOS images, so they are already public. Nothing here was
decrypted — the encrypted on-flash stages (BOOT_LOADER#1, TRUSTED_OS,
entropy ~8.0) remain encrypted and unanalysed; sOS is the earliest stage we
possess. The whole-flash dumps carry board SKU strings only
(XFX D702_XTX, 113-31XFSHBS1) — no serials, MACs or unique identifiers.
data/MANIFEST.sha256 pins the SHA-256 of all 301 images, so a result can be
tied to the exact bytes it came from even after a re-dump.
Not tracked:
*.i64— IDA databases (228 MB), regenerable from the blobs pluspsp_out/handover/import_symbols.py, and they churn on every incidental click. The reversing work itself is preserved as symbol maps inpsp_out/phase3/,vdiff/,sigs/andhandover/symbols/, which is the part with actual value.psp_out/decompiled/— Hex-Rays output mechanically derived from AMD code (26 MB).- AMD-internal register maps (
nv22_a0_memmap.txt,map2.txt) — vendor documents, a different provenance from public firmware.
Setup
cd psp_emu
python3 -m venv .venv && ./.venv/bin/pip install -r requirements.txt
./.venv/bin/python tests/test_smoke.py # 5/5
./.venv/bin/python harness/ccp_selftest.py # 11/11
./.venv/bin/python harness/verify_real_image.py # 5/5 + 5/5 negatives
Hardware scripts in psp_out/ additionally need tinygrad on PYTHONPATH and
a real GPU; they are the only things here that touch a device.
Working rules this project runs on
Earned the hard way, mostly by being wrong first:
- Validate every detector against ground truth with positive and negative controls. A negative result from an unvalidated oracle is not a result. The TA fuzzer refuses to report "no crashes" until its positive control fires.
- A negative is only meaningful if the harness demonstrably reaches the code — measure coverage, not the pass counter.
- Verify the emitter, and the values it supplies, before valuing the handler. Static reachability is not reachability.
- Model SVCs faithfully rather than patching validators, and prefer running the firmware's own kernel over stubbing it. A stub returning 0 is not neutral; it is a silent lie that surfaces much later.
- Self-consistent tests can be wrong together — the CCP RSA engine passed 6/6 only because the harness shared its mistake.
- Deterministic execution before bug-hunting. A flaky harness produces confident wrong negatives.
- Record retractions in place and propagate them to referencing documents.
Known tech debt
- ~86 hardware scripts in
psp_out/hardcode an absolute repo path (rewritten on the move from the tinygrad tree). They should resolve paths from a shared module or$PSP_OUT; the emulator harnesses already resolve relatively. - 16 scripts hardcode the tinygrad checkout path for imports; should honour
$TINYGRAD_ROOT. psp_out/mixes offline analysis tools with hardware probes; these deserve separate directories.
