aboutsummaryrefslogtreecommitdiffstats
path: root/psp_out/toctou_sysdrv2.py
blob: 0b271e8f620207149b5e60a9370e74860d1a2a6b (plain) (blame)
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())