aboutsummaryrefslogtreecommitdiffstats
path: root/psp_out (follow)
Commit message (Collapse)AuthorAge
* CORRECTION: crypto buffer confinement is the type-5 svc-0xf2 window ↵user2026-10-08
| | | | | | | | | | | | | | | | | | | | | | [0x400000,0x800000), not ptr<=0x40000000 A reviewer correctly flagged that validate_client_range's ptr<=0x40000000 bound does NOT mean GCP-owned -- SYS_DRV (0xE00000), sOS, and other TAs are all below 0x40000000; that check only excludes the high MMIO aperture. My 'self-corruption' verdict was right for the wrong reason. Verified in sOS: the real confinement is the type-5 caller gate sub_22B4 (sos_proc_switch). For a type-5 caller (GCP) it requires msg + arg ptr in [0x400000,0x800000) and RECORDS that window into tcb[24]=msg_base, tcb[25]=TA_end (type-1 uses tcb[26]/[27], 0x200000 window). SVC 0xB5 (validate_client_range's backing validator) checks each RPC (ptr,len) against that recorded window; teardown sub_24A4 reads tcb[25]/[27] back. So the window -- not the spoofed cur_tcb[0x4c] identity -- governs buffer validation. Consequence: even with the confused deputy setting cur_tcb[0x4c]=SYS_DRV for the ACL, the RPC arg window stays GCP's own [0x400000,0x800000). Every crypto buffer (in/out) must lie inside it, so crypto ops cannot read/write SYS_DRV/sOS/other domains via buffer descriptors. Output overrun = self- corruption within GCP's window. Conclusion holds, now correctly grounded. Caveat: verified the gate sub_22B4 + teardown sub_24A4; did not fully disasm the SVC 0xB5 body, but gate + prior service-1 overflow finding corroborate window-enforced validation. Updated the crypto overview.
* map the TA->kernel SVC surface under ROP; correct two SVC-OVERVIEW errors; ↵user2026-10-08
| | | | | | | | | | | | | | | | | | | | | rank escalation leads With repeatable ROP in GCP the TA can issue any SVC, so the 'compromised TA' surface is live. Resolved against the gate bytes at 0x58ba: a type-5 TA (byte 0xF5, cap bit 0x20 set) CAN issue 0xE0-0xE9, so the reachable set is 0x75, 0xE0-0xE9, 0xF0-0xF5 (16 SVCs) -- correcting AUDIT-TA-KERNEL-BOUNDARY's claim of only 0x75 and 0xF0+. Corrected SVC-OVERVIEW: decoding the 0x58F0 tbb shows 0xE8/0xE9 call bl 0x6f38 which is a standalone bx lr (no-op), NOT sub_6F3A. The 0x7C/0xA0 poke (sub_6F3A) is real, and its gate sub_6D20 is permissive (OK for addr+8 <= 0xCB00000 outside the ~0x37-0x3a000000 MMIO apertures, limit literal 0xCB10000) -- but 0x7C/0xA0 are <= 0xDF and thus denied to TAs. So ROP does NOT hand a TA a kernel write SVC; that is the main negative. Ranked the reachable leads: (1) SVC 0xF2 to the permission-exempt crypto service with a now-arbitrary crafted message -- richest surface, natural next target; (2) 0xE6/0xE7 -> sub_6E40, a per-object permission-bitmap op, audit whether a TA can grant itself a capability bit; (3) the 0xF2 third-window headroom overrun, which needs the nested-call condition a ROP chain might set up. Wrote fw/LEAD-GCP-ROP-TA-KERNEL-SURFACE.md.
* [HW] FE0A selector 8 confirmed on silicon: the store is real and host-aimed, ↵user2026-10-07
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | the value is zero, the reach is in-buffer only Ran the Add.142 step-3 probe on the card, reusing codeccap.py's proven state-3 handshake verbatim and diffing the whole buffer against an 0xBB fill. Confirmed, with positive and negative controls, reproduced across an ATX cycle: * selector 8 (tbb case 7) reaches 0x404fe8; sel7 -> 0x240 and sel9 -> 0x100 write nothing * the displacement field is *(params+0x834) and dst = params + 0x418 + D, hit exactly (+0x600) * the D > 0xFFFFFFFB branch really does bypass the bound check: D=0xFFFFFFFC zeroes params+0x414, seen as the single diff byte +0x415 going 0x10 -> 0x00 Retracting Add.142's central claim. The written dword is 00 00 00 00 at every landing and on every repetition, so this is a second ZERO store, not the ASD RPC result, and it does NOT satisfy the hijack report's lead 4. The standing claim that GCP exposes no non-zero chosen-value OOB write survives; I had inferred the value from the data flow instead of measuring it. New negative on the capability question: FE09's recipe of *(params+0x818)=0xFFFFFFFB is fatal on this path. The handler replicates the param block at 0x1538 stride across the entire buffer, never writes a completion tag (status reads back as the 0xBB fill), and leaves GCP unable to open a session -- FE07 then fails 8/8 and an ATX cycle is required. So D is bounded by 0x818, which must stay small, and the only out-of-window reach is the four wrap values landing on params+0x414..0x417. The write cannot reach stack, heap or .bss, so it is closed as an exploitation primitive and the Add.140/141 chain is unchanged. Also recorded two operational constraints learned the hard way: I wedged the card by killing a running probe mid-operation and then deleting its USB lock, and 'pkill -f' over ssh matched my own command line and silently killed the session, which reads exactly like a hang.
* LSB-zero ARM-mode re-entry: r8 is the one unswept gadget base, route 1 ↵user2026-10-07
| | | | | | | | | | | | | | | | | | | | | | | | | closed by call ordering Add.6 proved the saved-LR overwrite on silicon but never evaluated its own aside that P=Q-3 yields PC = LR & ~0xFF with bit0 cleared, i.e. an ARM-state entry in the same 256-byte block. Evaluated it. 0x405000 ARM-decodes to ldmdaeq r8,{...,sp,...,pc} -- pc=*(r8), sp=*(r8-8), idx-0, executing under the measured flags, the only aligned entry in the build that loads both pc and sp off a non-stack base. It matters because Add.132/133/134 closed the ldm class only for bases {r4=ctx, r0/r5/sp} on the ctx+0x4f carve floor; r8 was never swept, being 0 on the UNLOAD path, and an r8 base sources from the host command buffer, which is below no floor. The route does not close the gap, for a reason worth recording. A later PUSH rewrites the word, so the write and the corrupted return must share a call chain. The FE09 zero-store's live frames yield only blocks 0x404000 and 0x402a00, neither a pivot. The CodecToken stamp cannot help either, and not for the frame-destruction reason the report gave: 0x404828 calls 0x404fe8 at 0x404962 and the stamp fn 0x404b5c at 0x404988, so the frame holding the 0x4050xx call sites is a sibling and already dead. The UNLOAD shortcut is separately closed -- no function on that path pushes or pops r8. Qualified the report's 'nothing reads the memory we fully control' to the states we can reach, and promoted re-entrancy to lead 0. Tooling note recorded: the first call-graph pass used recursive descent, silently missed the 0x404962 -> 0x404fe8 edge and returned a false EMPTY; rebuilt on a global over-approximating scan validated against the direct disassembly.
* extend the decompilation to ASD and the three handover-symbol typesuser2026-10-01
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | The twelve Windows TA types take their symbols from fw/libdumps; ASD, HDCP, RAS and DTM have theirs in psp_out/handover, in a different shape that also carries prototypes and comments. This adds that reference path, so every component the corpus holds is covered by one pipeline rather than twelve of fifteen. ASD needed its own ELFs. It is driver-class, not a TA: base 0xD00100, and no TA entry trampoline, so dispatcher-anchored naming does not apply to it at all -- its RPC dispatcher hangs off trampoline+0x1C instead. The base is not guessed; it matches psp_out/handover/elf/asd2.elf, whose LOAD segment contains 1562/1562 of the recorded ASD symbols. The frame discriminator used for the TAs is inapplicable here for the same reason the trampoline is: there is none, and it reports "neither" for all 17 builds. Prototypes are applied where recorded. Without them Hex-Rays guesses argument counts and types, which is most of the distance between readable C and a wall of _DWORD; 1232 of 1434 land on the ASD reference and the rest are rejected by IDA, reported rather than hidden. One more false-positive class in the dispatcher cross-check, found by running it on HDCP: the agreement test assumed a ported name encodes its command number, which is GCP's convention and not HDCP's, so every one of HDCP's 16 commands was reported as conflicting. A name can only contradict the dispatcher if it claims a command itself, so the check now requires the ported name to match <ta>_cmd<hex> before judging it. HDCP drops to zero conflicts while GCP's single real one -- the 0xFE07 handler carrying the inherited name gcp_cmdFE03_CreateAuthenticatedSession -- is still reported. Coverage is honest rather than flattering. ASD lands 162-425 named of ~1100-1400 functions per build, and the dominant limiter is ambiguity, not similarity: 677-908 functions per build share a byte signature with a differently-named reference function and are skipped instead of being assigned arbitrarily. ASD is full of identical small stubs, so this is expected, but it means the ASD figures are a floor and a smarter tie-break would raise them. RAS is weaker still at 13-20 named per build.
* build a symbolicated, diffable decompilation of every GCP builduser2026-10-01
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | The project had one symbolicated build per TA type, so cross-version comparison was impossible: there was nothing to compare against. This adds the two pieces that were missing and proves them on GCP as a pilot. `fw/mkelf_ta.py` wraps each carved TA body as an ELF IDA can load. The format is read back off the twelve existing reference ELFs rather than assumed, and the control is strict: regenerating all fourteen of them is byte-identical, so the generator is not inventing a layout of its own. `fw/ta_re.py` ports the reference symbols onto every build and emits whole-program pseudocode. Names are applied in tiers -- EXACT bytes, MNEM identical mnemonics, FUZZY similarity above 0.90 -- and anything below the floor is left as sub_*, never guessed. A signature shared by two differently named functions is treated as ambiguous and skipped rather than resolved arbitrarily. The gate that makes the rest trustworthy is porting the reference onto its own build: 112/117 EXACT, and a type whose self-port fails is reported void instead of emitted. The output is shaped for diffing, which cost two iterations to get right. Addresses are omitted and functions ordered by name so the same function lines up across versions; named and unnamed functions go to separate files, because one function named in build A and anonymous in B reordered everything after it and turned a 48-function change into 2900 diff lines. Adjacent builds now differ by 1026 lines, consecutive ones by 261. Chained porting is included and honestly oversold: seeding each build from its nearest already-named neighbour moved names from MNEM into EXACT, which is a real confidence gain, but total coverage did not move. The 22.0.0.62-77 branch stays at 52% named because those builds genuinely diverge -- the function count jumps 119 to 126 -- not because the reference was too distant. FUZZY names are deliberately not fed back into the chain, so a single bad guess cannot propagate and look more confident at each hop. A scare worth recording as resolved: all 78 TA bodies carry a module header whose own metadata (end == base + length, 78/78) implies the project's ELF base is 0x100 too high, which would have made every recorded TA address wrong. It is not. The trampoline at header+0x10 must yield Thumb-bit entry pointers at +0xA0/+0xB0, and across all 78 builds that resolves only in the 0x400100 frame. The existing convention is now confirmed by evidence that discriminates between the two frames, unlike the emulator's verify(), which is self-consistent because its expected values came from harnesses in the same frame.
* index this session's findings, which existed but were unindexeduser2026-09-30
| | | | | | | | | | | | | | Six documents were committed without index entries, against the repo convention that a finding is discoverable from the index together with its limitations. Each now has an entry stating what was measured and, equally important, what it does not establish: the absent version enforcement is a capability rather than a memory-safety defect; the unchecked fence destination does not land in the trusted region under a dense functional test; the honoured translation identifier never yielded a controlled access and would grant nothing on a bare-metal part anyway; the misaligned ring base is a hang; the runtime bootloader mailbox is reachable but refuses to act; and the rollback reachability lead is closed negative. Recording the limitation in the index entry rather than only in the document is what keeps a reader from over-reading a title.
* write up the rollback finding for disclosureuser2026-09-30
| | | | | | | | | | | | | | | | | | The third disclosure-ready report, and the one with real exploitation relevance since it lets an attacker choose which firmware runs rather than merely crash it: the bootloader validates component signatures but not versions, so an older genuine signed set including the secure OS boots on a part shipping a newer one, confirmed by the processor's own version register after a cold power cycle. The report states its scope limit plainly, that this is a capability rather than a memory-safety defect whose severity depends on what was fixed in the rollback window, and notes that the two available older builds fuzzed no weaker on the same axes while the confirmed underflow needs the same unreachable payload in all three builds, so no chain is demonstrated. It records the reproduction including the mandatory cold power cycle, the identity positive control, the two objections closed by measurement, and the version-register misreading from an earlier draft. It is indexed with that limitation stated in the index entry.
* finding: the ring base is accepted misaligned and then wedges on first submituser2026-09-30
| | | | | | | | | | | | | | | | | | | | | | The last host-to-processor surface is the ring-creation registers, whose status field reports acceptance directly so nothing need be inferred. That validation layer is good: zero, one and all-ones sizes are rejected with one code, a two-gigabyte size with another, and a base inside the trusted region with a third, so the trusted region is explicitly excluded from the ring base. The gap is alignment. A base one byte off is accepted at creation and the processor then dies on the first submit. The aligned control runs first in the same boot and succeeds with the heartbeat advancing, so this is not an inherited dead processor, the failure mode that produced phantom hits twice in this audit. It is an inconsistency rather than a policy, since command-buffer and image misalignment are both rejected with their own codes, and the consequence is deferred to first use in the same shape as an earlier zero-length descriptor defect. It is an eighth denial-of-service trigger, not corruption. A process note is kept: the first sweep reported every case including the control as wedged, caused by a syntax error in the script so that nothing ran, and the control is what exposed it.
* close the ring index: bounded, and handled gracefullyuser2026-09-30
| | | | | | | | | | | | | | | | The frame contents were fully mapped but the ring index never was, a question hidden by the host driver's own bug of never wrapping the write pointer. An unclamped index would make the processor fetch a frame from outside the declared allocation, an out-of-bounds read by trusted firmware at a host-chosen index. Planting a valid frame in pages reserved beyond the ring, so an out-of-range fetch would read our own planted data and be unmistakable, shows the index is bounded: neither one ring-length nor a megabyte past the end executes, while the legal control does. The result worth noting is that the processor stays healthy in both out-of-range cases, ignoring the index rather than faulting, in contrast to the two frame fields that kill it. The index is therefore not merely checked but handled gracefully, which is the behaviour those fields are missing, and that contrast is recorded in the field map.
* close the last ring-frame field and publish the complete field mapuser2026-09-30
| | | | | | | | | | | | | | | | The command-buffer size field had been lost to an inherited dead card and never retested, and it is the most interesting kind of field, a length the host declares, which would be an overflow if the processor copied that many bytes internally. With the buffer flanked by pattern-filled guard pages and the control re-run after every mutation, every value from zero to all-ones completes cleanly with no guard writes in either direction: the field is ignored entirely and the command size is implicit in the fixed-size structure. The ring frame is now completely characterised and published as a field map: two unvalidated host-controlled fields, three of the seven denial-of-service triggers, one field ignored, and no corruption. The map is indexed with its limitation stated, namely that the unchecked write destination does not land in the trusted region under a dense functional test and that a host-chosen translation context grants a bare-metal host nothing it did not already have.
* hundredth pass: consolidate the denial-of-service family into a ↵user2026-09-30
| | | | | | | | | | | | | | | | | | | | | | disclosure-ready report The hardware work produced four distinct hang triggers but they had accumulated as appended sections in a file named after the first, which is not a form anyone could act on. The new report consolidates them with what each trigger needs, noting three require no crafted payload, reproduction commands with the in-run controls that make each result stand, the dispatch path for the two sharing one service opcode, the register evidence that the class is a hang rather than corruption, the two root-cause hypotheses that were tested and rejected so they are not re-proposed, the boundary checks that are correct so the defects are not mistaken for a generally weak validation layer, and three concrete fixes. It is indexed with its classification in the entry itself. Final audit: the objective asked for a memory-corruption primitive and one hundred passes did not produce one, with the negative now supported from every direction available on this part. The work produced two disclosure-ready reports, about twenty-five reproducible probes, verified corruption defects that the host cannot reach, four hardware-confirmed hangs, and a dozen retracted wrong answers that were tested rather than published.
* eighty-ninth pass: characterise the last two unread GFX commands and TMR ↵user2026-09-30
| | | | | | | | | | | | | | | | | | | | | | relocation The coverage document left exactly two handlers unread. Both are now characterised on silicon with a known-rejected command as a validity control. One is implemented and succeeds with a zeroed structure and is input-sensitive, consuming a single dword that must be zero while any other value returns an error, and it writes nothing to host memory. The other returns a constant error for every input and is inert. Neither is a primitive, but the command surface is now fully covered rather than mostly. A second trusted-region setup pointing at an attacker-chosen region is rejected. Together with the range checks verified in the preceding passes, covering both descriptor halves with end-exclusive boundaries and no off-by-one, the trusted region can be neither relocated nor overlapped, so the two ways a host would subvert that boundary are both closed. The objective is not met. Every command handler is now read or characterised on hardware, every load-descriptor field is fuzzed and every application command surface is fuzzed to depth, with the platform's boundaries holding and the defects found being either unreachable or hangs.
* seventieth pass: consolidate into a disclosure-ready report and index ituser2026-09-30
| | | | | | | | | | | | | | | | | | | | | | Sixty-nine passes of evidence existed only as an append-only audit log plus scattered harnesses, so this consolidates them into a form a vendor or reviewer can act on: the defect itself, the provenance of the unvalidated count, why it is not reachable on the parts analysed, the six reproducible artifacts, a suggested fix and the limitations. Both artifacts cited in the report were re-run while writing it, so its numbers are current rather than remembered, with the static verifier passing all fourteen structural claims and the composed emulation reproducing the planted count and the underflowed copy length alongside a benign control. The report is indexed with its non-reachability stated in the index entry itself so the limitation travels with the citation. Final audit: the objective asked for a found primitive, and a defect was found, characterised, verified and reproduced, but a primitive requires a path an attacker can drive and on these parts that path does not exist. Across seventy passes, nine controlled detector classes, both dispatcher families, thirty-one privileged-module builds and seventy-eight application images, the consistent finding is that this firmware contains genuine memory-unsafe code and does not expose it to the host.
* [ST] overturn the SYS_DRV 0x60xx 'no emitter' negative: POWERPLAY emits ituser2026-09-29
| | | | | | | | | | | | | | | | | | | | | | | | | | | The 0x6002 handler was already reversed as a real MMIO-write primitive, up to 64 writes per call, addresses restricted by an allowlist and the 32-bit value unconstrained, and was closed as unreachable because no module emitted the opcode. That scan covered the six modules known then: ASD, HDCP, DTM, RAS, SYS_DRV and sOS. POWERPLAY ships only in AMD's Windows driver train and was not among them, and it emits all three of 0x6001, 0x6002 and 0x6003 across 35 call sites, with the 0x6002 thunk marshalling a buffer pointer and length exactly as the handler expects. POWERPLAY is loadable and invokable on this card, so the emitting module is host-reachable. What is not established, and is stated as such: that a specific command id reaches those call sites, and that host params data reaches the TLV buffer. In fptbl08 the addresses come from internal tables and the values are masked fields of a global config block, so the open question is that block's provenance. A host-supplied PowerPlay table would make the written values host-controlled; fuse or ROM data would keep the primitive internal. Deliberately not tested on hardware. The experiment would drive power-management SMN registers on a live card, which is not a class of write that fails safe, and this project has already spent ATX cycles on a wedged SMU. This is the third standing negative here that was scoped to the then-known module set, after 'exactly four TAs' and 'no TA emits ASD 0x202', so the retraction also records the rule: an emitter-absence claim must name the set it searched.
* fw: restore AMD's own function names and apply prototypes/types across the TAsuser2026-09-29
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Names alone left Hex-Rays emitting *(_DWORD *)(a1 + 7680) for the most important expression in this audit, so this adds function prototypes and structure types, and in the process corrects names that had drifted. GCP logs AMD_TA_GCP: <Name> ENTER on entry to most handlers, which is ground truth. fw/ta_name_audit.py checks every derived symbol against it and found 18 mismatches in GCP, plus two false positives in WSPE where the logged words are ordinary prose. Combined with a fresh decode of the dispatcher's two tbb tables, giving command to handler address rather than the stale name-keyed map, this corrects: cmd 3 is ActivateDRMCertAuthSession and was not defined as a function at all; FE03 to FE06 are unimplemented and share the error arm; FE07 is CreateAuthenticatedSession; FE0C and FE0D are Create and DestroyCryptoSession; FE0A is QueryAuthenticatedSession rather than the invented QueryAccessibilityAttributes; FE0E and FE0F likewise. The hardware sweep independently agrees, since FE03 to FE06 all returned an identical 0x102 while FE07 alone returned 0, which had been sitting in the output mislabelled. This also corrects a finding's wording: FE0B manages the stride-80 auth array and FE0D the stride-88 crypto array, so they are two destroy paths over different arrays rather than a same-type sibling pair. The in-database comment is updated, including replacing its now-stale 'not yet shown' caveat with the hardware result. Five structures declared and verified by reading the layout back from IDA and comparing against the addressing the code performs; the three session arrays tile the context exactly, which is the check. 179 prototypes applied across all twelve TAs with no failures. Measured against a pristine database rather than one that already carries the work: raw casts 81 to 7, anonymous parameters 113 to 0. Method notes recorded: do not drive the TUI struct editor, since a focus change types into the filter box and a half-applied redeclare silently truncated three structs; declare interdependent structs in one parse unit; build prototypes from the database rather than the stale dump; a type that misrepresents addressing is worse than none, which is why the lvar retype was reverted; and two metric bugs were caught only by disbelieving a number that flattered or unfairly punished the work. Adds fw/ta_ida.py, fw/ta_name_audit.py, fw/ta_rename_from_logstrings.py, fw/cmdmaps/gcp_ea.json, fw/audits/name_vs_logstring.json and fw/pseudo/ artifacts.
* [HW] new finding: GCP crypto-session acquire gate bypassed by retryinguser2026-09-29
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | gcp_cmdFE0C commits the ctx+0x1E00 refcount increment before attempting the ASD acquire and does not roll it back when the acquire fails. A failed create therefore leaves the count at 1, and the next create sees a non-zero old count, takes the cbz fast path, skips the acquire entirely and succeeds. The gate only ever guards the first attempt. Proven on hardware with ASD deliberately not loaded: FE0C #1 is correctly refused with 0x2000002f, FE0C #2 and #3 return 0 with handles in slots 1 and 2, and the resulting sessions are usable via FE0F and FE0D. Control: FE0C as the first command on a fresh context does refuse, so the refusal is real and only priming defeats it. A failing cmd 1 primes it equally well. Found by accident, and the accident is worth recording: a differential sweep showed FE0C succeeding in the no-ASD configuration, contradicting direct measurement, and the contradiction was the bug, because command ordering in the sweep primed the count. Class is security-gate bypass and state desync, not memory corruption. This is the mirror of the FE0D decrement-on-rejected-handle defect, so the reportable root cause is that the counter is maintained outside the operations it tracks, in both directions. Also folds the ASD-load correction into the authoritative audit, with two retractions of my own claims recorded: the UAF reading is ruled out by handle rebinding on slot reuse, and the 0x202 disclosure verdict was checking only that bytes changed when GetContentKey is supposed to write there. Adds hw/gcp_acquire_gate_bypass_poc.py and hw/gcp_asd_differential_sweep.py.
* psp_out: fold the twelve-TA round into the authoritative audit and ↵user2026-09-29
| | | | | | | | | | | | | | | | | | | | disclosure summary The two GCP findings and the whole twelve-TA round existed only under fw/, so the authoritative documents had zero mentions of them and would have been read as the complete picture. Added a TWELVE-TA ROUND section to VULN-AUDIT-WHAT-WE-ACTUALLY-HAVE.md in the same verdict-first style as the GPU-engine round, and a corresponding note to the disclosure summary's scope block. Content: all twelve TA types load and invoke on this Navi 31 with controls, which corrects the standing exactly-four-TAs assumption from a fact about psp_13_0_0_ta.bin into a fact this repo had wrongly generalised to the platform; the two confirmed GCP lifetime defects with their hardware evidence and the FE0B-versus-FE0D asymmetry that makes FE0D a defect; the ASD reachability correction, since GCP emits 0x202 and OPM emits 0x301 and the old dismissal rested on a four-TA scan; the three leads raised and closed downward, FE09, ASD 0x301 and MERIT cmd05; and the single remaining blocker, asd_rpc_305(1) failing without real DRM key material, which gates four separate results.
* fw: reverse-engineer and annotate all twelve newly-hittable TAs in IDAuser2026-09-29
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | All twelve TA types this card loads and invokes are now opened in idalib via idatui, annotated and saved, with their command surfaces recovered. Three tools carry the pass. ta_symbols.py statically recovers the info header and the five TEE entry points from the ARM-mode trampoline, with a selftest against HDCP 23.0.4.49 that must reproduce the dispatcher address and the hand-read command set. It deliberately does not name command handlers any more: its jump-table walk guessed the first bl after each arm, which is wrong when the arm sets up arguments, and those wrong names persisted in the database and shadowed the correct map, so every TA was re-analysed from a deleted database once that was found. annotate_ta.py takes the map from the decompiler's own output and applies it by address, handling switch/case including the 1u suffix form and nested two-tier switches, if/else-if chains, wrapper entries that tail-call the real dispatcher, and callees that are already named. A third step promotes loc_ labels to functions, because handlers are often reached as loc_XXXX)(args) where IDA never made a function and the decompiler shows a cast-through-label that cannot be renamed; that step alone took OPM from three recovered commands to nine. Renaming is by address rather than token, since drive mv resolves names against the current view and silently failed for six of fourteen GCP handlers. Surfaces: GCP 21 commands, XGMI 12, OPM 9, POWERPLAY and FP and RAP 7, AUC and MERIT 6, CSU 3, OC 2, WSPE 2, and TMZ none since it is a stub. GCP is the most interesting of the twelve and is now fully named from its own 69 log strings: it is a DRM content-protection TA with a low family for session lifecycle and content keys and a high 0xFE0x family for authenticated crypto sessions, key exchange and key refresh. WSPE is fully resolved too, and shows that the cmp constants the static pass saw were error codes rather than command ids.
* fw: triage the twelve unaudited TA types this card accepts; TMZ is a stub, ↵user2026-09-29
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | RAP mapped LOAD_TA accepts sixteen TA components on this Navi 31 and only four were ever in scope, so this is a first pass over the other twelve, using a generic triage tool validated against hdcp_23.0.0.82 first -- it returns the entry, session table and guard addresses that GUARD-INIT-CHAIN.md recorded from IDA. Recording a trap: the entry trampoline is ARM while the rest of the image is Thumb, and decoding it as Thumb produces fluent-looking garbage rather than an obvious failure. All twelve are class TA rather than DR. That distinction is what made several ASD defects unreachable, since the PSP refuses to invoke a driver-class module, so these carry the class that is invokable and all declare a stack guard. TMZ, the most alarming name in the set, is an empty stub: six functions of two to four bytes each and all five TEE entry points returning zero, in both shipped builds. It authenticates and loads and does nothing, which is worth stating plainly so the name does not attract effort. RAP is the Register Access Policy validation TA matching amdgpu_rap.c. Its invoke is a thin wrapper that memsets, dispatches, and writes back cmd with bit 31 set; the dispatcher special-cases command 8 to pick one of four range/policy pairs before validating, and otherwise switches over a table for cases one to six. OPM, MERIT and GCP are the large, actively growing ones and are the obvious next targets. Explicitly not established: whether the host can invoke any of these. Class magic plus a successful load is an inference, and ASD is the precedent for a TA that loads cleanly and still refuses every invoke, so that is a hardware test to run before calling any of this attack surface.
* HDCP: map the oldest TA build (23.0.4.49) and sweep the overflow shape ↵user2026-09-29
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | across 17 builds Opened the oldest HDCP image we now hold -- 23.0.4.49 from the RX 7900 launch driver -- in idatui, wrapped at the project's usual TA base 0x400100. Recovered the info header (entry 0x404514, session table 0x406000, guard 0x4045a8), the TEE entry trampoline and its five entry points, and the invoke dispatcher's command set. Corrects my own count from the previous turn. I said eight new HDCP versions; it is five. The comparison globbed only psp_out/versions and missed versions_lfw, where blobs carry hash-based names, so 23.0.0.67, .73 and .77 were already in the corpus as hdcp_or_ras_* files. The harvest still pays for itself there, because the Windows metadata resolves that ambiguity and names them. Then swept the documented subcmd12 shape -- a five-times multiply whose multiplier is a raw byte load -- across all seventeen HDCP builds. Every build from the December 2022 launch image to the newest we hold has exactly four such sites; the addresses only shift with code motion and freeze once the layout settles at .64. The pattern was never fixed, so the overflow is not a later regression but present from the first shipped image. Worth recording how easily this went the other way: the first site I looked at in the oldest build was a ubfx r6, r0, #4, #5, a genuinely bounded 5-bit DEVICE_COUNT path, which reads as evidence the old build was safe. Bounded and unbounded paths coexist in every build, so a single site supports either conclusion and only the full enumeration settles it.
* fw: harvest all 53 Windows driver versions -- 110 new firmware images, 17 TA ↵user2026-09-29
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | types Ran the download/extract/carve/delete loop over every Adrenalin package for this card, 22.12.1 through 26.7.1, 53 of 53 processed with no failures. Recording a silent failure mode that nearly cost 30 packages: the first run reported "NO amdkmdag.sys" for every version from 23.7.1 onward. The cause is that 22.x-23.5.x are NSIS self-extractors while 23.7.1 and later are PE32+ 7-Zip SFX with the archive embedded around 0x1b3e00, and 7z -tnsis exits cleanly while extracting nothing from those. The harvester now tries both formats and retries versions previously recorded as failed. Dedup needed correcting too. The Windows sys_drv 0.49.0.51 differs from the linux-firmware build of the same version in exactly 256 contiguous bytes at the end of the body, which is the RSA-2048 signature -- identical code, different signature, as expected for randomised PSS. Counting on content excluding that trailing block gives 114 distinct contents from 117 Windows files, 4 overlapping the existing corpus, so 110 genuinely new firmware images rather than the naive count. Two groups inside the Windows set differ only by signature, including three copies of RAS 27.0.0.22, so AMD ships the same image re-signed; that is a useful data point for the rollback finding since multiple valid signatures exist over identical content. The corpus now spans 17 component types and 142 unique bodies, including CSU and OC which only appear in later drivers, so the TA inventory grows across the train. Also corrects the apu tag decode from the first pass, which was byte-reversed: the tags are NV31, NV33, NV24 and MI20. Seven sys_drv builds carry NV31, our exact silicon, and six of them are versions linux-firmware never shipped (0.49.0.35/.38/.40/.42/.46/.53), interleaving with the Linux set to give a near-contiguous .35 to .54 range for version diffing. No sOS images appear in Windows packages. Bodies, full signed containers, per-driver records and manifest hashes are all stashed.
* fw: carve PSP firmware from AMD Windows drivers; 10 TA types the Linux ↵user2026-09-29
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | corpus lacks Our firmware corpus came from linux-firmware. AMD's Windows Adrenalin packages carry their own PSP blobs inside amdkmdag.sys, from a different release train. This is the first pass over that source, starting with the oldest package for this card (22.12.1, the RX 7900 launch driver). The previous-drivers page serves its links in HTML, so no JS is needed; direct fetches 302 to Download-Incomplete.html but a Referer plus browser UA gets 200 and the CDN honours Range. The installers are NSIS, so 7z -tnsis extracts them. Firmware is concentrated in amdkmdag.sys, with 639 $PS1 hits against 31 in the next-largest file. Carving reuses the rules this project already got wrong twice: header 0x10 before the magic, body at container+0x100 of length 0x100+size_fw_signed, and validation of the module header plus load base. That matters here too -- of the raw $PS1 hits only 19 are module containers, 596 are headerless and 24 fail sanity. The carve has a positive control: the extracted sys_drv 0.49.0.35 body is byte-identical to an existing versions_lfw file, so an independent path reproduces a known-good body. The container version field is useless for TAs (all 0.7.0.1); the build number AMD names TAs by lives in the property table after the key amd.ta.version as little-endian {build, patch, minor, major}, verified against corpus hdcp and asd files. gpd.ta.description yields readable names. Result: 19 module bodies, of which 18 are new to the corpus. Three sys_drv builds carry ASCII apu tags NV13, NV33 and MI02, so the package spans ASICs and only the NV13 one is ours. More importantly the TA set includes ten types the Linux bundle never shipped -- OPM, MERIT, GCP, WSPE, TMZ, AUC, FP, XGMI, RAP and POWERPLAY -- alongside new build lines of ASD, HDCP, DTM and RAS. That makes the project's "exactly four TAs" result source-scoped rather than wrong: it holds for psp_13_0_0_ta.bin and not for AMD's TA inventory. Which of these target Navi 31 is NOT established, since TA containers carry no ASIC tag, so the next step is a LOAD_TA accept/reject sweep on the card.
* ASD 0x202: two new negative axes, and name the question that actually decides ituser2026-09-29
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | The not-host-reachable verdict rested on hardware invoke attempts and emitter scans over TA SVC 0xF2 sites. Emitter scans have already been wrong once here (the old scanner missed STM-form opcode stores), so this round tested different axes instead of re-running the same idea. Handler aliasing: an emitter scan asks only whether a TA emits 0x202 and cannot see a second opcode dispatching to the same handler. Decoding ASD's tbh table for the 0x2xx group (index = opcode - 0x200, 15 entries) shows 0x202 has a unique arm at 0xd0178e, the group's only alias is 0x20c/0x20d, and the three opcodes HDCP actually emits (0x200, 0x201, 0x208) each land on their own distinct arms. Indirect routes: across the whole ASD image sub_D14948 has exactly one direct branch, the dispatcher arm itself, and zero pointer-table references, so there is no function-pointer or vtable path either. Both axes are negative and strengthen the existing verdict rather than overturning it. Also recorded: ASD file offsets are VA - 0xD00000, not VA - 0xD00100; the 0x100 container-header trap produced a convincing garbage disassembly before that was caught. More importantly, the finding's value is dominated by an unresolved conditional rather than by reachability. Its impact claim depends on whether the loader guard is a per-boot cookie shared across TAs or per-TA random; if per-TA, both this leak and the HDCP subcmd12 upgrade path die at once. GUARD-INIT-CHAIN.md has the mechanism (sOS patches the TA's declared guard slot via SYS_DRV opcode 0x3008 at process creation) but not the answer. Two attempts this round failed: the 0x3008 handler is not visible to a constant search inside the high dispatcher, and an emulator boot leaves neither TA guard page resident. Recorded as the deciding question for whoever picks this up.
* Disclosure summary: correct 0x3103 reasoning, bound the VCN half, surface ↵user2026-09-29
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | GPU findings The consolidated disclosure document had drifted behind the audit in four ways. Section 2.1 cited the ACL capability check as an additional gate on the 0x3103 overflow. That reasoning is wrong: the host-request context demonstrably holds that capability, since GFX cmd 15 reaches SYS_DRV opcode 0x3127 inside the same ACL range and returns real data. The verdict survives on stronger grounds -- no host path selects a SYS_DRV opcode and no TA forwards one, and the stored copy length is pinned at zero by the allocator/enqueuer/task loop, making the copy invariantly 76 bytes. Same argument closes the 0x3102 direction. The C2P coverage correction still said the TSU mailbox at C2PMSG_115/116 was never probed. It has been: the mailbox is host-reachable, but the disclosure chain behind it is not, on every path tested. Finding 1.10's VCN half was stated as unbounded register writes. It is now bounded by measurement: the port is regUVD_DPG_LMA_CTL/_DATA, the eight firmware-overwritten entries are exactly the ucode window registers, index 195 is VCPU_CACHE_SIZE1 capped at 1 MB, and on hardware an accepted call with 56 host-controlled entries changed nothing with the register file proven accessible. The risk is injection into a legitimate DPG bring-up, not a standalone write, and the citation guidance says so. Finally the scope note pointed at the GPU audit without naming anything. It now lists the five reportable GPU-side items with their evidence classes and leads with the reachability framing, so a vendor reading only this file is not left with ten findings that look like ten exploits.
* VCN DPG round 2: LMA writes latch nothing reachable; false positive caught ↵user2026-09-29
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | by control With VCN registers accessible, the question was what makes an LMA write latch. Five variants, all negative, and the controls are what make that meaningful. Method control first: registers survive a gate cycle (GP_SCRATCH0 written by MMIO while un-gated still reads back after DPG on and un-gate again, and in fact survives a PowerDown/PowerUp pair). So write, gate-cycle, read is a valid observation and an absent value means the write did not land. The variants: DPG entered from the gated state as amdgpu does; VCPU_CNTL programmed with CLK_EN and BLK_RST via LMA before the marker; LMA_MASK all ones with mask_en set; LMA writes while un-gated with no DPG; and a different target register. All used correct internal indices and targets with MMIO writability proven separately. None landed. Variant 2 initially looked like a hit -- VCPU_CNTL read 0x1ff00000 after the LMA write while everything else stayed zero. It was not. That register reads 0x1ff00000 unconditionally, including right after an MMIO write of 0 or 0x200, so the apparent value was a fixed read-only pattern that happened to resemble the 0xFF<<20 field being written. Recorded because it is exactly the sort of coincidence that gets published, and only the before/after control stopped it. Since the PSP's type-49 entries go through this same port, this independently corroborates the earlier aperture diff: no register effect from an idle block. What remains untested is now sharply defined -- a running DPG state machine requiring the full vcn_v4_0_start_dpg_mode with LMI config, cache BARs pointed at real buffers, MASTINT, ring and the VCPU executing ucode. The PSP pinning exactly those cache/BAR registers is consistent with it expecting to own that bring-up itself.
* VCN DPG round: un-gate VCN registers, explain the old constant-window, sound ↵user2026-09-29
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | negative Working the DPG blocker produced a capability the project lacked plus a trustworthy measurement. First, PowerUpVcn does not make VCN readable. On a freshly cycled card every dword of the aperture reads 0x0000302c before power-up -- identical to GRBM_STATUS, i.e. stale bus data. That is the real cause of the historical "2560 registers return one constant and drift" pathology that voided earlier rounds. After PowerUpVcn, 64/256 dwords still read 0xdeadbeef including the LMA port itself, so the earlier "LMA echo" of idx<<8 was a bus artefact (CTL>>8), not a shadow file responding. Porting the else branch of vcn_v4_0_disable_static_power_gating fixes it: all 14 PGFSM sub-block configs to 1 then UVD_POWER_STATUS &= ~0x103 takes deadbeef from 64/256 to 0/256, and the target register round-trips 0x1234abcd through MMIO. The VCN register file is readable and writable here for the first time. With that, the diff experiment is finally valid. GP_SCRATCH0 is reachable both as DPG internal index 94 and MMIO dword 0x7f1e and is not firmware-fixed, so it is the ideal target. Un-gated, 763 of 1024 dwords decode, the no-command drift baseline is one known free-running counter, and an ACCEPTED type-49 call carrying 56 magic entries changes only that counter, with zero registers holding the magic. Host LMA writes at idx 94 also fail to reach the register, both un-gated and written while DPG-gated then read after exit. Consistent reading: LMA writes are latched only by an actively running DPG state machine and applied at a resume that never happens on an idle block. So the residual risk narrows to injection during a legitimate VCN DPG bring-up rather than a standalone write from idle, and showing that needs a full vcn_v4_0_start_dpg_mode port, which is a driver port rather than a probe.
* VCN type-49 closure: target space identified as the DPG shadow register fileuser2026-09-29
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | The open item was that the target registers were unidentified, so nobody could say what the writes do. Answered from the firmware's own data rather than more hardware poking, which was never going to resolve it. vf_table_lookup picks a 0x1c-byte entry by fw_type and region_window_open uses a1[5] as DATA and (a1[6] & 0xFFFFF) + (a1[5] & 0xFFF00000) as INDEX. That table lives in .bss, so it was read at runtime in the emulator after SYS_DRV init: VCN0 gives dwords 0x7e12 and 0x7e11, which are regUVD_DPG_LMA_DATA and regUVD_DPG_LMA_CTL, and VCN1 gives the same pair on its own IP-discovery base 0x12000. The DPG/LMA guess this file carried for several rounds is now established, and it explains the sound MMIO negative: the writes land in the shadow file, which the MMIO aperture does not expose. The host index is an internal IP address, not a register offset. Inverting the kernel's SOC15_DPG_MODE_OFFSET maps the eight firmware-fixed indices onto exactly the VCN ucode's memory window -- VCPU_CACHE_OFFSET0/1, CACHE_SIZE0/1, LMI_VCPU_CACHE_64BIT_BAR_LOW/HIGH and CACHE1_BAR_LOW -- and the single bounded entry, index 195 rejected above 0x100000, turns out to be VCPU_CACHE_SIZE1 bounded to 1 MB. A bound previously known only as "4*index == 780" being semantically a cache-size check is independent agreement that the mapping is right. This corrects my own first decode in the same session, which read the index as a BASE_IDX 1 register offset, named 192 as UVD_TOP_CTRL and would have claimed the cache DMA base was host-controlled. It is not; the loader pins it. So the boundary is narrow but real: the signed loader pins where the VCN VCPU fetches its firmware, while the NC0/NC1 BARs and LMI_VCPU_CACHE_VMID stay host controlled. Pinning an address but not the address space it translates through is the most promising direction left. Impact moves from open-ended to bounded, and the remaining blocker is now specifically DPG-mode bring-up to show the shadow values take effect.
* VCN type-49: validated read channel, first sound write-observation, two new ↵user2026-09-29
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | preconditions Every earlier write-observation in this finding was void: the host MMIO window returned a single constant while drifting between identical snapshots. This round rebuilds the measurement on a channel proven before it is used. The channel is SDMA POLL_REG_WRITE_MEM, which makes the SDMA engine read a register on-die and DMA it to memory we own, with a positive control every run (GRBM_SCRATCH_REG0 round-trips 0xfeedface). VCN0's bases now come from the card's IP-discovery table rather than SMN/4 arithmetic: 0x7800 is dead (all 0xdeadbeef) and 0x7e00 decodes, with host and SDMA agreeing exactly. Two preconditions nobody had recorded. The type-49 table is rejected 0xffff0007 until the VCN ucode is loaded via type 0x0D, after which the same bytes return 0x0 -- the original campaign had it loaded by an earlier step. And PowerUpVcn must be paired with PowerDownVcn or the driver's fini path wedges the SMU, which cost two ATX cycles before it was understood. With gate verified open, ucode loaded and the call accepted, a 1024-dword diff of the decoding aperture against a no-command drift baseline of one register showed only that same free-running counter move and zero registers holding the magic. That is the first write-observation here backed by a positive control, a confirmed-decoding aperture and a confirmed-accepted call, and it says the writes do not land in the host-visible VCN0 register file. The DPG/LMA hypothesis matches the kernel macro's shape exactly, and the port sits in the live aperture, but LMA_DATA echoes the index (DATA = off<<8) and no host round-trip sticks with or without SRAM_SEL, so it is a latch artefact rather than a shadow file. Worth noting the echo yields 64 distinct values over 64 offsets, so a distinct-value control passes on pure echo; the test that caught it is whether the value is a function of the address. Testing the hypothesis properly needs DPG-mode bring-up, which tinygrad does not implement. Capability remains demonstrated, writes remain unobserved but now on a valid detector, and impact remains open with a newly measured boundary.
* SYS_DRV 0x3103: close reachability -- the stored length is pinned at zerouser2026-09-29
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | The open question was whether host-controlled data can reach the sysdrv_rpc_3103 call inside the sOS service task. Resolving the slot life-cycle and the task loop answers it more strongly than expected. All four sites referencing the slot table are accounted for: init memsets it, the allocator sets state 1 and explicitly zeroes the stored length at +0x28, the 0x3102 handler copies slot[10]+76 out and sets state 3, and the 0x3103 handler copies slot[10]+76 in and sets state 4. The allocator's only caller is the enqueuer at 0xe1856c, which fills the slot, does a bounded 0x20-byte copy to +0x2c, sets state 2, signals, and waits for state 4. Nothing on that path ever writes +0x28. The sOS task then closes the loop onto itself: it fetches the slot into a stack buffer with opcode 0x3102 and writes that same buffer back with opcode 0x3103 (stub 0x12b24, confirmed by its movw immediate), touching only buf+0x20 and scratch in between. So a1[10] is whatever came out of slot[10], and 0x3103 stores it straight back -- a fixed point at zero, making both copies invariantly 76 bytes. Arming the overflow requires invoking 0x3103 directly with a chosen a1[10], which is what the emulator PoC did. That needs an actor able to emit an arbitrary SYS_DRV opcode; the host selects GFX commands that map to fixed rpc stubs, and no TA issues 0x31xx or forwards a host opcode. The same reasoning closes the 0x3102 sOS task-stack direction. Also fixes this file's internal contradiction: it carried both a "definitively closed" section and a later update reopening it. The ACL capability is indeed held on the host path, so that old closure's reasoning was wrong even though its verdict survived; emission and the pinned length are the real blockers.
* TSU round 7: live mailbox survey -- no command populates the gating tableuser2026-09-28
| | | | | | | | | | | | | | | | | | | | | | | | | | | | Round 6 called the survey "not live" because every command produced identical 1280-instruction traces. The observation was real but the cause was mine: the setup that writes the command word sat inside a try whose exception was swallowed, so every run executed with the same stale word. The dispatcher was never gating anything. Verified the setup explicitly this time -- ctx = *(0xe2579c) = 0x3010000, the write to ctx+0xacc reads back, and dispatch extracts the expected id -- and the survey is now live, with differing instruction counts (7864 to 20002) and differing path hashes per command as the control; cmd 4 runs the full 20002 instruction parse flow. Result: none of the twelve reachable TSU mailbox commands writes into the table range, and the bound word stays zero for all of them. That makes four independent checks agreeing the gating table is never populated on any path we can drive: init write-watch, cmd-4 chain write-watch, a single static reference (the consumer itself), and this survey. So tsu_verify_region fails in every host-reachable configuration and the a6=0 disclosure site is not reachable through tsu_write_active_slot from the mailbox. The code path and its attacker-controlled source remain confirmed and were never debunked, but reachability moves from unproven to negative-on-all-tested-paths. That is a downgrade of the tree's most-serious-by-potential-impact item, and both the authoritative audit and the index are updated to say so.
* TSU round 6: void the mailbox survey -- it failed its own liveness controluser2026-09-28
| | | | | | | | | | | | | | | | | | | | | | | Round 5's next step was to survey the other TSU mailbox commands for one that populates the gating table at 0xe33a7c. The survey ran commands 1-12 and 18-23 through tsu_mailbox_dispatch with a write-watch on the table range and reported zero writes and a zero bound word for every one of them. That negative is void. The liveness control shows every command executing exactly 1280 instructions and 259 memory writes -- identical regardless of the command id -- so the dispatcher never reached its per-command switch and no handler was exercised. It bails earlier, presumably on TSU mailbox state that sysdrv_env does not model. Recorded rather than dropped because a table of "0 writes across 18 commands" reads as strong evidence while actually measuring nothing, which is precisely the failure mode this tree keeps having to correct. Round 5's three checks are unaffected, since each had a working control: the init write-watch, the cmd-4 chain write-watch, and the single static reference to the table. Reachability therefore remains leaning-negative rather than settled, and the real next step is modelling the dispatcher's entry gate so a survey can actually run.
* TSU round 5: answer the open question; the gating table is never populateduser2026-09-28
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | Rounds 3-4 chased the wrong address and this corrects them. 0xe0e030 does not walk the table its caller passes in r0; it loads its own base from a literal at 0xe0e0b0 = 0xe33a7c, which is past the image end and therefore .bss. The caller's 0xe264fc is the 16-byte search key, and the image does hold a GUID there. So the loop bound is [0xe33b7c], measured at the loop head as zero with cursor 0xe33bcc, which exits immediately. The open question -- who writes it -- is answered negatively by three independent checks. A write-watch across SYS_DRV init sees 64 writes in the range, all from the memset/memcpy pair, and never the bound word. A write-watch across the whole cmd-4 chain sees zero writes into the range, with verify as the first event. Statically there is exactly one reference to 0xe33a7c in the image, the lookup itself, no movw/movt pair forms the range, and the two nearby referenced literals are key material rather than a table base. This flips the burden of proof. It had looked like the verify abort was an artefact of an unexercised init path; it now looks structural, since nothing in SYS_DRV writes or even mentions the table besides its consumer. Either another component populates it on a real part, or tsu_verify_region always fails here and the a6=0 disclosure site is unreachable via tsu_write_active_slot on hardware too. The second is now the more economical reading -- the first evidence pointing at unreachable rather than unproven. The finding's code path and attacker-controlled source are unchanged and still not debunked; only the reachability assessment moves, and the authoritative audit is updated to say so.
* TSU round 4: no writer found for the bound word; correct round 3's over-readuser2026-09-28
| | | | | | | | | | | | | | | | | | | | | | | | | Searched both components we hold for whatever sets the zero word at 0xe265fc that makes the lookup exit immediately. No pc-relative literal in SYS_DRV references it (11 distinct values in that range, 26 sites, none of them this address), no movw/movt pair in SYS_DRV or sOS forms any address in the range, and sOS contains no literal there at all. Correcting round 3 in the process: it called 0xe2664c the registry's first entry, but that memory is read by a function at 0xe148e0 as an array of register addresses, indexed by a bounds-checked id and fed to the SVC 0x7C register-write call, with +0x40 and +0x80 sub-tables used the same way. So the "GUID registry" framing over-read the layout. The mechanical fact that decides the abort is unaffected: end = (base+0x100) + [base+0x100] = 0xe265fc, which is below the cursor 0xe2664c, so the walk exits on the first pass and returns its error constant. Net: the disclosure code path and its attacker-controlled source stand; the verify step is still not established as a gate, since its failure here is that zero bound word rather than any security decision; and the site is still not shown reachable past it. The open question is now narrow -- who writes [0xe265fc] -- and it is not written via either common addressing idiom in the two components we hold.
* TSU: the verify abort is an empty registry, not a security checkuser2026-09-28
| | | | | | | | | | | | | | | | | | | | | | | | | | Chased the 0xe0e030 lookup to its root. Modelling it as returning success is not sufficient and says so: verify's error clears but the run then dies in memset at a near-null pointer, because the lookup's out-parameters carry the located entry's address and size and a bare return 0 leaves them garbage. Fabricating them would test the fabrication, so it was not done. The real cause is structural. The lookup walks a length-prefixed registry whose end pointer is (base+0x100) + [base+0x100], with base 0xe264fc. In the shipped .54 image that length word is zero, so end (0xe265fc) is below the first entry (0xe2664c), the loop body never executes, and the function returns its error constant on the first pass -- which is exactly the 54-instruction, no-crypto trace observed earlier. Scanning every pc-relative literal load in the image for 0xe264fc finds exactly one reference, the call site inside tsu_verify_region itself, so nothing in SYS_DRV populates the registry via a literal; it is filled by another component or through a computed pointer. So the abort is not a signature check, not a size check, and not a judgement on our unsigned image. The verify step is still not established as a gate, and the flow still cannot be shown proceeding past it. The blocker is now a single named word -- the registry length at 0xe265fc -- and the next step is to find its writer, run it, and re-run the gate harness.
* TSU disclosure: investigate the verify gate, and propagate the caveatuser2026-09-28
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | The question was whether the TSU memory-disclosure finding had ever been debunked. It has not -- but it carried an unresolved reachability caveat that none of the three summary documents mentioned, so all three now do. New harness tsu_verify_gate.py runs the chain twice, differing only in whether tsu_write_block's body executes. Stubbed, the a6=0 disclosure site is reached twice; with the real body running, the flow stops at the earlier a6=5 step. So the original demonstration does depend on its stub. Found and fixed a base error while investigating: pspemu maps sys_drv at 0xE00100, not 0xE00000, so an earlier static read of tsu_verify_region was 0x100 off and decoded it as a struct validator whose first instruction rejects the observed argument. That reading was wrong. This is the third 0x100 offset error in this tree, after the DMCUB load base and the MEC PM4 table. At the correct base the abort is measurable and is neither a signature nor a size check: the bounds tests pass (stack arg 5 is the 2 MiB flash size, and 0x200000-0xe0000 >= 0x100000), and the failure is a call to 0xe0e030 -- a lookup that walks a table comparing 16-byte keys and returns an error when nothing matches. Its table lives in SYS_DRV's own data at 0xe264fc, i.e. runtime state our partial boot never populates, which also explains the old "no crypto SVCs issued" observation. Status is therefore: code path confirmed with an attacker-controlled source, reachability past the earlier verify step unproven, blocker now identified as an unpopulated lookup table rather than an unknown. Next step is bounded and needs no hardware: populate that table and re-run harness B.
* MEC/PM4 round 10: no firmware privilege check on MAP_QUEUES; thread closeduser2026-09-28
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | Took the cheap static route rather than more KIQ porting, and it settles the question. Round 5 established on hardware that priv_state is bit 31 of CP_HQD_PQ_CONTROL and kmd_queue bit 30, so a firmware-side check has to extract those bits. Walking the CFG from the validated MAP_QUEUES case label 0x3241c, following branches and descending into callees to depth 3 (3984 instructions), every candidate idiom resolves to something else: the two srli/srliw 0x1f are a ring-slot phase wait (lw; srliw 31; bne back to the same address), the nine srli 0x1e are masked index arithmetic ((idx & (count-1)) * 4), and the eleven lui 0x80000 set the bit into an outgoing descriptor rather than testing it. No test of the submitting queue's privilege exists anywhere on the path. Combined with the hardware results -- round 5's register write refused with the queue faulting, which is a hardware signature rather than a firmware branch, and round 6's MAP_QUEUES not serviced on a compute ring at either privilege level -- the reading is that PM4's privilege boundary is enforced by hardware and packet routing, not by checks inside MEC. That is weaker than SDMA's explicit RB_PRIV gate, and it is recorded as a conditional: anything that gets a packet to the HWS path from a queue hardware would not have routed there meets no second line of defence in firmware. Caveats kept: the closure starts at a case label in a shared dispatcher (which makes an absence stronger, not weaker), callees were followed only to depth 3, and the search is idiom-based, so a check written as a table lookup would not have been caught.
* MEC/PM4 round 9: stop the KIQ port on cost/benefit, name the cheaper routeuser2026-09-28
| | | | | | | | | | | | | | | | | | | | | | | Ported the last two prerequisites from gfx_v11_0.c: RS64 compute enable turns on all four pipes together with MEC_HALT=0 (CP_MEC_RS64_CNTL 0x04000000 -> 0x3c000000), and kiq_init_register programs CP_MEC_DOORBELL_RANGE_LOWER/UPPER, outside which the CP silently ignores doorbells. Neither closed the gap. With the right slot, CP_HQD_ACTIVE=1, the scheduler pointed at the queue, all pipes active, the MEC unhalted and the doorbell range programmed, the queue still does not retire a NOP. The remaining candidate is the MEC firmware's own per-pipe initialisation, since amdgpu loads MEC microcode per pipe and the RS64 MEC likely only services pipes initialised at load time -- a substantially larger port. Cost decides it: each failed attempt leaves an undequeueable HQD and GRBM_STATUS 0xa800302c that only a cold ATX cycle clears, and three have now been spent on one open question. Stopping the thread. The cheaper route is recorded for whoever picks it up: MAP_QUEUES sits at 0x3241c in the validated PM4 table, inside the ~12 KB dispatcher sub_31360, and whether any path from that label consults a privilege bit is a bounded static reverse with no hardware cost and no power cycles.
* Record the stuck-HQD hazard from the KIQ port, and the ATX recoveryuser2026-09-28
| | | | | | | | | | | | | Round 8's pipe-3 queue activates but never fetches, and it cannot be dequeued: CP_HQD_DEQUEUE_REQUEST leaves CP_HQD_ACTIVE = 1, GRBM_STATUS stays at 0xa800302c against a 0x0000302c baseline, and neither AM_GFX.reset_mec() nor a full driver re-init clears it. A cold ATX cycle does, after which GRBM_STATUS is back to baseline and the HQD reads inactive. Added to CONSTRAINTS.md as a standing hazard, since the failure is silent from the driver's point of view -- the card keeps answering, sOS and SMU stay alive, and only GRBM_STATUS shows it. Bring up compute pipes only when a power cycle is acceptable, and check GRBM_STATUS afterwards rather than trusting the re-init.
* MEC/PM4 round 8: port amdgpu's KIQ slot selection; queue activates, still stallsuser2026-09-28
| | | | | | | | | | | | | | | | | | | | | | | | Continuing round 7's blocker by reading amdgpu's KIQ bring-up and porting it. Three concrete fixes. The KIQ slot was wrong, not merely unallocated: amdgpu_gfx_kiq_acquire scans the queue bitmap downward requiring queue == 0, and gfx11 has num_mec = 1, so ring->me = mec + 1 = 1. tinygrad hardcodes me=2/pipe=1, a VF layout, which is exactly why round 7 saw CP_HQD_ACTIVE = 0. The real slot is me=1 pipe=3 queue=0, reachable through setup_ring's ordinary path at idx=12 so the MQD is still built by driver code -- CP_HQD_ACTIVE now reads 1. The HIQ pointer encoding is RLC_CP_SCHEDULERS = (me<<5)|(pipe<<3)|queue|0x80 per gfx_v11_0_kiq_setting. And because gfx11 is RS64, per-pipe enables live in CP_MEC_RS64_CNTL rather than CP_MEC_CNTL; it read 0x04000000, only pipe0 active. Still stalled: with the right slot, an active HQD, the scheduler pointed at it and pipe3 enabled, CP_HQD_PQ_RPTR stays 0 while the doorbell and CP_HQD_PQ_WPTR_LO both reach the queue. A pipe-0 queue on the identical code path retires the same NOP, so the submission mechanism is sound and the gap is pipe-3 specific -- most likely the per-pipe HPD/EOP programming from amdgpu's compute resume. CP_MEC_RS64_CNTL restored to its original value. MAP_QUEUES remains untested, but the slot, scheduler encoding and pipe-enable register are now known and verified, leaving one named piece.
* Consolidate the GPU-engine campaign into the authoritative validity audituser2026-09-28
| | | | | | | | | | | | | | | | | | | | | | | | | | | | VULN-AUDIT-WHAT-WE-ACTUALLY-HAVE.md is the file this tree treats as winning when others disagree, and it covered only the PSP components -- so it implied we had no DMCUB, SDMA, MEC, MES or SMU results at all. It now carries a GPU-engine section on the same reachability axis the PSP tiers use. The verdict goes first, because the count is misleading without it: none of the GPU-engine findings is exploitable by an unprivileged process. They all need the DMUB inbox, which is kernel-only under amdgpu, and the hostile field is driver-chosen in every case. Recorded with their real worth rather than their headline: FINDING-10 is a timing oracle over PSP-protected memory, bounded by its own CW0 sweep (the content is public firmware); FINDING-1 is confined to a window the host already owns, which the PSR work lowered rather than raised; FINDING-8 matters mainly because it needs no power cycle, unlike sOS rollback; FINDING-9 is [ST] and not on this ASIC. The boundary results are listed as first-class output, not as absence of findings: SDMA gated across write, read-to-memory and read-by-timing; MEC/PM4 gated, established as a 2x2 with the memory form as encoding control; MES and SMU clean. SDMA and MEC are the only user-submittable surfaces on this card and both gates held, which is the actual reason the campaign produced no escalation. Open items keep their named blockers. DISCLOSURE-SUMMARY.md gets a scope note pointing at the consolidated audit, since it predates all of this.
* MEC/PM4 round 7: HIQ plumbing blocked, blocker characterised preciselyuser2026-09-28
| | | | | | | | | | | | | | | | | | | | | | Attempted to stand up a KIQ/HIQ ring to give round 6 its missing control. Cleared two obstacles: tinygrad's mqd_paddr has 2+is_vf entries while setup_ring(kiq_xcc=0) indexes 2 (fixed by appending an allocation so the driver's own MQD code runs unchanged), and the KIQ's doorbell index 0 is outside the apertures AM enables (re-pointed at MEC_RING0+4, an index the compute probe had already rung). Did not clear the third: CP_HQD_ACTIVE reads back 0 after setup_ring, so the CP rejects the queue and no NOP retires. Root cause is that tinygrad's KIQ path is VF-only and hardcodes me=2/pipe=1/queue=0, while amdgpu picks them dynamically via amdgpu_gfx_kiq_acquire() and additionally needs the HPD EOP region, MEC pipe enablement and the KIQ doorbell aperture -- an amdgpu KIQ port rather than a patch. The MAP_QUEUES chain therefore stays open and untested, with no claim in either direction. It would have been easy to combine round 6's stall with this round's dead KIQ into "MAP_QUEUES is gated everywhere"; neither result supports that, because a queue that never activates cannot refuse anything.
* MEC/PM4 round 6: MAP_QUEUES chain is untested, not closeduser2026-09-28
| | | | | | | | | | | | | | | | | | | | | | Round 5 closed the direct route (unprivileged queues cannot write registers), so the follow-up is the indirect one: use MAP_QUEUES from an unprivileged queue to map a queue whose MQD sets priv_state=1, then write registers through it. That would be a user->kernel chain requiring no memory-safety bug at all. The test is inconclusive and is recorded that way. MAP_QUEUES stalls the queue identically at priv_state=0 and priv_state=1, and identically again after seeding the target MQD with the one the driver had just built. Because the privileged queue stalls too, the stall is not a privilege effect -- MAP_QUEUES is an HIQ/KIQ-pipe packet and is not serviced on an ordinary compute ring. Worth stating because the failure mode is deceptive: a stall looks exactly like the round-5 privilege fault, and without the priv_state=1 comparison this would have been written up as "the gate covers MAP_QUEUES too". The probe's verdict text is corrected to say a stall alone proves nothing. The chain stays open. Testing it needs HIQ submission with a user-owned ring, which tinygrad's AM does not expose (setup_ring(kiq_xcc=0) indexes past its allocated MQD slots) -- a driver-plumbing blocker, not a firmware one.
* MEC/PM4 round 5 [HW]: the user->kernel register boundary holdsuser2026-09-28
| | | | | | | | | | | | | | | | | | | | | | | | | | | PM4 is submitted by unprivileged user queues and WRITE_DATA(dst_sel=register) names a memory-mapped register, so an ungated path would be the user->kernel primitive none of the DMCUB findings can be. Tested as a 2x2 on silicon, because either factor alone is uninterpretable: dst=memory dst=register priv_state=0 wrote refused, queue then faults priv_state=1 wrote wrote, queue alive The memory column proves the packet encoding, ring, doorbell and submission path are correct -- without it, "the register did not change" is equally consistent with a malformed packet, which is exactly what made the first SDMA RB_PRIV=0 run inconclusive. The priv_state=1 row rules out the other explanation, that WRITE_DATA->register is simply unsupported on MEC. Privilege was changed by live-flipping CP_HQD_PQ_CONTROL (0x8509 -> 0xc0008509) on an already-running queue, so that bit is the only difference between the two runs -- the same isolation technique used for SDMA's RB_PRIV. Verdict: the boundary is real and enforced on gfx11. Across both user-submittable engines the pattern is identical -- memory confined by the VM, registers gated by a per-queue privilege bit, and the gate holds. Also fixes the probe's verdict text, which printed the unprivileged conclusion for a privileged run.
* MEC/PM4 round 4: correct the dispatch table, refute the length-bound guessuser2026-09-28
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Acting on the synthesis, which put PM4 at the top because it is the only user-submittable surface left unaudited. Round 1's table base was 0x100 low -- the same nested-$PS1 error that shifted every DMCUB address in this tree. At the corrected base 0x415d8 all five of Round 1's anchor opcodes verify, and 5 of 7 populated slots carry published PACKET3_* names (MAP_QUEUES, UNMAP_QUEUES, QUERY_STATUS, RUN_LIST, MAP_PROCESS_VM). At the old base, zero did, which is what makes the naming a validation rather than a decoration. That table covers queue management only. Two scans found no second dispatch structure -- no further (handler, flags) pairs and no plain pointer table that opcode-indexes onto the enum -- so the remaining packets are dispatched by compare chains, matching Round 2's reading. Round 1's suspected per-packet length bound is refuted: MAP_QUEUES carries 2241 where UNMAP/QUERY/RUN_LIST carry 2 and MAP_PROCESS_VM carries 1. Those are not dword counts for those packets, so the low half is a flags field and the parser's length validation lives in code. New tooling (mes/mec_audit.py: table/bounds/taint) taints from the packet data port 0x6F8(tp) with dataflow that respects both calls and basic blocks. The block rule was added after the pass produced a striking false lead: a raw packet dword shifted left 36 into MMIO-aperture bits and used as a store address, which would have been a host-controlled register write. An unconditional j sits just before that block, so the base comes from elsewhere; block-awareness removed it and 16 other sinks. Of the 75 survivors, 27 assemble a 64-bit GPU VA from two packet dwords and are VM-confined as proven for SDMA. Still no finding.
* Synthesise the corpus: test the strongest compound, and reorder the targetsuser2026-09-28
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | A deliberate pass over all findings asking whether any two combine into something neither is alone. The strongest candidate is tested and falsified. FINDING-10 leaks memory with no memory-safety bug -- it turns an unreadable value into measurable time -- and its only real limitation is that the DMUB inbox is kernel-only. SDMA's POLL_REGMEM has the identical shape on queues that ARE user-submittable, and since a poll writes nothing, the gate covering SRBM_WRITE and POLL_REG_WRITE_MEM need not have applied to it. On silicon the privileged control shows the oracle plainly (match 0.08 ms vs mismatch 3002 ms) and it vanishes at RB_PRIV=0, where both outcomes become an indistinguishable hang. SDMA's register boundary is therefore consistent across write, read-to-memory and read-by-timing -- a stronger claim than any single probe made. Recorded alongside it: an untested cross-engine question (SDMA's security is one bit, DMCUB's types 1/3 are an intentional arbitrary register write -- can one engine set another's privilege bit?); the systemic pattern that memory is confined by hardware while registers are confined by firmware convention, which is exactly what rollback undoes; and that DMUB rollback needs no power cycle, unlike sOS rollback. The useful output is a reordered target list. The only user-submittable surfaces here are SDMA queues (audited, gated) and PM4 -> MEC, which went unaudited only because MEC was unreadable. It is RV64 and readable now, and the SDMA methodology transfers directly, so MEC-PM4-FOOTHOLD.md becomes the highest-expected-value target left. Rejected compounds are kept too, including FINDING-10 against the HDCP canary fence (no PSP memory aperture reachable from DMCUB) and FINDING-1 against the PSR state machine (the host owns CW6 anyway, which lowers FINDING-1's severity).
* Scope the DMCUB findings by reachability, and test the user-influenced pathuser2026-09-28
| | | | | | | | | | | | | | | | | | | | | | | None of findings 1-10 is exploitable by an unprivileged user: they all need the DMUB inbox, which is kernel-only under amdgpu, and the hostile field is always driver-chosen. Record that plainly so the count is not mistaken for ten exploits, and keep the residual value where it actually is -- FINDING-10 crosses a PSP->host confidentiality boundary, FINDING-8 breaks rollback, 1/2/6 are vendor-fixable bugs in firmware meant to be isolated. There is a user-influenced surface, traced in the kernel rather than assumed: FB_DAMAGE_CLIPS is settable by any client with a DRM fd, DRM core validates only the blob's element size, and amdgpu uses the raw blob rather than the clipping iterator, computing x2 - x1 into a uint32_t. Arbitrary coordinates therefore reach UPDATE_DIRTY_RECT verbatim, while the count stays bounded to 3 and the instance fields stay driver-chosen. Tested on hardware inside exactly that envelope: nine hostile-coordinate cases, all consumed, firmware alive, no unexpected CW6 write, and input-dependent (8 vs 4 bytes changed) so the handler is not ignoring them. No user-reachable bug demonstrated. The DMCUB is idle with no PSR session, so the deep region programming likely never ran -- the same unlock FINDING-4 needs; type 68 is shown user-influenced but not yet tested.
* Inventory every loadable firmware component, and record the sweep hazarduser2026-09-28
| | | | | | | | | | | | | | | | | | | | | Answers "what can be loaded on this card, and which of it have we examined?" The component list is enumerated from the live stack (adev.fw.descs, adev.fw.sos_fw, the gfx11 RLC-autoload set, on-demand DMUB/TA/VCN) rather than from memory, and then measured: submitting a zero blob under every GFX_FW_TYPE_* separates "unsupported on this ASIC" (0xffff0006, 49 types) from "supported, our garbage failed authentication" (0xffff0008, 37 types). Nothing in the supported set is an unexamined image. The TA question closes as a negative: psp_13_0_0_ta.bin holds exactly four TAs, all already audited, so no SECUREDISPLAY/RAP/XGMI TA exists for this ASIC. HAZARD: that sweep wedged the PSP -- the sOS ring stopped responding and the SMU timed out, needing a cold ATX cycle. SMU/PPTABLE are the suspected trigger. Recorded in CONSTRAINTS.md so it is not repeated; the result is already banked. Also corrects the inventory's ISA map (IMU is Xtensa, MES/PFP/ME are RV64) and notes that every PSP-loaded payload carries a nested $PS1 header, so code starts at file 0x200 -- the generalisation of the DMCUB base error.
* Refute the TA-to-SMU reframing: SVC 0xE8/0xE9 are stubsuser2026-09-26
| | | | | | | | | | | | | | | | | | | | | | | The previous commit claimed the SMU path is TA-reachable because the dispatcher's TA arm maps SVC 0xE8/0xE9 to what the symbol map calls smu_wait_ack+0x34. Checking the code before building an SMU model shows that is wrong. Both SVCs do mov r0,r5 (the TA's argument) then bl 0x6f38, and the bytes at 0x6F38 are 70 47, i.e. bx lr. smu_wait_ack itself spans 0x6F04..0x6F30, 0x6F32-0x6F36 is literal-pool data, and 0x6F38 is a standalone empty stub that is not part of that function at all. The name smu_wait_ack+0x34 is a nearest-preceding-symbol artifact, the same trap that made the v53 map unusable for the TSU subsystem. So SVC 0xE8 and 0xE9 return immediately with r0 set to the caller's own argument. A TA cannot drive SMU messaging through them. Net on SMU after two corrections: it is not an attacker-controlled channel since replies come from on-die firmware, it is not truncating emulator coverage since zero SMU paths execute during boot plus ring init, and it is not TA-reachable via those SVCs. No known caller reaches the PSP's SMU protocol code in anything we can drive, so there is no route to it and no justification for building an SMU model. Recorded so the idea is not revived a third time without a caller being found first.
* Correct the SMU entry: not an attacker channel, and not currently truncating ↵user2026-09-26
| | | | | | | | | | | | | | | | | | | | | | | | | | | coverage Two errors in the previous commit's ranking, both worth correcting explicitly. SMU is not an attacker-controlled input channel. Its replies come from on-die SMU firmware; the host can send SMU messages, which is how PPSMC_MSG_PowerUpVcn opened the VCN gate on silicon, but it cannot forge the SMU's replies to the PSP. Listing it alongside a monitor, which an attacker owns outright, was a category error. And the emulator's missing SMU model is not truncating coverage today. There is no SMU model at all -- one comment in pspemu/system.py and no implementation -- so the natural worry is PSP-side code spinning on replies that never arrive. Measured instead: across smu_send_msg, smu_wait_ack, smu_mailbox_xfer, smu_version_get and msmu_handle_param, ZERO SMU paths execute during a full boot plus ring init, with no faults. The code is never entered, so modelling replies would change nothing until something sends a message. The gap is a missing caller, not a missing model. The genuinely interesting part, from this project's own TA capability audit: the TA-reachable SVC arm maps SVC 0xE8 and 0xE9 to smu_wait_ack+0x34. So the SMU path is reachable from a TA -- a lower-privilege context than SYS_DRV -- and is unused in our runs only because no TA we drive issues those SVCs. That reframes the question from "can an attacker forge SMU replies" (no) to "what can a compromised TA do by driving SMU messaging through SVC 0xE8/0xE9, and what does sOS do with the response", which is host-reachable and testable once an SMU model exists.