| Mode | Name | Size | |
|---|---|---|---|
| -rw-r--r-- | .gitignore | 26 | logstatsplainblame |
| -rw-r--r-- | FIDELITY-NOTES.md | 30071 | logstatsplainblame |
| -rw-r--r-- | README.md | 28214 | logstatsplainblame |
| d--------- | harness | 8679 | logstatsplain |
| d--------- | logs | 667 | logstatsplain |
| d--------- | pspemu | 629 | logstatsplain |
| -rw-r--r-- | requirements.txt | 46 | logstatsplainblame |
| d--------- | symbols | 37 | logstatsplain |
| d--------- | tests | 83 | logstatsplain |
psp_emu — Unicorn-based emulator for AMD PSP trusted applications
Emulates the PSP TAs (HDCP, ASD, DTM, SYS_DRV) and the RPC edges between them, so handler behaviour can be measured directly instead of inferred from a hardware crash oracle that costs a PCIe retrain per data point.
Setup
python3 -m venv psp_emu/.venv
psp_emu/.venv/bin/pip install -r psp_emu/requirements.txt
psp_emu/.venv/bin/python psp_emu/tests/test_smoke.py # must be all PASS
psp_emu/.venv/bin/python psp_emu/harness/hdcp_subcmd12.py
Images are read from ../psp_out/ (elf/*.elf, falling back to *_full.bin).
Runtimes and the knobs that control them
| harness | runtime | what it proves |
|---|---|---|
tests/test_smoke.py |
<1s | images load, ARM/Thumb voting, leaf routines |
harness/ccp_selftest.py |
~4s | 11/11 crypto vectors against the CCP model |
harness/verify_real_image.py |
~6s | 5/5 containers verified, 5/5 negatives rejected |
harness/boot_sos_full.py |
~1s | sOS boots from the reset vector to its service loop |
harness/boot_sos_and_tas.py |
~7s | sOS + SYS_DRV + 4 modules loaded by the firmware |
harness/boot_and_mailbox.py |
~15s | host drives the real mailbox/ring into a TA invoke |
harness/load_asd.py |
~51s | ASD driver loaded, verified and executing (+2 negative controls) |
harness/gcp_fe08_fullsys.py |
~21s | GCP and ASD in one emulated boot: LOAD_ASD status 0, service 2 registered; logs GCP RPC returns |
harness/full_system.py |
~40s | sOS + SYS_DRV + HDCP + DTM + RAS + ASD all running in one boot — each attributed by address space + live PA, and each shown started (trampoline + lifecycle entry), with a SKIP_ASD control |
harness/asd_rpc.py |
~10s | HDCP's ASD RPCs executing in the real ASD dispatcher |
harness/dtm_ras.py |
~20s | DTM commands 2/3/4 driven; RAS gate documented |
harness/sysdrv_bridge.py |
~60s | experimental — TA SVC 0xF2 service-1 routed into a real SYS_DRV; does not yet open RAS's gate |
harness/fuzz_pss_differential.py |
~90s | firmware PSS parser vs RFC 8017, own keypair |
harness/fuzz_ta_invoke.py |
~0.25s/invoke | host-driven TA invoke fuzzing — exits 2, see below |
gcp_fe08_fullsys.py supplies the Navi 31 ASIC ID (0x31) at the sOS boot-input byte
0x80D0, which the earlier boot stage would set on hardware. Without it ASD
rejects startup with status 7. Ring.submit() returns a dispatch-probe hit
count, not a firmware status; loaders must read Ring.response() and check
service-table registration before interpreting later RPC results.
The current FE08 probe stops at ASD's device-key unwrap (rpc_201 returns 1,
FE08 returns 0x263), before its zero ciphertext is processed. That is a
separate device-key unwrap failure; its cause remains unproven.
These used to take minutes each, almost entirely spent running a system that had
already finished the work being measured. See "Harness runtime" in
FIDELITY-NOTES.md for the measurements and the one real fidelity tradeoff.
fuzz_ta_invoke.py deliberately exits non-zero. Its controls pass, but its
randomised invokes cover no blocks beyond the benign control, so its "0 faults"
is not evidence of absence. It now refuses to report that, the same way it
already refused when its positive control failed.
The reason is now understood: hdcp_invoke_entry ignores the GP commandID in
R1 entirely and requires paramTypes & 0xF == 7 plus a memref with
size > 0x2817, taking its command selector from the first DWORD of the
shared buffer. The host path supplies paramTypes = 0x100, so the first check
fails and every invoke returns error 6 after 22 blocks. Satisfying the ABI at
the entry point unlocks 73 new blocks across commands 1..0x13.
The only code that can produce the required memref parameter
(tee_unmarshal_params) was then measured to execute zero instructions
across boot, a mailbox sweep, every ring command id 0x00..0x0F and a TA load —
with tee_cmd_dispatch firing in the same run as a positive control. So in this
configuration the HDCP command surface is not host-reachable, and this
harness is correctly blocked rather than broken: removing the gate would just
restore the old vacuous "0 faults". Full decode, jump table, per-command
measurements and the scope limits of that claim are in
psp_out/FINDING-HOST-MAILBOX-AND-RING-IN-EMULATOR.md.
Escape hatches, for when you suspect the fast path is hiding something:
| variable / flag | effect |
|---|---|
boot_sos_full.py --full |
disable quiescence exit, run the old fixed 60M-instruction budget |
RUN_FULL=1 |
boot_sos_and_tas.py: ignore the completion predicate, burn the whole wall-clock budget |
TIMER_PERIOD=2000 |
restore the original guest clock rate (see the caveat below) |
RUN_SECONDS, BOOT_BLOCKS |
wall-clock / block budgets, unchanged in meaning |
The one thing to know before trusting a number: TIMER_PERIOD sets how many
basic blocks make up one emulated millisecond (now 200, was 2000). Lowering it
makes firmware delay loops finish in less host time, but it also makes the
emulated CPU slower relative to guest time, so firmware timeouts become more
likely to fire. Results were identical at 2000/500/200/100 here, but if a
finding ever depends on timing, pin TIMER_PERIOD=2000 and say so in the
write-up.
Layout
pspemu/
consts.py firmware addresses, SVC numbers, RPC ABI, session offsets
memmap.py per-component memory layouts (HDCP/DTM/ASD/SYS_DRV)
loader.py ELF / raw image loading
isa.py ARM vs Thumb detection (call-site voting)
component.py Component: Unicorn instance + AAPCS run_function() + faults
svc.py SVC decode/dispatch
services.py emulated crypto service + ASD RPC (configurable, incl. bugs)
bus.py inter-component routing (HDCP -> ASD across address spaces)
trace.py WriteTracer, ContentDiff, Watchpoint, CallTracer
harness/
hdcp_subcmd12.py rx_ids + v58 stack overflow measurement
Each TA gets its own Unicorn instance — HDCP and DTM are both based at
0x400100 on real hardware and cannot share an address space.
Three caveats that will bite you
1. IDA's T segment register is wrong on these images. HDCP ta_memcpy
@0x401058 is marked T=1 (Thumb) but the bytes
e3520003 9a000017 e210c003 0a000008 are an ARM memcpy prologue
(CMP R2,#3 / BLS / ANDS R12,R0,#3 / BEQ); IDA's own listing there degenerates
into DCD. The TAs are mixed ARM/Thumb: handlers are Thumb, several libc
leaf routines are ARM, reached by BLX (state-toggling) rather than BL.
isa.build_mode_map() votes over every call site instead: ta_memcpy gets
62 ARM votes / 0 Thumb, ta_memclr 91 Thumb / 0 ARM.
2. UC_HOOK_MEM_WRITE corrupts Thumb ITSTATE. Conditional stores that
should be skipped execute while a write hook is installed. Measured on
ta_memclr(0x4ff980, 0x20):
| bytes written | |
|---|---|
| no hook | 32 (correct) |
| write hook installed | 47 — the it hs / it mi tail stores fire |
The optimised memcpy/memset routines are full of IT blocks, so hook-derived
extents are not trustworthy. Use trace.ContentDiff (sentinel-fill, run
hook-free, diff) for any load-bearing measurement. WriteTracer is still
useful for attribution//exploration, not for numbers.
3. The image blobs end with a 256-byte RSA signature that overlaps BSS.
*_code.bin is [verified body ($PS1.size)][0x100 signature], and the TA header
field +0x1C is the BSS base -- which lands exactly on that tail (ASD BSS
0xD33000 vs image end 0xD33100; HDCP 0x407000 vs 0x407100). Loading verbatim maps
high-entropy signature bytes where zeroed BSS belongs, and they masquerade as
initialised data -- one of them passed a CBZ null guard in ASD sub_D02794 and
became a BLX target. Component._zero_signature_tail() zeroes it after load.
4. ta_memcpy jumps out of its own function bounds (0x401058 → 0x401702),
so filtering writes by "PC within [func, func+size)" silently misses most of
the copy.
Validated against hardware
__stack_chk_guardlives at0x404334. The image holds0x33333333, but that is only a build-time placeholder: on hardware the runtime guard differs (at N=56 the copy fills the canary with0x33333333; the emulator returns cleanly, the device faults). Treat the emulated guard as a stand-in, and do not assume a0x33payload preserves the canary on real hardware.- A smashed canary reproduces the exact hardware crash-oracle signature:
pc=0x40433creading0xdeadbeef(__stack_chk_fail's deliberate trap). - Fault attribution works:
LR=0x40410fidentifieshdcp_verify_mac's canary check rather than subcmd12's.
Result: subcmd12 stack overflow (harness/hdcp_subcmd12.py)
Frame: PUSH {R4-R11,LR}; SUB SP,#0x214, v58 at SP+0x100
→ canary at v58+272, saved LR at v58+308.
RETRACTED: the "return-address control" result below no longer holds, and the reason is that the emulator got more faithful.
The table originally read:
N 5N v58 reach canary savedLR PC-ctl out <-- SUPERSEDED
62 310 +319 0x33333333 0x3e003333 True FETCH at 0x3e003332
70 350 +359 0x33333333 0x33333333 True FETCH at 0x33333332
103 515 +524 0x33333333 0x33333333 True FETCH at 0x33333332
and concluded that a 0x33 payload preserves the canary (payload == guard),
so for N >= 62 the saved LR is overwritten and the CPU fetches from
0x33333332. That depended entirely on __stack_chk_guard holding the image's
build-time placeholder 0x33333333.
It no longer does. The per-load stack canary is now modelled (SVC 0xA2 TRNG +
loader write, see FIDELITY-NOTES), so the guard is a real value — measured
0x31cca5e4, stable across loads because the TRNG model is deterministic. A
0x33 payload therefore smashes the canary instead of preserving it, and
__stack_chk_fail traps before the corrupted LR is ever used. Current measured
output:
N 5N v58 reach canary savedLR PC-ctl out fault
40 200 +312 0x31cca5e4 0x00ff0001 False 106
52 260 +312 0x31cca5e4 0x00ff0001 False 106
54 270 +312 0x00000000 0x00ff0001 False None READ_UNMAPPED at 0xdeadbeef
56 280 +312 0x33333333 0x00ff0001 False None READ_UNMAPPED at 0xdeadbeef
62 310 +319 0x33333333 0x3e003333 False None READ_UNMAPPED at 0xdeadbeef
70 350 +359 0x33333333 0x33333333 False None READ_UNMAPPED at 0xdeadbeef
103 515 +524 0x33333333 0x33333333 False None READ_UNMAPPED at 0xdeadbeef
What survives, and what does not:
- The overflow is real and the reach numbers are unchanged. v58 reach,
canary at
v58+272and saved LR atv58+308all still measure exactly as before; at N >= 62 the saved LR still takes attacker bytes (0x3e003333). The ARM memcpy stores whole words, so the canary is corrupted at N=54 (270 bytes), slightly below the naive 272. - PC control does not survive. The canary check now fires first, every time,
producing
pc=0x40433creading0xdeadbeef— the exact hardware crash-oracle signature.PC-ctlisFalseat every N.
This is the emulator converging on hardware, not regressing: the caveat two
sections above already said "do not assume a 0x33 payload preserves the canary
on real hardware", and the hardware oracle always faulted where the emulator
returned cleanly. Those now agree. The earlier unconditional PC-control claim
was an artefact of the placeholder guard and should not be cited.
The conditional form of the claim is still exactly true, and was re-verified by forcing the guard back to the placeholder and changing nothing else:
guard = real per-load value (0x31cca5e4) guard forced to 0x33333333
N=62 savedLR=0x3e003333 PC-ctl=False N=62 PC-ctl=True FETCH at 0x3e003332
N=70 savedLR=0x33333333 PC-ctl=False N=70 PC-ctl=True FETCH at 0x33333332
N=103 savedLR=0x33333333 PC-ctl=False N=103 PC-ctl=True FETCH at 0x33333332
So the guard value is the sole blocker, which is what
psp_out/SUBCMD12-RESOLVED.md and psp_out/FINDINGS-SUMMARY.md already say
("with a matching guard, N>=63 yields full PC control", "blocked solely by the
runtime stack guard value"). Those two documents were written conditionally and
remain accurate as-is; only this README overstated it.
The hardware divergence is now explained (harness/hdcp_cmd0f.py).
hdcp_cmd0F_handler has a latch at 0x402234: ldr r1,[r2,#0x18] (sess->err_state)
/ cmp r1,#7 / return with all response sub-status fields left at 100. subcmd12
sets err_state=7 on every failure path (109/107/108/106), so the first
failing subcommand bricks the session for cmd 0x0F:
A) fresh session, N=40 -> subs=[106,..] err_state=7 rx_count=40 (ran)
B) same session, N=90 -> subs=[100,..] err_state=7 rx_count=40 (NOT called)
C) err_state=7 preset -> subs=[100,..] rx_count=0
D) FRESH slot, N=90 one-shot-> rx_count=90, PC=0x33333332 (LR hijack)
Earlier hardware runs "primed" a slot first, which latched it — so every later
measurement observed a handler that never executed. Correct methodology is
one observation per freshly opened slot. Still: the emulated hijack in (D)
uses stubbed crypto; on hardware the real MAC fails and the uninitialised-context
overrun smashes hdcp_verify_mac's canary first, so this is not yet a hardware
result.
What is still stubbed (fidelity gaps)
| area | current | why it matters |
|---|---|---|
| crypto service (SVC F2, id 1) | SHA-256 stand-in; optional overrun_uninitialised. Measured not load-bearing for fuzz_hdcp |
real MAC always fails for unauthenticated data -> err_state=7 on the first call, which is the reason hardware is one-shot-only. Lives in sOS/secure SRAM, absent from every image we have — can only be modelled behaviourally. Swapping the stand-in for a per-call random secret leaves every fuzz_hdcp statistic bit-identical, so no conclusion there depends on crypto fidelity; see "not load-bearing" in FIDELITY-NOTES.md. Unmeasured for fuzz_asd/fuzz_sysdrv, which do more crypto. |
| ASD TA (SVC F2, id 2) | wired — AsdRpcService(bus=...) runs the real ASD dispatcher (harness/asd_rpc.py); stub still the default elsewhere |
the stub returned 0 for every verb; the real dispatcher returns 2 / 5 and executes 28-30 ASD blocks per call. Note subcmd12 issues zero ASD RPCs, so the old claim that this "changes subcmd12's control flow" is not reproduced — see the divergence note in harness/asd_rpc.py. |
| TA entry / init | done — TA_CreateEntryPoint (0x40139C) is executed and dword_40237C is left at its image value (0x407000, the TA's own BSS table). Nothing patches it any more. |
measured: create returns 0 and writes no globals, so the image default is the real table; the old hand-patch pointed the TA at a harness fiction. |
| session lifecycle (cmd 0x01/0x0E) | done — sessions are opened by a real cmd 0x0E invoke through hdcp_invoke_entry; the TA allocates slots itself (1..7, never 0) |
measured: one cmd 0x0E takes a slot from (state=0, active=0) to (state=2, active=1) and touches nothing else in the 0x28c-byte session — byte-for-byte what the old hand-written bypass produced. |
| sOS gate semantics | region validation modelled and validated (pspemu/gate.py, harness/sos_gate.py); DACR switch was already modelled — the old "not modelled" claim is retracted; async dispatch still absent |
sos_check_user_ptr (FA 0x178C) and sos_check_user_range (FA 0x191C) are reimplemented and differentially tested against the firmware: 292/292 cases agree, and the differential is shown to catch a deliberately wrong model (the old always-succeed behaviour is caught on 18/84 pointer cases). The gate is enforced in the RPC path, not inert: out-of-window request pointers get the firmware's own 0xA and the service never runs, while a real HDCP invoke has 0 of its own requests rejected. config['gate']=False restores the old behaviour, config['proc_type']=4 models a privileged caller. check_range is validated to the same standard but deliberately not applied generically — knowing which request fields are buffers is service-specific, and guessing would invent semantics; on the one path where the layout is known (SYS_DRV RPC) the firmware's own sub_E0DEF4 and sub_E0AB44 run unpatched in sysdrv_bridge.py, so range checking there is faithful by execution. DACR: the row used to claim no domain switch. That was a stale restatement of a claim FIDELITY-NOTES.md had already retracted — the MMU is enabled and domains are live. Measured both ways: the emulator really enforces domains (domain=no-access faults, client+AP=0 faults, manager reads — positive control passes), and sOS's own mcr p15,0,rX,c3,c0,0 sites execute 3944 times in a bounded boot (86501/86500 paired entry/exit in a full run). Async dispatch: also measured rather than assumed, and the old "not modelled" claim is retracted for the full-system harnesses — sOS's own scheduler runs there, creating 22 tasks with 24008 reschedules and 8 stacks busy enough to carry >1000 blocks each, which is real context switching, not one thread. It remains genuinely absent component-level, where there is no sOS and an SVC completes synchronously inside the handler. All four properties are re-checked on every harness/sos_gate.py run, each with a failure mode that reports NOT REPORTABLE rather than a vacuous pass. |
| persistence | modelled — fuzz_hdcp keeps TA state across iterations by default (HDCP_PERSIST=0 restores the old wipe); still a fresh Component per harness run |
hardware keeps TA globals for a whole boot, which is exactly what the cmd 0x0F latch exploits. Measured over 1500 iterations: persistence gains ~9% HDCP coverage (767 -> 839 distinct blocks) and the run now ends with sessions genuinely latched (err_state 7), reproducing the one-shot behaviour. CANARY_SMASH drops 46 -> 7 because the latch stops subcmd12 being reached — that is the latch being modelled, not lost bug-finding. |
| DTM / RAS | DTM driven (harness/dtm_ras.py): commands 2/3/4 through the real GP invoke entry, with negative controls. RAS blocked on an init gate |
DTM could not have been driven before: loader.resolve picked elf/dtm2.elf, which carries the full image (0x2100, header+body) but declares the body-only base p_vaddr=0x400100, so everything landed 0x100 high and all five GP entry points resolved into the container header's zero padding. Now uses elf/dtm.elf (same build, correct base). RAS gates every command on a byte at 0x4060e0. Traced: the only store to it is inside TA_OpenSessionEntryPoint, guarded on an inner call chain that bottoms out in SVC 0xF2 service 1 -- the SYS_DRV RPC (opcodes 0x0001/0x000d/0x0002/0x1077, measured). So RAS is blocked on SYS_DRV RPC responses, not a mystery flag; component-level, service 1 is bound to CryptoService, the wrong service. Routing it into a real SYS_DRV the way Bus.route_asd does for ASD is the fix, and is now tractable because sysdrv_env opens SYS_DRV's own init gate for real. Command coverage explicitly not reported until then. |
| TA image versions vs silicon | RAS matches the card byte-for-byte; HDCP/DTM/ASD do not | Checked against an RX 7900 XTX's psp_13_0_0_ta.bin. Our ras2.elf body is bit-identical to the card's RAS 27.0.2.5 (db2ed53e...b58385), so RAS findings apply to that silicon directly. The card runs HDCP 23.0.0.63, DTM 18.0.0.22, ASD 33.0.0.217 — none of which are the images in this tree (we do not hold .63 at all; vdiff has .49/.70/.82). Caveat now discharged for HDCP and DTM: the card's own builds were extracted into elf/hdcp_23.0.0.63_edab4e4f.elf, elf/dtm_18.0.0.22_adbf8c8b.elf, elf/asd_33.0.0.217_c4c1e5bc.elf, and re-running hdcp_cmd0f, hdcp_subcmd12 and dtm_ras against them gives identical output except the PC of one sentinel fault (0x40433c → 0x4042b4, same code at a different link address). fuzz_hdcp runs comparably (817 vs 815 blocks). Entry points are resolved from the image by pspemu/ta_entries.py (validated 3/3 against known builds) rather than hardcoded. ASD now covered too: resolve_asd() reads its dispatcher from trampoline +0x1C (validated across 12 builds, all 0xD013B8, corrupted stub refused), and fuzz_asd resolves asd_validate_ptr by unique-match signature instead of the hardcoded 0xD12E90 that made it fetch from 0 on other builds. Against the card's ASD 33.0.0.217: fuzz_asd 967 vs 938 blocks with the same 395 ok / 5 noise split, asd_rpc same return values, and load_asd puts the card's signed container through the firmware's own loader — RSA check passes, 76 blocks, both bit-flip controls still rejected. Not in SPI flash, incidentally: spi_parse.py finds no TA entries at all, and the PSP images that are there are enc_option=1 encrypted. See psp_out/HW-VALIDATION-RX7900XTX.md and psp_out/HW-TA-VERSION-REPRODUCTION.md. |
| RAS on silicon | the 0xE575B8 blocker is confirmed a missing-state gap, not a firmware defect | The card's own signed RAS 27.0.2.5 (body byte-identical to ras2.elf) was loaded over the GFX ring with psp_out/load_ta.py (built for this — tinygrad has no LOAD_TA path). Command 1 returns ras_status=0, so RAS processed it rather than short-circuiting on its init gate at 0x4060E0 — a gate only opened via the SYS_DRV RPC chain containing opcode 0x000d. So 0xE575B8 is populated on a real boot. Hardware oracle for when the emulator's gate opens: 1→0x0, 2→0xa002, 3/4→0xa010, 5..0x10 rejected at the ring with 0xFFFF000A. Caution recorded: resp.status is ring delivery only and returns 0 for bogus command ids too — the TA verdict is ras_status in the shared buffer. See psp_out/HW-RAS-ON-CARD.md. |
| ring status codes vs silicon | 7/9 exact | GET_FW_ATTESTATION→0x0, unknown ids (0x00/0x55/0x99/0xfe/0x1000)→0x100, INVOKE_CMD→0x117 all match hardware exactly. LOAD_TA (hw 0x5 vs emu 0x116) and UNLOAD_TA (hw 0x117 vs emu 0x11a) differ, but those are the only state-dependent paths and the ring harness has no TMR or loaded TAs — not reportable as firmware divergence. Inputs were equalised first. |
| timing / concurrency | task scheduling is modelled in the full-system harnesses (retracting "none"); timing fidelity is still approximate | "no preemption or task scheduling" was wrong in the same way the DACR row was: sOS's own scheduler executes, and harness/sos_gate.py measures it every run — 22 tasks created, 24008 svc_60_reschedule calls, 8 busy stacks. A timer FIQ is installed and does interrupt execution, so tasks that sleep genuinely wake. What is not claimed: the scheduler is cooperative in sOS's own design, so this is not evidence of preemptive time-slicing, and timing remains approximate — TIMER_PERIOD defaults to 200 rather than hardware's 2000, which makes the emulated CPU slower relative to guest time (pin TIMER_PERIOD=2000 if a finding depends on timing). Component-level harnesses remain single-threaded and synchronous. |
| SVC permission gate | modelled and validated — gate.check_svc, enforced in SvcDispatcher |
sos_svc_dispatch (FA 0x587C) is not a bare jump table: it checks the SVC against the calling process's entry in the same table check_ptr uses and denies with 0xE before any handler runs. Rule: svc <= 0xDF denied if (proc_byte & 0xF) is 5 or 6 and svc != 0x75; 0xE0..0xEF denied unless proc_byte & 0x20; > 0xEF unchecked. Differentially tested against the real dispatcher with handler bodies stopped at entry so no side effects confuse it: 140/140 agree, 46 of them actual denials. Live in the dispatcher: an ordinary process has 0 of 279 SVCs denied, a type-5 process 47 of 167. config['proc_byte'] selects the class; config['svc_gate']=False restores the old unconditional behaviour. |
| component-level sOS SVC bodies | RESOLVED — measured from a real boot — ASD issues SVCs nobody models (0x51 unknown, 0x57 svc_57_obj_op, 0x5D svc_5D_alloc, 0xB4 svc_B4_multiop), answered with a fabricated 0 |
previously silent; now warned per SVC (PSPEMU_SVC_STRICT=1 to make fatal). Measured over 300 iterations at fixed seed: baseline 924 ASD blocks, but flipping 0x51/0x5D/0xB4 individually to a failure gives 518 / 482 / 771 — each is individually load-bearing, up to 48%. The fabricated value is the permissive one, so fuzz_asd explores paths a real failure would cut off: its coverage is an upper bound and its clean runs are not evidence of absence. Note the near-miss — returning a plausible handle from 0x5D instead of 0 is bit-identical, so only driving the value to a failure exposed the dependency. Resolved: full_system.py dispatches all four through the real sos_svc_dispatch, so the true values were measured, not modelled — 0x51 returns 1,2,3,... and 0x5D returns 0,1,2,... (sequential handle allocators, strictly increasing, no reuse), while 0x57/0xB4 genuinely return 0. services.register_measured_sos_svcs() installs these and fuzz_asd uses them by default (ASD_RAW_SVC=1 restores the old behaviour); nothing falls through the silent default now. The upper-bound qualification is withdrawn: measured fabricated-0 vs real values gives 959 blocks both ways with a byte-identical block set, so no past result was distorted — the fabricated value was wrong but happened to be path-equivalent. Bound: one boot, small call counts (18/39/3/1), so this is normal-operation behaviour, not allocator policy at exhaustion. See psp_out/FINDING-ASD-SVC-STUBS-LOAD-BEARING.md. |
Priority to close the gap: ~~(1) wire Bus so ASD RPCs execute in the real ASD
dispatcher~~ done (harness/asd_rpc.py, 5/5 thunks routed and executing),
(2) model the crypto service's real failure semantics, ~~(3) run TA init instead
of patching globals~~ done (TA_CreateEntryPoint executed, dword_40237C
no longer patched, sessions opened by real cmd 0x0E).
SYS_DRV does have init, and harness/fuzz_sysdrv.py was not running it --
it called a sub-initialiser that left the gate shut, so every iteration bounced
off sub_E087E4 after 4 basic blocks while reporting a clean 5000/5000. It now
runs the real init (sysdrv_env.run_init, opcode 0x1000) and raises if the gate
is still closed. See "Independently rediscovered" in
psp_out/FINDING-FEATURE-MASK-GET-UNBOUNDED-INDEX.md.
ASD has no init phase to run: its entry trampoline at 0xD19868 is
mov r0,r2 / blx *(0xD19884) / svc 0x75, and *(0xD19884) == 0xD013B9 is the
dispatcher itself. So (3) has no ASD counterpart. Two ASD code patches were
examined while checking that: asd_feature_enabled is no longer patched
(measured redundant -- it already returns 1 for ids 0..0x1F unpatched), and
~~asd_validate_ptr is still stubbed~~ also fixed: it is a genuine range
check accepting only 0x400000..0x600000 (the caller's window sOS maps in for the
RPC). fuzz_asd.py now allocates its argument buffers inside that window so the
REAL check gates them, and asserts at startup that the validator both accepts
those buffers and still rejects ASD scratch -- a stub that accepts everything
would pass the first half alone. Effect: scalar-deref noise 533 -> 82 over 5000
iterations, at a measured cost of 53 distinct blocks (-4.4%) that were only
reachable via pointers the real ASD refuses.
Extending
- New service: implement
__call__(self, comp, req_ptr)and register it inComponent.set_rpc_services({id: svc}). - Cross-TA calls: build a
Bus, add components, passAsdRpcService(bus=bus)so HDCP RPCs execute inside the real ASD dispatcher. - New harness: copy
harness/hdcp_subcmd12.py; useContentDifffor numbers.
