aboutsummaryrefslogtreecommitdiffstats
path: root/hw/loadta_windows_sweep.py
blob: 816a79570c10d704994f8e644ae3d671b1ca1c45 (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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
#!/usr/bin/env python3
"""Which of the Windows-driver TAs does THIS Navi 31 PSP accept?

`fw/AUDIT-WINDOWS-DRIVER-CORPUS.md` extracted 19 PSP module containers from the Windows
Adrenalin 22.12.1 package, including ten TA types the linux-firmware bundle never ships
(OPM, MERIT, GCP, WSPE, TMZ, AUC, FP, XGMI, RAP, POWERPLAY). Those containers carry
`apu_family_id = 0xffffffff`, so nothing in the file says which ASIC they target.

The PSP answers that for us: `LOAD_TA` authenticates the container against this part's
keys, so the status separates "wrong part / not for this silicon" from "a TA this PSP
will actually accept" — without invoking anything.

Controls, both required:
  POS  a TA carved from this machine's own `psp_13_0_0_ta.bin` must load (status 0).
       Without it, an all-reject result would be indistinguishable from a broken harness.
  NEG  a junk buffer must be rejected, so "accept" means something.

Any *successful* load allocates a PSP session and this rig has a known
session-exhaustion wall (`hw/loadta_ring_exhaustion.py`), so every success is unloaded
immediately.

usage: PYTHONPATH=/root/psp/tinygrad loadta_windows_sweep.py --dir /root/psp/cont22
"""
import argparse
import glob
import os
import struct
import sys
import time

STATUS = {0x00: 'ACCEPTED', 0x34: 'signature verify failed',
          0x01: 'invalid cmd', 0x02: 'invalid params'}


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument('--dir', default='/root/psp/cont22')
    ap.add_argument('--start', type=int, default=0,
                    help='batch start index; the PSP hits a session wall around 45 '
                         'loads per boot, so sweep in batches with AM_RESET=1 between')
    ap.add_argument('--count', type=int, default=0, help='batch size (0 = all)')
    ap.add_argument('--reverse', action='store_true',
                    help='sweep in reverse order: results must not depend on ordering')
    ap.add_argument('--ta-bin', default='/lib/firmware/amdgpu/psp_13_0_0_ta.bin')
    args = ap.parse_args()

    from tinygrad import Device
    import tinygrad.runtime.autogen.am.am as am
    sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

    adev = Device["AMD"].iface.dev_impl
    psp = adev.psp
    cmd_pa = adev.mm.palloc(0x10000, align=0x1000, zero=True, boot=False)
    mcc = adev.paddr2mc(cmd_pa)
    app_pa = adev.mm.palloc(0x80000, align=0x1000, zero=True, boot=False)

    def load_ta(blob):
        n = (len(blob) + 3) & ~3                      # USB4 needs 4-byte aligned size
        adev.vram.view(app_pa, n)[:] = blob + b'\x00' * (n - len(blob))
        adev.gmc.flush_hdp()
        mc = adev.paddr2mc(app_pa)
        c = am.struct_psp_gfx_cmd_resp(cmd_id=am.GFX_CMD_ID_LOAD_TA)
        t = c.cmd.cmd_load_ta
        t.app_phy_addr_lo, t.app_phy_addr_hi = mc & 0xFFFFFFFF, mc >> 32
        t.app_len = len(blob)
        t.cmd_buf_phy_addr_lo, t.cmd_buf_phy_addr_hi = mcc & 0xFFFFFFFF, mcc >> 32
        t.cmd_buf_len = 0x10000
        try:
            r = psp._ring_submit(c)
            return r.resp.status & 0xFFFFFFFF, r.resp.session_id
        except Exception as e:
            try:
                return int(str(e).strip().split()[-1]) & 0xFFFFFFFF, None
            except Exception:
                return -1, None

    def unload(sid):
        """Unload and REPORT the status.

        The first full-corpus run swallowed this and leaked a session per accepted
        TA, hitting the PSP session wall after ~45 loads; every later command then
        returned a counter-like value incrementing by 0x10 instead of a status, and
        the condition persisted across processes until AM_RESET=1.
        """
        c = am.struct_psp_gfx_cmd_resp(cmd_id=am.GFX_CMD_ID_UNLOAD_TA)
        c.cmd.cmd_unload_ta.session_id = sid
        try:
            return psp._ring_submit(c).resp.status & 0xFFFFFFFF
        except Exception as e:
            try:
                return int(str(e).strip().split()[-1]) & 0xFFFFFFFF
            except Exception:
                return -1

    def show(tag, st, sid, extra=''):
        name = STATUS.get(st, 'status %#x' % st)
        print('  %-34s -> %-24s %s%s'
              % (tag, name, ('sid=%#x' % sid) if st == 0 and sid else '', extra),
              flush=True)

    # ---- POSITIVE control: a TA from this machine's own firmware bundle ----
    import subprocess
    tab = args.ta_bin
    if not os.path.exists(tab) and os.path.exists(tab + '.zst'):
        subprocess.run(['zstd', '-d', '-f', '-o', '/tmp/ta.bin', tab + '.zst'],
                       check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
        tab = '/tmp/ta.bin'
    pos_ok = False
    if os.path.exists(tab):
        data = open(tab, 'rb').read()
        # same carve rules as fw/carve_driver.py
        pos = 0
        while True:
            m = data.find(b'$PS1', pos)
            if m < 0:
                break
            pos = m + 4
            cont = m - 0x10
            if cont < 0:
                continue
            n = struct.unpack_from('<I', data, cont + 0x14)[0]
            if not (0 < n < 0x400000) or cont + 0x200 + n > len(data):
                continue
            body = data[cont + 0x100: cont + 0x200 + n]
            if body[:4] != b'\x00\x02\x00\x00':
                continue
            st, sid = load_ta(data[cont:cont + 0x200 + n])
            show('POS ctrl: bundle TA @%#x' % cont, st, sid)
            if st == 0:
                pos_ok = True
                if sid:
                    unload(sid)
                break
    if not pos_ok:
        print('  POSITIVE CONTROL FAILED -- a bundle TA did not load, so a')
        print('  sweep of rejects below would prove nothing. Stopping.')
        return 1

    # ---- NEGATIVE control ----
    st, sid = load_ta(b'\xa5' * 0x2000)
    show('NEG ctrl: junk 0x2000', st, sid)
    if st == 0:
        print('  NEGATIVE CONTROL FAILED -- junk accepted. Stopping.')
        return 1

    # ---- the sweep ----
    print('\nWindows-driver containers:')
    accepted = []
    prev_unknown = [None]
    KNOWN_ST = {0x00, 0x34, 0xffff0000, 0xffff000a, 0xffff0006, 0xffff0008}

    def degraded(st):
        """Detect the session-wall signature: unknown statuses marching by 0x10."""
        if st in KNOWN_ST:
            prev_unknown[0] = None
            return False
        if prev_unknown[0] is not None and st - prev_unknown[0] == 0x10:
            return True
        prev_unknown[0] = st
        return False
    files = sorted(glob.glob(os.path.join(args.dir, '*.bin')), reverse=args.reverse)
    if args.count:
        files = files[args.start:args.start + args.count]
    elif args.start:
        files = files[args.start:]
    for p in files:
        blob = open(p, 'rb').read()
        st, sid = load_ta(blob)
        extra = ''
        if st == 0:
            us = unload(sid)
            extra = '  unload=%#x%s' % (us, '' if us == 0 else '  <-- SESSION LEAK')
            accepted.append(os.path.basename(p))
        show(os.path.basename(p), st, sid, extra)
        if degraded(st):
            print('\n  ABORT: statuses are marching by 0x10 -- this is the PSP')
            print('  session wall, not a verdict. Results past this point would be')
            print('  fabricated. Re-run with AM_RESET=1.')
            return 2
        time.sleep(0.05)

    print('\nACCEPTED by this PSP: %d %s' % (len(accepted), accepted))
    return 0


if __name__ == '__main__':
    raise SystemExit(main())