aboutsummaryrefslogtreecommitdiffstats
path: root/psp_emu
ModeNameSize
-rw-r--r--.gitignore26logstatsplainblame
-rw-r--r--FIDELITY-NOTES.md30071logstatsplainblame
-rw-r--r--README.md28214logstatsplainblame
d---------harness8679logstatsplain
d---------logs667logstatsplain
d---------pspemu629logstatsplain
-rw-r--r--requirements.txt46logstatsplainblame
d---------symbols37logstatsplain
d---------tests83logstatsplain

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_guard lives at 0x404334. The image holds 0x33333333, but that is only a build-time placeholder: on hardware the runtime guard differs (at N=56 the copy fills the canary with 0x33333333; the emulator returns cleanly, the device faults). Treat the emulated guard as a stand-in, and do not assume a 0x33 payload preserves the canary on real hardware.
  • A smashed canary reproduces the exact hardware crash-oracle signature: pc=0x40433c reading 0xdeadbeef (__stack_chk_fail's deliberate trap).
  • Fault attribution works: LR=0x40410f identifies hdcp_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+272 and saved LR at v58+308 all 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=0x40433c reading 0xdeadbeef — the exact hardware crash-oracle signature. PC-ctl is False at 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 in Component.set_rpc_services({id: svc}).
  • Cross-TA calls: build a Bus, add components, pass AsdRpcService(bus=bus) so HDCP RPCs execute inside the real ASD dispatcher.
  • New harness: copy harness/hdcp_subcmd12.py; use ContentDiff for numbers.