| Commit message (Collapse) | Author | Age |
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
[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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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).
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|