1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
|
"""Hardened sys_drv (LOAD_IP_FW) delay curve.
Fixes the confound that made the first run uninterpretable: delays were tested in
ascending order while loading type 94 progressively corrupted MEC's stack, so
accumulating GFX damage was indistinguishable from a genuine second read at long
delay.
Hardening:
1. RANDOMIZED delay order -> breaks the time<->delay correlation. Damage
smears across all delays; a real second read
stays pinned to its delay.
2. REAL health gate -> checks GFX VM fault status, not just
sOS/SMU alive (the old check missed the
UTCL2/L2-protection faults entirely).
3. RECOVER-ON-FAULT then resume -> no attempt is scored on a degraded GPU.
4. INTERLEAVED PRISTINE CONTROL -> at every delay. A control failure means the
GPU is sick, not that the flip did anything.
Scoring only counts attempts where the paired control also passed, so results are
immune to drift.
WARNING: type 94 (RS64_MEC_P0_STACK) is NOT inert -- it perturbs live MEC. Run
with psp_recover.py available.
"""
import ctypes, os, random, sys, time
from tinygrad import Device
from tinygrad.runtime.autogen.am import am
from tinygrad.helpers import data64
from psp_out.toctou_bridge import Flipper
FW_TYPE = int(os.environ.get('FWTYPE', 94))
OFF = int(sys.argv[1], 0) if len(sys.argv) > 1 else 0x800
ROUNDS = int(sys.argv[2]) if len(sys.argv) > 2 else 4
SZ = ctypes.sizeof(am.struct_psp_gfx_cmd_resp)
DELAYS = [0, 1024, 3072, 4608, 5632, 6144, 6656, 7680, 8192, 12288, 16384, 24576, 32768, 49152, 65535]
f = Flipper(); adev, psp = f.adev, f.psp; f.select_method(0)
blob = None
for t, bl in adev.fw.descs:
if FW_TYPE in t: blob = bytes(bl); break
assert blob, "no descriptor for fw_type %d" % FW_TYPE
g = blob[OFF]
def stat(): return int.from_bytes(bytes(adev.vram.view(psp.cmd_paddr, SZ)[:])[864:868], 'little')
def gfx_faulted():
"""True if GFX reports a VM/L2 protection fault. The previous run's health
check missed exactly this and scored attempts on a corrupted GPU."""
for rn in ("regGCVM_L2_PROTECTION_FAULT_STATUS", "regGCVM_L2_PROTECTION_FAULT_CNTL"):
try:
if adev.reg(rn).read() not in (0, 0xFFFFFFFF): return True
except Exception: pass
return False
def healthy():
try:
return (adev.reg(f"{psp.reg_pref}_81").read() != 0
and adev.smu.is_smu_alive() and not gfx_faulted())
except Exception:
return False
def submit():
c = am.struct_psp_gfx_cmd_resp(cmd_id=am.GFX_CMD_ID_LOAD_IP_FW)
c.cmd.cmd_load_ip_fw.fw_phy_addr_hi, c.cmd.cmd_load_ip_fw.fw_phy_addr_lo = data64(psp.msg1_addr)
c.cmd.cmd_load_ip_fw.fw_size = len(blob); c.cmd.cmd_load_ip_fw.fw_type = FW_TYPE
try: psp._ring_submit(c); return stat()
except Exception: return stat()
def recover():
print(" !! fault detected -- recovering", flush=True)
try:
u = adev.pci_dev.usb; u = getattr(u, 'usb', u)
u.control_write(0xF3, value=0, timeout=10000); time.sleep(2.5)
u.control_write(0xF3, value=1, timeout=10000); time.sleep(2.0)
except Exception: pass
print(" (fresh process required to fully re-init -- stopping)", flush=True)
sys.exit(2)
def attempt(delay, corrupt):
psp._prep_msg1(memoryview(blob))
f.set_ab(OFF, g, (g ^ 0xFF) if corrupt else g)
f.cmd(7, OFF, widx=delay)
return submit()
if __name__ == '__main__':
print("sys_drv LOAD_IP_FW fw_type=%d off=%#x genuine=%#04x health=%s"
% (FW_TYPE, OFF, g, healthy()))
base = sum(1 for _ in range(3) if (psp._prep_msg1(memoryview(blob)), f.disarm(), submit())[2] == 0)
ctrl = attempt(0, True)
print("baseline %d/3 corrupt-control status=%#x" % (base, ctrl))
if base < 2 or ctrl == 0:
print("GATE FAILED -> refusing to produce a curve"); sys.exit(1)
res = {d: [0, 0] for d in DELAYS} # [passes, scored]
order = [(r, d) for r in range(ROUNDS) for d in DELAYS]
random.shuffle(order) # <-- kills the time<->delay correlation
print("\nrandomized order, %d attempts (control-paired):" % len(order), flush=True)
for i, (_r, d) in enumerate(order):
if not healthy(): recover()
c_st = attempt(d, False) # paired pristine control
if c_st != 0: # GPU sick -> don't score this one
if not healthy(): recover()
continue
st = attempt(d, True) # real flip
res[d][1] += 1; res[d][0] += (st == 0)
if (i + 1) % 20 == 0:
print(" %3d/%d done" % (i + 1, len(order)), flush=True)
print("\ndelay curve (scored only when paired control passed):")
for d in DELAYS:
ok, n = res[d]
r = (ok / n) if n else 0
print(" %-7d %2d/%-2d %5.0f%% %s" % (d, ok, n, 100 * r, '#' * int(r * 30)))
scored = [(d, res[d][0] / res[d][1]) for d in DELAYS if res[d][1]]
tail = [r for d, r in scored if d >= 24576]
mid = [r for d, r in scored if 6656 <= d <= 16384]
print("\nmid-plateau (6656-16384): %s" % (["%.0f%%" % (100 * x) for x in mid]))
print("long tail (>=24576) : %s" % (["%.0f%%" % (100 * x) for x in tail]))
if mid and tail:
print("\n-> %s" % ("TAIL DIP PERSISTS under randomization => candidate SECOND READ"
if (sum(tail) / len(tail)) < (sum(mid) / len(mid)) - 0.2 else
"tail matches mid => earlier dip was cumulative damage, NOT a second read"))
print("final health:", healthy())
|