aboutsummaryrefslogtreecommitdiffstats
path: root/hw/psp_addr_classify.py
blob: b9ce77049e58b8866693c7fc4518a5d22c9d5404 (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
#!/usr/bin/env python3
"""Does the PSP's status distinguish UNREACHABLE memory from readable-but-invalid?

Step 2 of STRATEGY-MEASUREMENT-FIRST.md. The emulator can only separate
mapped/unmapped because that is all it models; the axis we need
(protected vs absent vs present) only exists on silicon.

LOAD_TA is the lever: the host chooses app_phy_addr, and the PSP reads and
verifies that buffer using the CCP. So the returned status is a function of
what the PSP found at an address WE choose.

Cheapest kill-test first: if an ABSENT address returns the same status as a
readable-but-invalid buffer, there is no access-vs-content separation at all and
the strategy dies here.

CONTROLS (per pspemu.oracle discipline):
  positive : a real TA image at a real address must give the known-good status,
             proving the path works and the status is meaningful
  negative : two DIFFERENT readable-but-invalid buffers must give the SAME
             status, proving the status is not just noise
"""
import sys, struct, traceback
sys.path.insert(0, "/root/psp/work")
from tinygrad import Device
import tinygrad.runtime.autogen.am.am as am

def main():
    dev = Device["AMD"].iface.dev_impl
    adev = dev

    import time
    hb = adev.reg("regMP0_SMN_C2PMSG_81")
    def alive():
        a = hb.read(); time.sleep(0.3); return hb.read() != a
    if not alive():
        raise SystemExit("PSP heartbeat not advancing before we start -- aborting")
    print("PSP healthy before we start")

    def rnd(n, a=0x1000): return (n + a - 1) & ~(a - 1)

    import load_ta
    blob, _ver = load_ta.ta_blob("ras")
    good_pa = adev.mm.palloc(rnd(len(blob)), align=0x1000, zero=True, boot=False)
    cmd_pa  = adev.mm.palloc(0x10000, align=0x1000, zero=True, boot=False)
    junk_pa = adev.mm.palloc(rnd(len(blob)), align=0x1000, zero=True, boot=False)
    junk2_pa= adev.mm.palloc(rnd(len(blob)), align=0x1000, zero=True, boot=False)
    adev.vram.view(good_pa, len(blob), "B")[:] = blob
    adev.vram.view(junk_pa, 0x1000, "B")[:] = b"\xA5" * 0x1000
    adev.vram.view(junk2_pa, 0x1000, "B")[:] = b"\x5C" * 0x1000

    # NO absent-address arm. Aiming LOAD_TA outside VRAM produced a VMC VM-fault
    # storm that wedged the SMU and cost a power cycle (psp_out/CONSTRAINTS.md).
    # Every arm below is inside real, backed memory.
    #
    # TMR is PA [0x300000, 0x1600000) on this card (tmr_paddr=0x300000,
    # tmr_size=0x1300000). Backed VRAM, but protected -- which is the axis the
    # emulator cannot model and the whole reason for going to silicon.
    TMR_PA = 0x900000

    def load_at(mc_app, size, tag):
        c = am.struct_psp_gfx_cmd_resp(cmd_id=am.GFX_CMD_ID_LOAD_TA)
        c.cmd.cmd_load_ta.app_phy_addr_lo = mc_app & 0xFFFFFFFF
        c.cmd.cmd_load_ta.app_phy_addr_hi = mc_app >> 32
        c.cmd.cmd_load_ta.app_len = size
        mc_cmd = adev.paddr2mc(cmd_pa)
        c.cmd.cmd_load_ta.cmd_buf_phy_addr_lo = mc_cmd & 0xFFFFFFFF
        c.cmd.cmd_load_ta.cmd_buf_phy_addr_hi = mc_cmd >> 32
        c.cmd.cmd_load_ta.cmd_buf_len = 0x10000
        try:
            r = adev.psp._ring_submit(c)
            st, sess = r.resp.status, r.resp.session_id
        except Exception as e:
            # tinygrad raises "PSP command failed <cmd> <status>"; the status is
            # the datum, so parse it rather than discarding it as an error.
            msg = str(e); st = None
            try: st = int(msg.strip().split()[-1])
            except Exception: pass
            print("   %-32s status=%-#12x (raised: %s)  psp=%s"
                  % (tag, st if st is not None else -1, msg[:34], "alive" if alive() else "DEAD"))
            return st
        hp = "alive" if alive() else "DEAD"
        print("   %-32s status=%-#12x session=%-#8x psp=%s" % (tag, st, sess or 0, hp))
        return st

    print("addresses: good=%#x junk=%#x junk2=%#x  TMR_MC=%#x"
          % (adev.paddr2mc(good_pa), adev.paddr2mc(junk_pa), adev.paddr2mc(junk2_pa), adev.paddr2mc(TMR_PA)))
    print()
    print("LOAD_TA with the image buffer aimed at different memory:")
    s_good  = load_at(adev.paddr2mc(good_pa),  len(blob), "real TA image (positive control)")
    s_junk  = load_at(adev.paddr2mc(junk_pa),  len(blob), "readable junk A")
    s_junk2 = load_at(adev.paddr2mc(junk2_pa), len(blob), "readable junk B")
    s_tmr   = load_at(adev.paddr2mc(TMR_PA),   len(blob), "inside TMR (protected, backed)")
    print()
    pos = (s_good is not None)
    neg = (s_junk is not None and s_junk2 is not None and s_junk != s_junk2)
    print("CONTROL positive (path works, real image gives a status): %s" % ("PASS" if pos else "FAIL"))
    print("CONTROL negative (two junk buffers agree, status not noise): %s"
          % ("PASS" if not neg else "FAIL"))
    if not pos or neg:
        print("RESULT NOT REPORTABLE"); return 1
    sep = (s_tmr != s_junk)
    print()
    print("readable-invalid = %s   TMR-protected = %s"
          % (hex(s_junk), hex(s_tmr) if s_tmr is not None else None))
    if sep:
        print("=> SEPARATE: a protected source is distinguishable from an unreadable-content one.")
        print("   The classification oracle is REAL on this axis.")
    else:
        print("=> IDENTICAL. Most likely reading: the PSP is PERMITTED to read its own")
        print("   TMR (it owns it), so protection never triggers on a PSP-initiated")
        print("   read. That is a useful fact, but it means LOAD_TA cannot classify")
        print("   protection, and this oracle does not exist via this path.")
    return 0

if __name__ == "__main__":
    try: sys.exit(main())
    except Exception:
        traceback.print_exc(); sys.exit(3)