| Commit message (Collapse) | Author | Age |
| | |
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
gcp_rop_crypto2.py: issues TWO svc 0xf2 service-1 calls inside ONE FE0A-hijacked chain (the FE0A->ROP
hijack is one-shot per boot, so stateful sequences must be in one chain). Hardware-validated:
- create SHA256 -> handle 0x10 (status 0, dumped back)
- create -> create -> handles 0x10 then 0x11: multi-op sequencing + crypto CONTEXT STATE PERSISTENCE
proven on silicon
- create -> digest(0x08): input 32B placed in the GCP-window scratch and dumped back correctly; the
transform returned 0xffff0006 because op 0x08 reads ctx[5]>>3 (digest len) which create leaves 0
(it stores diglen at ctx[3]) -- op 0x08 expects the operation 'started' (ctx[5] set by a
digest-init step). Precisely characterized via sub_E0B148/sub_E1CDEC/sub_E055D0; documented as the
next lead.
Added fw/HARNESS-GCP-CRYPTO-ROP.md (mechanism, message layout, AllocateOperation tuple, buffer-
direction table off_E0AB70@0xe25850, validated results, the digest state-machine lead) and mirrored
hw/gcp_rop_crypto.py, hw/gcp_rop_crypto2.py + runners into the repo.
The harness is clean, parameterized, and hardware-validated driving real crypto operations (context
creation with state persistence) with input/output buffer plumbing and result dump-back.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Parameterized crypto-op harness built on the proven stack-migration svc 0xf2 service-1 chain. Takes
OPCODE + MSGSPEC (JSON dword_index->value) to drive the SYS_DRV GP-TEE crypto dispatcher (sub_E1DC30,
opcodes <0x1000, ACL-free) from a ROP'd GCP TA, and adds a SECOND in-chain memcpy that dumps the
post-op message region (STACK_DST+0x300, 0x80 bytes) back to host-readable params+0x3010 -- so inline
results (e.g. a created context handle at msg[14]) are observable. Status r0 exfil'd to params+0x3000;
graceful-return marker to params+0x3100.
Hardware-validated mechanics (op 0x01 create, algo 0x50000004): graceful marker 0x5a (clean return,
ring survived), dump correctly echoed M[0]=opcode and M[15]=algo, svc reached the dispatcher (status
0xffff000a = handler reject of that algo, NOT ACL 0xffff0001). Confirms: harness drives crypto ops and
dumps output buffers back. Algo IDs are standard GP TEE (dword_E023FC=0x50000005=SHA384, statically
init'd); sweeping recognized algos to land a successful create. hw/crypto_run.sh + crypto_algo_sweep.sh
are the runners.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
(0x70xx) all ACL-rejected from GCP
Prompted by FINDING-SYSDRV-ACL-RANGE-TABLE.md (which documents the 0x6000/0x7000/0x8000 families the
first sweep missed -- 0x6001 otp_block_read = FUSE read, 0x6002 = SMN MMIO-write, 0x70xx = TA reg
access). Swept them from GCP: 0x1021 (free) REACHED; 0x6001/0x6002/0x7001/0x7002/0x8001 all
0xffff0001 ACL-REJECTED.
So all 8 distinct ACL range masks (0x200/0x40/0x1000/0xffff0/0x2000/0x4000/0x8000/0x10000) are denied
to the GCP caller -- GCP's off_6428 entry grants none of the gated ranges. Definitive: from a ROP'd
GCP TA, svc 0xf2 service 1 reaches ONLY the crypto dispatcher (<0x1000) and free high opcodes
(outside all 10 ranges). The overflow (0x3100), OTP/fuse read (0x6001), MMIO-write (0x6002), and reg
access (0x70xx) are ALL unreachable from GCP.
Definitive answer to the fuse question: fuse/OTP read 0x6001 is ACL-gated, NOT reachable from GCP.
Strategic note: the ACL-table doc shows POWERPLAY emits 0x60xx and RAS emits 0x70xx, so those TAs
hold the caps GCP lacks -- reaching OTP-read/MMIO-write needs compromising POWERPLAY or RAS, not GCP.
GCP ROP is a dead end for the gated SYS_DRV surface. Updated the audit doc.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
free opcodes pass
Swept one opcode per ACL range through the stack-migration chain on silicon. 0x1021 (free) REACHED
(status 0x0); 0x3103/0x3005/0x2005/0x1105/0x4001 (ranges 0x3100/0x3000/0x2000/0x1100/0x4000) all
0xffff0001 ACL-REJECTED. So off_6428 grants the caller NONE of the range masks.
This settles the confused-deputy accurately: svc 0x76 checks the REAL caller's (GCP type-5) minimal
caps, not SYS_DRV's -- the opcode-range ACL is NOT fooled by the nested RPC. The confused deputy only
defeats sos_proc_switch's permission bit (dest-1 exempt); the ACL stands.
Accurate reachable SYS_DRV surface via svc 0xf2 service 1: crypto opcodes <0x1000 (sub_E1DC30, no
ACL) and free high opcodes >=0x1000 outside every ACL range (sub_E105A8, incl. the ASD-emitted
0x1021/0x103B.. space). The 10 ACL ranges are UNREACHABLE. Consequence: the #9 overflow
(0x3102/0x3103, range 0x3100) is confirmed UNREACHABLE on silicon -- that escalation is closed. The
audit now targets the reachable set (crypto dispatcher + free high opcodes), much of which overlaps
fuzz_sysdrv's ASD-emitted coverage. Updated fw/AUDIT-GCP-ROP-TRIGGERABLE-SVCS.md; harness
hw/gcp_rop_sweep.py.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
rejects 0x3103 on silicon
The operator's version question prompted verifying the live SYS_DRV: C2PMSG_58=0x00310036 = 0.49.0.54
= sys_drv2.elf, so all the .54 offsets ARE valid for the card. Not a version error.
Ran the service-1/0x3103 chain on hardware. The transport works: the ROP migrated to the GCP stack,
issued a real svc 0xf2 service 1, and got back a SYS_DRV error code 0xffff0001 (not a sos_proc_switch
reject 0x2f/0xe) -- so the call passed the svc 0xf2 gate (dest-1 exempt, SP+msg in the type-5 window)
and reached SYS_DRV's dispatcher. The svc-issuing ROP machinery (stack migration, exfil, graceful
return) all worked and the ring survived.
But 0xffff0001 = dword_E05A70, returned by sub_E05A24, the high-opcode ACL: it finds the {lo,hi,mask}
range for the opcode, issues svc 0x76 (caps check), and returns 0xffff0001 if the caps check fails.
sub_E105A8 propagates it (if ACL!=0 return it, skip dispatch). 0x3103 is in range 0x3100-0x31ff
(mask 0xffff0) and its svc 0x76 caps check FAILED for the GCP->service1 nested-RPC caller.
This retracts the emulator-based 'confused deputy reaches the overflow' claim for 0x3103. The
emulator demos called sub_E087E4 directly, so their svc 0x76 ran in a context that passed; the real
nested RPC evaluates caps that do NOT grant the 0x3100 range. The emulator never ran sos_proc_switch
and was infidelitous on exactly this gate.
Stands: GCP code exec; ROP issuing arbitrary svc 0xf2 (migration+exfil+graceful return); the call
reaching SYS_DRV past the svc 0xf2 gate. Blocked: 0x3102/0x3103 are ACL-gated out. Next: measure
which opcode ranges' masks pass for this caller (one opcode per range, read exfil status), and look
for an exploitable handler at an allowed opcode (incl. the freely-allowed non-range space ASD uses).
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Ran the operator's stack-migration chain on silicon as the service-2 (ASD) survive-check. Result:
FE0A status 0x240 (planted), no fault; graceful-chain marker 0x5a at +0x3100 (migration+graceful ran);
exfil'd svc status 0x4 at +0x3000 (ASD returned 0x4); follow-up FE07 ta=0 (ring survived, no ATX).
So from a GCP ROP chain on hardware: pivot into the command buffer, memcpy the svc sub-chain to the
in-window GCP stack (0x410000), mov sp to it (SP now in-window), pop r0=2/r1=msg (msg in-window),
svc 0xf2 -> ASD returns 0x4, str r0,[host_buf] exfil, graceful return. The svc went through the REAL
sos_proc_switch gate (SP and msg both in the type-5 window) -- the link the emulator never exercised
-- and the ring survived.
Complete svc-issuing + exfil + graceful-return machinery validated on silicon. Next: r0=1 (service 1
= SYS_DRV) + 0x3103 message, svc gadget entered at bare svc 0x4061ca to keep r0=1 -- the
confused-deputy confirmation. Harness hw/gcp_rop_svc.py.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
survives without ATX
Addendum G wedged on a deliberate fault; this returns cleanly. The write/pivot only corrupts the
stack at/below 0x41bfbc, leaving 0x404828's real frame (0x41bfc0+) pristine, so restoring SP to
0x41bfc0 and reaching 0x40498c lets the TA unwind normally. The return uses the 0x40498c error
branch (cbnz r0), because 0x4049d8 reloads r4 from a literal and needs only r0/r6 -- immune to the
register shuffle the pivot causes.
Chain: pivot into the buffer; strb x5 to (a) write the observable marker 0x5A to params+0x3000 and
(b) rewrite the popped-PC stack slot 0x41bfbc to 0x0040498d; pop{r0,..} to set r0=0x240; final
mov sp,r4;pop to put SP=0x41bfc0 and pc=0x40498d. Then 0x40498c -> error path -> 0x404926 pops
0x404828's real frame -> returns to 0x40433c -> TEE.
Measured: FE0A status=0x240 (our planted r0, returned to the host), FSR=0 no fault, marker present,
and follow-up FE07 returns 0 with NO ATX cycle. Three confirmations: marker = strb ran; status=0x240
= the whole real-stack return path ran and the TEE wrote our chosen value; FE07 ok = ring not wedged.
So the primitive is complete and repeatable: chosen PC, SP pivot into the host buffer, arbitrary ROP
with chosen write-what-where, and clean resumption with a host-chosen status. Remaining for
weaponisation: word-granular store (strd gadgets need controlled r0..r2) and a concrete payload;
code injection stays out (no W+X page). Updated both documents.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
command buffer
Addendum F pivoted SP into the command buffer; this runs an actual chain from there with a readback
effect, not just a terminal fault.
Chain: first pop sets r4=params+0x2000, pc=0x40371d; the pivot gadget mov sp,r4;pop{..pc} moves SP
to params+0x2000; then the self-chaining gadget 0x402266 strb r4,[r5];pop{r4,r5,r6,r7,r8,pc} runs
four times, each fed a (byte,address) pair and the next pc from the buffer, then a terminal sentinel
pc 0x00bada54.
Result on silicon: marker @ params+0x3000 = b'R0P!' (was 0xBB fill), FSR=0x5 faultaddr=0x00bada54.
Both confirmations hold -- four strb executed in order with chosen value and chosen address each,
and control followed the planted sequence to its sentinel end. 0x00bada54 is only in the chain,
never on the stack.
So end to end, one FE0A selector-4 invoke gives: chosen PC (Add.E), SP pivot into the host command
buffer (Add.F), and an arbitrary-length ROP chain doing chosen write-what-where (Add.G). Code
injection stays out (no W+X page, report section 5), so payloads remain ROP, but 'arbitrary ROP
chaining is a structural ceiling' is now refuted with a running chain.
Limits kept honest: byte-store gadget (word-store needs controlled r0..r2, one more pivot); no
graceful TEE return yet so each run needs an ATX cycle; no weaponised payload chosen. Updated both
documents.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
report section 4 refuted
Addendum E gave a chosen PC plus chosen values in the registers 0x404d16 pops. That is enough to
pivot: GCP has 0x40371c mov sp,r4 ; pop {r4,r5,r6,r7,r8,sb,sl,fp,pc} with pc at sp+0x20. Point hop-1
PC at it and hop-1 r4 at the command buffer.
Measured, one ATX cycle: first pop loads r4=0x04026000 (params+0x2000) and pc=0x0040371d; the gadget
does mov sp,r4 then pops pc from params+0x2020, where we planted 0x00BADA54; fault is FSR=0x5
translation at addr=0x00bada54. That value exists only in the command buffer and nowhere on the
stack, so the fetch can only have arrived via first-pop -> gadget -> mov sp,r4 -> second pop sourcing
PC from attacker memory. SP is now pivoted into the host command buffer, so an arbitrary-length ROP
chain can be laid out there and executed.
This refutes two of the three claims section 4 of the control-flow report rested on: chosen chain
words (the buffer is plain host data, no blob alphabet, no carve) and a controlled SP above the
ctx+0x4f floor (SP is in the command buffer, unrelated to the floor). Updated both documents.
Honest boundary kept: this proves the pivot and that chain memory is fully controlled, not yet a
useful computation (needs a GCP-resident gadget catalogue and a built chain), and code injection is
still blocked because no writable page is executable (report section 5, unchanged). But 'no arbitrary
ROP chaining' as a structural ceiling is now false.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
on silicon
Addenda C and D declared the saved-LR route a negative. Wrong, and the error was mine in decoding
the fault frame rather than in the experiment. I assumed faultFrame[0] was the faulting PC; on
22.0.0.77 word0 = 0x4055d8 is a build-specific constant, which Add.6 of the FE09 document states
outright. The real discriminator is word3 (FSR) plus word4 (address), and the hijack report already
documents the decode. Re-read that way, every row I labelled 'deref: painted a pointer reg' was a
control-flow redirect.
Calibration at slot 0x41bfbc settles it against two independent prior measurements:
PC_VAL=0x00400000 -> FSR=0x80f (write perm, WnR) addr=0x0, which is Add.92's 17 ARM NOPs from
0x400000 reaching the store at 0x400044, with addr=0 because r4/r8 are zero (Add.95). So
execution PROCEEDED from the planted PC.
PC_VAL=0x00416e60 -> FSR=0xf addr=0x416e60, the report's own XN-on-fetch signature for .bss.
The earlier 0xDEADBEE0 rows read the same way: FS=0x5 translation at the unmapped planted PC.
So the geometry is measured, not predicted: SP0=0x41c000, SP_e(0x404c74)=0x41bfc0, lr at 0x41bfbc,
which matches the analytic chain SP0 -0x10 -0x10 -0x20 exactly. Addendum B's method was right and
only its SP0 anchor was off, because the UNLOAD path it was derived from carries tlApiFree's
~0x40-byte request block.
Net: a single FE0A selector-4 invoke sets PC to any 32-bit value, no carve, no lottery, no wedge,
since control leaves before the parent's unclamped copy. The pop also restores r4..sl from slots
inside the write's reach, and each invoke carries two independently aimed 8-byte writes with
independently chosen contents, so sl and PC can be set together. This breaks section 4 claim 1 of
the hijack report outright.
Process note recorded: three runs produced the hit signature before I recognised it, and I wrote two
negative addenda on top of it. I validated the write with an in-buffer positive control but never
validated the fault-frame decoder against a known reference, which is precisely the project rule I
had been applying everywhere else.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
as a negative
Found the flaw in Addendum C's sweep: all destinations were 8-byte aligned, so slots at ==4 mod 8
were only ever painted together with their ==0 mod 8 neighbour. If the LR slot sits next to a
pointer-holding register, the deref fault preempts the pop every time, which is exactly the observed
pattern -- so that sweep could not have detected a redirect there even if one existed.
Re-ran with single 4-byte writes (0x81c=0x20 -> len 4) at one slot at a time, second destination
parked in-buffer, serialised with one ATX cycle per shot. The alignment hypothesis was confirmed in
one sense: exactly one slot, 0x41bfbc, feeds the pointer dereferenced at 0x4055d8, and the old sweep
could never isolate it. But no slot produces faultPC == PC_VAL, including 0x41bfcc tested alone,
which is where the saved LR must be if 0x41bfbc is 0x404c74's saved r7 (pop takes r4..sl,pc from
SP_e-0x20..SP_e-0x04, so lr = r7+0x14).
So the route is a negative and I recorded the contradiction rather than explaining it away: either
the dereferenced register belongs to another frame, invalidating the r7 -> SP_e -> lr inference, or
the exit taken is not 0x404d16 -- and static reading gives the second no support, since both the
write path and the error path converge on that pop.
The measurement that would settle it is blocked: 22.0.0.77 does not load in psp_emu at 60 or 110
drain slices, while 22.0.4.50 loads and reproduces its hardware-confirmed SP0=0x419000. Noted that
my earlier 'hang' there was an 8x oversized budget, not a deadlock. Getting .77 to load is the
prerequisite for finishing this line.
The finding itself is unchanged: the chosen-content/chosen-destination write and the cross-session
disclosure are proven; reach is bounded; no control-flow hijack is demonstrated.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
no PC redirect
Ran Addendum B's experiment. Eight shots at 8-byte stride give full 4-byte coverage of
[0x41bf90,0x41bfd8) and not one faultPC == PC_VAL. No control-flow redirect.
Two method errors found and corrected mid-run, with the affected data discarded rather than used.
The first HIT criterion matched PC_VAL anywhere in the fault frame, so it flagged data-fault
addresses as hits; the three 'hits' it reported were derefs, not transfers. And drive_lr3 was
launched while drive_lr2 still had six candidates pending, so two drivers were invoking the TA and
cycling power at once -- drive_lr3's output is void and drive_lr2's rows 3-7 are discarded, leaving
only its pre-overlap rows. All retained numbers come from a clean serialised re-run with one ATX
cycle per shot.
What the sweep does establish: the write reaches the stack, since every non-deref row ends in the
parent's copy faulting at params+0x40000, which only happens on the sub-handler's success path. And
two bands make a live register deref the planted value at 0x4055d8 (the logger's ldr r0,[r7]),
localising a pointer-holding saved register to 0x41bfb8 or 0x41bfbc, with two more bands faulting at
PC_VAL+0x418, i.e. a painted register reused as the params base.
Recorded the contradiction instead of smoothing it: if that register were 0x404c74's saved r7 then
the LR slot would be 0x41bfc8/0x41bfcc, both of which were painted by the SP_E=0x41bfd0 shot, which
returned normally. So either the register belongs to another frame or the exit taken is not the pop
at 0x404d16. Blind sweeping cannot separate those, so the next step is the emulator measurement,
with a note that pick_container returns .50 and that the drain budget is too small for .77 -- and
that my first attempt at 40x slices and 9M blocks was an 8x-oversized workload, not a hang.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
host-chosen, proven on silicon
This refutes a claim the FE09 document and the control-flow report both rest on -- that GCP exposes
no chosen-value write, so a chain word can only be zero, 0x9e400000, or random.
gcp_QueryCryptoSession (0x404c74, FE0A selector 4) calls gcp_GetCryptoSessionByDecodeHandle
(0x403b70), which scans all 64 crypto slots comparing slot+0x18 with no liveness test. FE0C never
writes slot+0x18, so an all-zero search key matches slot 0 whatever its state, and the lookup
returns the slot POINTER. The caller then memcpys slot+8 and slot+0x10 to
params+0x418+*(params+0x840) and +*(params+0x838). Those two slot fields are precisely the arg10 and
arg18 that the creating FE0C request supplied, so the attacker picks the content with FE0C and the
destination with FE0A.
Measured, buffer pre-filled 0xBB, arg10=0x1122334455667788 arg18=0x99AABBCCDDEEFF01:
keyzero len=8 -> status 0, +0x1000 = 8877665544332211, +0x1400 = 01ffeeddccbbaa99
keyzero len=4 -> status 0, 4 bytes only, so the length field is honoured
key nonzero -> status 0x233 (lookup miss), no write -- the attributing negative control
Three rig facts recorded because they each cost a run: *(params+0x14) must be nonzero or FE0A
returns 0x100 before the sub-handler for every selector including the known-good sel 15, so the
zero search key has to be relocated via *(params+0x83c); the destination must sit below
params+0x1950 because the parent's post-switch memcpy at 0x4049a8 erased our first two correct
writes before readback; and each FE0A needs a fresh FE08/FE09 because of the anti-replay ratchet.
Reach is proven in-buffer only, deliberately. The bound is sl+len <= *(params+0x818), which is
itself host-supplied, and the write executes before the parent's runaway memcpy, so an
out-of-allocation destination looks reachable at the cost of a post-write fault -- untested, and
written up as the next step rather than claimed.
Also corrects the FE0F document: its closing retraction says the two extra liveness-free lookups are
unreachable until state 3 can be driven, and state 3 is reachable via the FE09 forged CMAC, which
this result uses. ByDecodeHandle is now demonstrated host-reachable on hardware.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
not the window
Prompted by the right question -- the store computes a 32-bit sum, so wraparound should reach
anywhere. It does, and the earlier "both directions abort" reading was too coarse.
The fault dump's word[4] is the faulting address. Two-point test, one fault per boot since a fault
wedges the ring: D=0x7FFFFFFF gives word[4]=0x84013417 and D=0x80000FFF gives 0x84014417, a delta of
0x1000 matching the delta in D. Solving fault = params_VA + 0x418 + D gives params_VA = 0x04013000,
page-aligned, so the host can compute D = target - 0x4013418 mod 2^32 for any address.
Confirmed by aiming at three absolute addresses; in every case the reported fault address equals the
requested target exactly. 0x402fa0, the store instruction itself, 0x404f4c, in-image data, and
0x406000 all fault. The control matters most: it reaches the buffer through a wrapped negative
displacement 0xFFFFFCE8 and writes successfully, so the arithmetic really is mod-2^32 and the aim is
exact.
So this is an arbitrary-address 4-byte write attempt with an exactly predictable target, not
something confined to the request. What confines it is page permissions: the loaded image at
0x400100..0x416100 is not writable for either code or data, one page below the buffer faults, and the
only writable mapping found is [params_VA, params_VA + cmd_buf_len) -- whose size the host chooses.
That makes this and the cmd_buf_len finding one mechanism rather than two: name any address, and the
ones that land are those inside a window the attacker sizes itself.
Still open and stated as such: the TA's stack and heap are writable somewhere and their addresses are
unknown. Four addresses tested, one writable, at one ATX power cycle per probe. A cheaper mapping
oracle would turn this into a survey.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
confirmed
Strengthens the cmd_buf_len finding. The one-page demonstration understated it: placing the target a
chosen distance away with a filler allocation and declaring a matching cmd_buf_len, the write lands
wherever it is aimed. Declared 0x8000 with the target 0x2000 away, declared 0x20000 with it 0x11000
away, and declared 0x100000 with it 0x81000 away -- 516 KiB past a single-page allocation -- all with
status 0 and no fault. The loader accepted every declaration up to a megabyte.
So the attacker picks the window size and then the offset within it, and the reach is bounded only by
the length it declares.
Recorded in place: an earlier run of this same test looked like it confirmed reach and did not. The
allocator hands out consecutive pages, so all three "far" targets were actually one page away and the
distance column read 0x1000 three times. The filler allocation is what makes the distance real;
without it the test was vacuous while appearing to pass, which is the third time in this work a
harness has needed its own control before its output meant anything.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
outside it
New finding, and it corrects a claim in REPORT-PSP-DOS-TRIGGERS.md.
LOAD_TA enforces a floor on cmd_buf_len -- at least 0x1000, measured earlier -- and nothing else. It
never checks the declared length against the memory the host owns at that address, and the PSP then
reads and writes TA parameters anywhere inside [cmd_buf, cmd_buf + cmd_buf_len). The host therefore
chooses the size of the region the PSP will write into, independently of what it allocated.
Demonstrated with allocation A of one page declared as 0x8000, and allocation B a separate page
immediately after it, pre-filled and never given to the TA. Both the control and the aimed case return
success with no data abort and modify B. Reproduced identically after a power cycle.
The control separates two facts. With the displacement at zero, GCP's ordinary output write to
params+0x1950 already lands at B+0x950, because that offset is past the end of A -- so over-declaration
alone is sufficient and no TA defect is needed. Adding GCP's unbounded displacement from
FINDING-GCP-FE09-CONFIGINIT-HOST-OFFSET-WRITE.md puts four bytes at B+0x40, exactly where aimed. The
two defects compose: the missing length validation gives the reach, the missing offset bound gives the
aim.
Earlier work reached the opposite conclusion and the reason is now clear. hw/aperture2.py ran the same
two-allocation shape but with A = 0x8000 and cmd_buf_len = 0x4000, so the declared window ended well
inside A and nothing could reach B. That test was correct for its configuration; the variable that
matters is cmd_buf_len relative to the allocation, and it was only ever varied downward. The claim in
REPORT-PSP-DOS-TRIGGERS.md is annotated in place rather than rewritten.
Impact stated without overreach: the PSP writes outside the object the host granted it, at a host-chosen
offset, with success status. Not demonstrated is a write into another security principal's memory --
this is a single-tenant part and both allocations are ours, which is the limitation that report already
noted. The architectural consequence is that any privilege-separated caller, a guest VM with a VRAM
grant or a restricted process, gets a directed write outside its own allocation, because the PSP takes
the caller's word for how large its buffer is.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
store
R9 is a full 32-bit value, so a large value is a negative offset. If anything below the shared buffer
were mapped, the write would reach memory the host never granted, which is the difference between a
denial of service and a corruption primitive. Tested with a fresh session per case so a poisoned
session could not be mistaken for a result: the in-window control at D=+0x100 lands with no fault,
and D=-0x1000 aborts with pc=0x402fa0, the same store as the positive direction. The write is
confined to the mapped window both ways and no displacement escapes it on this platform.
Two measurement notes kept in the document because either could have produced a false finding. The
first attempt at the negative case reported "RING DEAD, no fault", which would have been the
interesting answer -- a write that landed somewhere that broke the PSP rather than being aborted. It
was an artefact of reading the fault registers before they were populated; re-reading after the ring
timeout shows the abort. The harness now always reports the dump on a ring death. And DECL must be at
least GCP's entry gate of 0x2958: a first run with 0x2000 wedged on the very first invoke, before any
displacement was exercised.
Net position on the finding is unchanged but now bounded in both directions: a real out-of-bounds
write with a host-controlled 32-bit displacement and no bound in the TA, whose consequence here is
denial of service because the MMU contains it. The defect would be a write outside host-owned memory
on a platform where the mapping were larger than the request or adjacent to something privileged.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
as proof
The earlier revision of this finding called the write "not a memory corruption primitive, backstopped
by the PSP's window enforcement". That was wrong, and the correction is now the headline.
The inference came from two true observations -- an out-of-window write does not land, and the status
is non-zero -- and from an oracle that could not tell "the TA checked and refused" apart from "the TA
wrote out of bounds and the hardware stopped it". Adding the PSP fault dump settles it:
cmd 0xFE09, offset 0x828, value 0x7FFFFFFF
fault dump: 0x4055d8 0x25021013 0x402fa0 0x805 0x84013417 0x0 0x8da7463d
0x402fa0 is the store itself, STRB.W R6,[R1,R9] with R9 = *(params+0x828). So GCP computes
params + 1048 + host_dword and stores, and the memory controller aborts. Reproduced twice with an
identical dump and a power cycle between runs. That is an out-of-bounds write with a full 32-bit
host-controlled displacement and no bound anywhere on the path; the MMU, not any validation, is what
stands between it and PSP-internal memory.
Impact stated in two measured regimes rather than one sentence: inside the declared window the write
lands at a host-chosen offset, but in the host's own buffer, so nothing the attacker does not already
own is corrupted; outside it, a data abort at 0x402fa0 kills the ring and needs an ATX power cycle.
The consequence is denial of service, and that is a property of the mapping rather than of GCP.
Getting here also required fixing the harness twice, both recorded in hw/ta_saturated_fuzz.py. A
saturated baseline breaks equality-gated selectors -- cmdFE09 needs *(params+0x820) == 1 and
saturation set it to 0xFFFFFFFB, so ConfigureInitialize was never reached and the positive control
passed by never executing its target. It also breaks "x + 4 <= limit" checks, since both sides at
0xFFFFFFFB overflow. Reaching the code needs limits large and offsets small, which one constant
cannot express, so fields now carry explicit roles. Two runs produced a silently vacuous negative
before the control fired.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Closes a coverage gap found by reading the records rather than guessing. The ~2,000-case crash-oracle
run in psp_out/FUZZING-RESULTS.md was entirely HDCP, so the PSP fault dump at C2PMSG 82..88 had never
been pointed at another TA; and ta_fieldfuzz.py on the box only mutates offsets 4..0x80 for command ids
0..7 with a status oracle, while most TAs' real command ids and field offsets lie outside that box.
Coverage, with command lists taken from the dispatchers in fw/decomp: gcp 1,345 cases over 1..5 and
FE02/FE07..FE0F; opm 996 over 0..10; rap 542; merit 540 over 1..6; csu 270; dtm 183; wspe 181; oc 180.
About 4,237 cases driving 0xFFFFFFFF, 0x80000000 and 0x1000 through 30 offset and limit fields, with a
0x4000 declared window over an 0x8000 allocation so out-of-window writes would show.
The fault dump never fired once, and nothing wrote past the declared window. auc and fp were not
reached because both already have recorded DoS commands and the sweep stops on a hang; that is stated
in the report rather than glossed.
Six new DoS triggers, none of them in REPORT-PSP-DOS-TRIGGERS.md's list of five: gcp FE03 via +0x10 or
+0x14, opm cmd 11 via +0x08, dtm cmd 2 via +0x04, wspe cmd 5 via +0x04, and rap cmds 3 and 6 both via
+0x04. The hang-versus-crash distinction is measured on each -- sOS stays alive and the fault dump
stays all-zero while the ring stops answering -- so these are spins or blocked polls, not faults, the
same signature as the two HDCP hangs already on record. RAP's two share the same field, which
rap_dispatch takes as arg1, so that is one unvalidated argument rather than two bugs.
This does not find the objective's bug. What it removes is "insufficient dynamic coverage" as the
explanation for why it has not been found: the static conclusion that every TA-side bound is either
correct or backstopped by a PSP-side check now has ~4,200 cases of independent corroboration under a
detector that would have seen a data abort. Card recovered and verified.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
corruption, one new hang
Two coverage gaps found by reading the existing records rather than running another detector. The
~2000-case crash-oracle run in psp_out/FUZZING-RESULTS.md was entirely HDCP, so the fault-dump oracle
at C2PMSG 82..88 had never been pointed at another TA. And ta_fieldfuzz.py on the box mutates offsets
4..0x80 for command ids 0..7, whereas GCP's commands are 1..5 and 0xFE02..0xFE0F and its size and
offset fields sit at 0x414, 0x818..0x830 and inside data20 -- so GCP's parameter space had never been
fuzzed with any oracle.
hw/gcp_faultoracle_fuzz.py closes both, using the field list that
FINDING-GCP-FE09-CONFIGINIT-HOST-OFFSET-WRITE.md showed GCP actually reads as offsets and limits.
The negative is the larger part of the result: 1,345 cases and the fault dump never fired. Commands
1, 2, 3, 4, 5 and 0xFE02 are clean over 577 cases, and 0xFE07, FE08, FE09, FE0A, FE0B, FE0C, FE0D and
FE0F are clean over 768. Every one tolerates 0xFFFFFFFF, 0x80000000 and 0x1000 in each of 32
offset/limit fields with no data abort and no write outside the declared window, which corroborates
the static conclusion that GCP's offset arithmetic is bounded or window-contained.
One new DoS trigger: 0xFE03 with a large arg10 or arg14 stops the ring. Confirmed four times, each
costing an ATX power cycle. It is a hang and not a crash, and that is measured rather than assumed --
sOS stays alive and the fault dump stays all-zero while the ring stops answering, so GCP is spinning
or blocked rather than faulting, the same signature as the two HDCP hangs already recorded. New
because REPORT-PSP-DOS-TRIGGERS.md lists five triggers and none is in GCP.
Not memory corruption, so the objective is still unmet. Card recovered and verified.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
XGMI init gate
This lead was marked "not tested on hardware" and was the best-defined remaining candidate, because
the access is performed by SYS_DRV rather than the TA and so sits outside the shared-buffer window
enforcement that contains TA-side writes. Picked it by reading the prior docs' own admitted-gap
statements first rather than running another detector.
Two corrections to the lead's framing came out of choosing a build. Only 32.0.0.20 carries the
command allowlist, and its mask 0xB6F sets bits 0,1,2,3,5,6,8,9,11 -- so cmd09 is reachable and
cmd0A, the (value,address) pair the lead leans on, is not. And in that build cmd09 no longer passes a
pointer into the host buffer: the entry copies the request to a local bounce buffer and hands
handlers v12.argB, so the destination is the TA's stack copy. The host still controls the address
dword, and the result would still be observable since the entry copies the snapshot back to
request+0x418 with loc_40114C(v8+1048, v12.argB, 2056).
Result: cmd09 never reaches rpc_1061. It returns 4 at its second check, *(_BYTE *)ctx == 0, and only
cmd00 sets that byte. cmd00 fails with status 8 for every value of the single host-controlled byte
the entry feeds it (byte_406014 = v12.unk0C), tested with 0, 1, 2 and 0xFF. Address probes with 0,
the magic buffer's MC address, MC+0x40 and 0xDEAD0000 all return 4 with a zero readback, consistent
with the call never happening.
The reason is hardware: cmd00 needs xgmi_sysdrv_rpc_104B to enumerate XGMI nodes and there is no
fabric on a single-GPU board. The only path that sets the gate without enumeration tests
dword_406038 == 1, which xgmi_TA_CreateEntryPoint sets from session-creation data, not from host
input.
So the lead is not exploitable on this rig and the blocker is identified precisely. Still unsettled:
the semantics of 0x1061/0x1062, and whether this is live on a board with a real XGMI fabric where
cmd00 would succeed. That needs multi-GPU XGMI hardware.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
silicon, not corruption
Found a genuine TA-side defect, confirmed it on hardware, then established it is not a memory
corruption primitive. All three steps recorded.
The defect: gcp_ConfigureInitialize takes a 32-bit write displacement straight from the request at
params+0x828 and never bounds it, then stores four bytes of zero at params+1048+displacement. The
auth-state test inside the function fails on a fresh context and its failure branch converges on the
same store, so no session state is needed. Upstream, gcp_cmdFE09 reaches it with params+0x820 == 1
through comparisons that have host-controlled values on both sides -- params+0x828 against
params+0x818, params+0x82C against params+0x414 -- so none of them is a bound. Neither 0x828 nor
0x818 is ever written by GCP; both are purely host-supplied.
Confirmed on Navi 31 with GCP 22.0.4.50, no session setup, one invoke per displacement: the zeroed
dword lands at exactly 0x418 + D for D of 0x0, 0x100, 0x200 and 0x400, moving with D. So the
displacement really is host-controlled in running firmware.
Not corruption: with the declared window shrunk to 0x3000 over an 0x8000 allocation, a target at
0x4000 is outside the window but still in memory we own, and the write does not land. Reproduced
twice. The PSP enforces the declared window, backstopping the TA exactly as the loader's one-page
floor backstops the four TAs with no length check. Inside the window the write only touches the
host's own buffer.
One misreading corrected in the document: the failing statuses looked like they encoded the faulting
page, 0x141/0x151/0x161 for 0x4000/0x5000/0x6000. They do not. They increment by 0x10 on every
invoke regardless of displacement, and a later in-window target failed too, so the first
out-of-window attempt poisons the session and the status is a counter.
Side effect worth knowing: every out-of-window attempt left the PSP dead, ring unresponsive and
AM_RESET useless because the SMU's mode1_reset also timed out. Three ATX power cycles during this
work. Distinct trigger from the cmd_buf_len=0 wedge, same DoS class, so it is recorded in this
document rather than as a separate finding. Card left healthy and verified.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
persistent PSP DoS
Tested the unvalidated RAS entry point on the Navi 31 card. Three results, in order of how much
they change the picture.
The decompilation is confirmed. With a normal window the OPEN build writes exactly the two predicted
words at offsets 4 and 8, and the invalid-command path writes them as well -- status 0xffff000a
returned and both words still modified -- which is the unconditional LABEL_12 writeback observed on
hardware rather than inferred.
The short-buffer scenario I proposed is closed by the loader, not by the TA. LOAD_TA rejects every
cmd_buf_len from 4 through 0xFFF with status 6 and accepts 0x1000 upward, so the floor is a full
page. That is far above both the hardened variant's 0x30 threshold and the largest statically
visible access at offset 8, so for all nonzero lengths the missing check has no consequence through
this path. My original severity framing was wrong and is corrected in the document.
But the loader accepts cmd_buf_len=0, and there the missing check decides everything. HARD builds
reject cleanly and the PSP stays alive: 27.0.0.26 returns 0xffff0006 at invoke, 27.0.0.22_6e11d9d0
fails the load with 0xffff0000. All three OPEN builds tested -- 27.0.0.25, 27.0.0.23 and
27.0.0.22_1cb5cb5b -- hang the sOS ring for the full 10 s timeout and leave the PSP dead, with
subsequent loads failing.
Attribution is clean because version 27.0.0.22 ships both variants: _6e11d9d0 survives and
_1cb5cb5b wedges, same version string, same host sequence, opposite outcome, differing only by the
16 bytes of entry-point checking.
Recovery needs an ATX power cycle. AM_RESET is not enough -- after a wedge the SMU's own mode1_reset
times out, so the usual recovery path is itself gone. That cost two power cycles during this work.
Net: one LOAD_TA with cmd_buf_len=0 plus one INVOKE_CMD gives a persistent PSP denial of service,
reproduced on three builds. The precondition is loading one of the five unvalidated RAS binaries,
which are AMD-signed production images the card accepts, so this is rollback-style rather than a
signing break. Card left healthy and verified.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The corpus holds seventy-eight application builds and the shipping firmware refuses eleven;
refusal is a property of the running loader, and the rollback lets an attacker choose that
loader, so any build loading only on an older set would be surface this audit never
exercised.
Sweeping the whole corpus against both firmware versions, with the version register
recording which was running and every successful load unloaded immediately to avoid the
per-boot session wall, gives an identical refused set of eleven with an empty set difference
in both directions. Those refusals are therefore not version-gated but invariant across
releases, consistent with images for a different chip family.
The rollback finding remains valid as a capability but does not widen this audit's reachable
surface, which closes the last of its consequences: the payload requirement was already
shown unchanged, the older builds fuzz clean, and no extra application loads.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The historical containers hold all six components rather than only the system driver, which
required reading the descriptor array from raw bytes since the flexible array is declared
with one element and a buffer copy truncates it, the reason an earlier attempt saw a single
entry.
Substituting the entire set together, so it stays self-consistent rather than risking a
version mismatch being blamed on a security gate, and starting from a cold power cycle, the
version register now reports the older build instead of the shipping one, while an
application still loads and invokes cleanly.
That is the direct read-back the earlier single-component test could not produce, and it
retroactively settles the register question: it was unchanged before because it reports the
secure OS, which those runs never substituted.
No component in the set is version-enforced, the secure OS included, so the whole validated
hardware fuzzing corpus now has a second firmware version to run against.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
version enforcement
An earlier pass called this blocked for want of an older build of this component for this
chip, which was wrong: the container the part loads is published and historical versions
are public, so two older vendor-signed builds for this exact chip were obtained.
The older signed build boots and runs. After a full cold power cycle, with the injected
bytes the only copy of that component supplied, the secure OS comes up and an application
both loads and invokes successfully, and application service is provided by that component,
so the injected build is what executes. A positive control re-injecting the running bytes
also boots, so the injection mechanism is sound.
Two objections are closed by measurement rather than argument, because the first version of
this test proved nothing: a resident copy from a previous boot could have been servicing
requests, since the driver-level reset does not clear processor state, so the test was
redone from cold; and the load might have been refused with the secure OS booting anyway,
which the working application service rules out.
A register that cannot answer the question is also recorded: it reports the secure OS
version while only the driver component was substituted, and an earlier draft treated it
first as confirmation and then as refutation, being wrong both times.
Signature enforcement is intact but version enforcement is absent, so any memory-safety
defect fixed between an old build and the current one becomes reachable again by choosing
the old build. This is a route to reachable corruption rather than corruption itself, and
whether it yields one depends on a security fix existing in that window.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The open question was whether the live bootloader mailbox would act on a valid component
load after boot, which would reinitialise firmware in place while the secure OS runs and,
with many signed builds of one component in the corpus, allow a rollback.
It will not. Submitting the exact bytes this boot already loaded, so that acceptance would
change no content and only show the path acts, is refused for three different genuine
components with the same status as a nonsense component identifier. That uniformity is the
point: the status means not permitted in this state rather than bad parameter, so the
mailbox is reachable but state-gated closed, and the rollback case cannot even begin
because the current build is refused.
The harness twice refused to run for components this boot did not load, rather than emit a
row, and those voids are why the table is trustworthy.
The result is kept for the right reason: the difference between surface being absent and
surface being reachable but gated matters, because a future firmware could relax the gate
while the mailbox stays as reachable as it is today.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
All prior passes drove the graphics ring, the application command space or the ring control
registers. The bootloader mailbox, which loads the secure-OS components at boot, had never
been touched.
With the secure OS alive, the ring created and the heartbeat advancing, the bootloader's
ready bit is asserted. Because a latched idle bit proves nothing, the test used an invalid
component identifier pointing at a zeroed page, asking the bootloader to answer without
asking it to load anything. It answered with a status, so its mailbox is live and
processing host commands at runtime, concurrently with the secure OS, and rejects a bad
identifier cleanly rather than hanging.
Whether a valid component load is accepted after boot is left explicitly open rather than
guessed, since that is what decides between an architectural note and a primitive: an
accepted load would reinitialise firmware in place while the secure OS runs, and with
thirty-one distinct signed builds of one component in the corpus the interesting case is
loading an older signed build than the running one.
Safety notes are recorded for whoever continues, naming the component identifier that is a
known wedge and the two that would retrain memory on a running system.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Ring creation rejects a base inside the trusted region with a distinct status, and the
interrupt-ring setter is the sibling path that points the processor at a host-supplied ring
which hardware writes into, taking its address from the same register pair, so the question
was whether that path carries the same check.
Every case returns the same error, including the valid address, so the path rejects
everything and is not functional in this configuration. The comparison therefore cannot be
made and the result is recorded as vacuous on the question asked, explicitly not as
evidence that the check is present. Writing a row claiming the path validates its address
would have repeated an earlier mistake where a detector's negative control never engaged.
What can be said is that the identifier is harmless, since the processor stays usable in
all four cases, consistent with the control-space sweep.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The control register carries an identifier in four bits, so there are sixteen possible
commands, eleven named in the headers and five undocumented, of which only ring-create and
destroy had ever been used. This is a command space distinct from both the ring command
space and the application command space and it had never been enumerated.
With one identifier per fresh boot and each run passing its own control first, every one of
the fourteen tested identifiers leaves the processor able to create a ring and run a
command. The two excluded are the destroy command the driver uses normally and the reset
command that resets the chip by design.
The interrupt-ring setter deserved attention since it points the processor at a
host-supplied ring, the same shape as the unvalidated fence destination, but writing it
changes nothing observable, so on this firmware it needs parameters this path does not
supply.
This is the only structure in the ring family with no defect, the frame having two
unvalidated fields and creation an alignment gap, and it exhausts that family.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The identifier failing after the context was configured needed diagnosis rather than a
guess. The level-two protection fault status is zero for the control and a decoded read
fault with walker and mapping errors for the non-zero identifier, at the processor's own
command buffer address, which strengthens the finding: the processor demonstrably issued a
read of its own command buffer through the host-named context, and the clean control
attributes the fault to that frame byte alone.
Two explanations are ruled out by measurement rather than argument: reading the registers
back shows the configuration applied and the context matching context zero exactly, and
the contexts-disable register reads zero. The mechanism is therefore still not fully
explained and no guess is substituted for it.
A conclusion that matters more than the mechanism is recorded: even if a non-zero
identifier could be made to work it would not be a bare-metal primitive, because the host
already owns the working context's page tables and already chooses the frame's addresses,
so redirecting through another host-owned context grants nothing new. The value is
confined to the virtualized hypothesis, which cannot be tested here.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
processor's own accesses, and unvalidated
Sweeping all sixteen dwords of the ring frame found that one kills the processor, and that
offset is not reserved: it holds the virtual-memory identifier and the frame type. Any
non-zero identifier wedges the processor, a seventh host-triggerable trigger needing a
power cycle, while the frame type is ignored entirely.
The interrupt log during the attempt shows memory-controller protection faults tagged with
the host-supplied identifier, at the processor's own command buffer address, so the value
is propagated into the processor's own memory accesses as the translation context with
nothing validating it and death rather than an error status as the failure mode.
What was not achieved is stated plainly: configuring the hub's context one with the same
root page table and flushing its lookaside buffer does not make the non-zero identifier
work, so either a different hub or context set is used or more setup is required, and a
controlled access under a host-chosen context was not demonstrated. A first version of
that experiment measured before and after inside one boot and its verdict is recorded as
void, because one attempt kills the processor so the later control inherited a dead one.
The concerning shape is a virtualized configuration, where a frame field naming the
translation context and honoured unchecked by trusted firmware is a confused deputy; that
is labelled a hypothesis, since the virtualized mode is not available here.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
command buffer
Every other test in this audit varied command content, but the ring frame itself is
equally host-controlled and carries both an address the processor writes to and the value
it writes there. The command buffer address is range-checked against the trusted region;
this destination never was.
Confirmed on hardware: the processor writes a host-chosen value to a host-chosen address;
a destination inside the trusted region is accepted and the command completes with the
processor healthy, where the equivalent command-buffer address is rejected; and a null
destination completes the command but freezes the heartbeat and wedges the card, a sixth
trigger of the known class needing a power cycle.
Corruption is explicitly not demonstrated. Host reads of the trusted region return all
ones so read-back cannot settle it, and a dense functional test of sixteen thousand
host-chosen values at sixty-four byte granularity across the whole first megabyte, where a
loaded application image lives, leaves that application's behaviour identical. The likely
explanation is that the write is subject to the same hardware protection, so the missing
software check is covered. A first sparse attempt of three hundred values over nineteen
megabytes is recorded as discarded for being too thin to support any conclusion.
This is therefore recorded as a defence-in-depth gap rather than an exercisable primitive,
with fixes to range-check the destination and to reject a null one with a status.
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
Every load in this audit used two disjoint allocations, so aliasing was untested even
though it is where a loader that copies the image out and then treats an overlapping
region as the command buffer would fail.
With the buffer equal to the image address, placed inside the image, and with the image
placed inside the command region, every arrangement loads and invokes normally with no
stall, and the processor is healthy afterwards, so the loader copies the image out before
the buffer is used and overlap is harmless.
Aliasing joins the list of correctly handled cases and the objective is not met.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Untried variant: feeding a driver-class blob through the application load command and an
application blob through the driver load command, which would take a mismatched path if
the loader trusted the command rather than the image's own class field.
Both mismatched loads are accepted but neither is confused. The command identifier is
irrelevant and the image's class field governs: the driver-class blob loaded as an
application still refuses invocation with the documented status, and the application
loaded as a driver still invokes normally. This is consistent with the handler coverage
document mapping both load commands to the same handler. There is no stall and the
processor loads a known-good application afterwards.
The objective is not met.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
corpus builds
The last image class never exercised. That class loads but refuses invocation so every
application-side fuzz skipped it, and its load command had only ever been driven with the
in-use image, while the corpus holds seventeen distinct builds each being a differently
shaped signed blob through the loader's parse and authenticate path.
With a processor-health control after every load, requiring a known-good application to
still load, fifteen builds load cleanly and two are cleanly rejected for authentication,
with no stalls and no case where the processor was unhealthy afterwards.
That path and every corpus build of that class are now covered, which was the last image
class outside the application corpus, with no memory corruption anywhere in it. The
objective is not met.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
An earlier pass recorded its own handle test as weak evidence, since harvested handles
were injected at a single offset so a real handle field elsewhere was never exercised and
a command ignoring that offset made the control fire vacuously. That was the only row in
the coverage table not backed by a discriminating test.
The new probe harvests the handles an application emits, invokes every command once so it
can free whatever it frees, then replays each handle at every dword offset, each time
paired with a random value at the same offset as the discriminator, since an offset
accepting the handle while rejecting the random value would be a field that is read and
ownership-checked and therefore a use-after-free if the handle was freed first.
Across three applications no offset in the swept range accepts a stale handle while
rejecting a random value. One application yielded only a single harvested handle, so its
row remains thin and that is stated rather than counted as strong.
The coverage table now has no row backed only by weak evidence, and the objective is not
met.
|
| |
|
|
|
|
|
|
|
|
|
| |
Fifty-three probes were committed but about twenty were not named in any document, which
made them effectively undiscoverable. The index groups them by what they establish:
infrastructure, the denial-of-service triggers with their controls, the boundary checks
found correct, the memory-safety sweeps and their coverage, and the earlier lifetime work.
It records the run and recovery commands, notes that most probes carry an in-run control
and that several results in the log were discarded because a control failed, and flags the
one probe whose evidence is explicitly weak.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The previous pass carried state within one session, while the known lifetime defects are
about slot reuse across sessions, a slot freed by one client and reached by another. This
runs several concurrent sessions, interleaves state-carrying invokes among them, and
randomly unloads and reloads non-survivor sessions mid-run so slots recycle underneath the
survivor, detecting drift in the survivor's fixed re-probe, cross-session marker leakage,
stalls and writes past the window.
Across four applications and forty-two hundred multi-session state-carrying invokes there
is no drift, no cross-session leakage, no stall and no out-of-bounds write.
One incidental observation is kept: the application whose lifetime handling is known to be
defective accepts only two concurrent sessions where the others accept five.
Slot reuse across sessions, the last structural variant of the axis where the only known
defects live, is now covered and negative. The objective is not met.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Every fuzz so far issued independent invokes with a freshly built buffer, which cannot
reach defects needing a sequence of create, use, destroy and use again, precisely the
shape of the known lifetime defects, and an earlier pass recorded that the generic handle
test was too coarse to rediscover them.
The new fuzzer carries state instead, leaving the buffer exactly as the previous invoke
left it so handles the application emits are fed straight back to it on the next command,
including after whatever command might free them, with half the steps additionally
perturbing a few dwords of the carried content. Detection covers non-benign status,
stalls, writes past the window and drift in a periodic fixed re-probe, drift being the
signature of corrupted internal state.
Across six applications and seventy-five hundred state-carrying invokes there is no status
anomaly, no stall, no out-of-bounds write and no drift, so no observable internal-state
corruption accumulates over long sequences. The axis on which coverage was weakest is now
the same as the others, with a working control established before each run and re-checked
periodically. The objective is not met.
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
The previous pass swept single dwords exhaustively, but corruption often needs a
combination such as a length field together with a count field, and the randomized
multi-field fuzz had only ever drawn offsets from the first part of the buffer. Widened to
draw from the whole declared window with up to five simultaneous mutations per invoke,
seven applications and over fifteen thousand invokes produce no non-benign status, no
stall, no write past the declared window and no drift in the periodic fixed re-probe.
Combined with the previous pass's exhaustive single-dword mutations, the shared-buffer
parameter space is now covered both exhaustively for singles and combinatorially for
tuples on real hardware. The objective is not met.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The last surface that was sampled rather than swept. Payload fuzzing had mutated only the
first part of the window or filled broad regions uniformly, so a command reading a
parameter deeper in the sixteen-kilobyte window would be missed by both, and the earlier
ring fix makes a per-dword sweep of the whole window affordable for the first time.
For every dword offset in the window the sweep sets it to all-ones, invokes, and compares
status and output against the unmutated baseline while watching for stalls and writes past
the window. Across three applications that is over twelve thousand single-dword mutations
with no stall, no write beyond the window and no status change. The entries where output
differs are the application echoing input into its output area, since the status is
unchanged and the differing bytes are the mutated value itself, so they are not hits.
Every host-supplied byte, the whole shared buffer, both ring command structures and the
image, has now been mutated at dword resolution on real hardware, and nothing produces a
write outside a host-designated buffer. The objective is not met.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The previous pass characterised every dword of the invoke structure, and the same had
never been done for the load command, whose named fields were fuzzed earlier but whose
command region was never swept word by word.
The first run was an artifact worth recording: it reported about a hundred and sixty
fields changing the outcome, every dword from one offset onward returning the same status,
which is the session-wall code, with the hits beginning at exactly the iteration where the
session limit is reached because the sweep loaded a trusted application per mutation and
never unloaded. Reported as a finding it would have been a hundred and sixty phantom
fields.
With an unload after each successful load, no dword changes the outcome across all
one hundred and ninety-six, so only the named fields are read and every other dword is
ignored, consistent with the targeted tests that found the loader validating exactly the
address, length and alignment of the two buffers.
Both ring command structures are now swept at full resolution and neither yields
corruption. The objective is not met.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
structure
The previous pass showed a wholesale-filled invoke structure is rejected, meaning something
in those bytes is validated, and a validated field is a used field. A full-resolution sweep
of all one hundred and ninety-six dwords, at word stride rather than the earlier coarse
stride, isolates exactly one field.
Characterising its accepted range shows only zero is accepted, small values give one error
code and large values another, with no stall and nothing written beyond the declared window
at any value, so the field is strictly validated.
The invoke ring structure is therefore fully characterised, with almost every dword ignored,
the single field that is read range-checked, and no value producing corruption. The objective
is not met.
|