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
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
|
#!/usr/bin/env python3
"""EXPERIMENTAL: route a TA's SVC 0xF2 service-1 calls into a real SYS_DRV.
Status: partially working. It does NOT yet open RAS's init gate, and it does not
report RAS results. Committed because the mechanism works and the remaining
blocker is precisely located, not because the job is done.
Why this exists
---------------
`harness/dtm_ras.py` traced RAS's init gate to a SYS_DRV RPC dependency: RAS's
`TA_OpenSessionEntryPoint` issues four SVC 0xF2 service-1 calls (opcodes 0x0001,
0x000d, 0x0002, 0x1077) and only sets the gate byte at 0x4060e0 when they
succeed. Component-level, service id 1 is bound to `CryptoService` -- the wrong
service entirely -- so those calls could never do anything.
The request RAS builds is already in SYS_DRV's own format, verified by dumping
it at the gate: `[0]=opcode`, `[1]=2`, `{ptr,len}` pairs from dword 2. So the
bridge is the same shape as `Bus.route_asd`: shadow-copy the caller's buffers
into SYS_DRV, rewrite the pointers, run `sub_E087E4`, copy results back.
What works
----------
* SYS_DRV comes up with its init gate genuinely open (`sysdrv_env.run_init`,
opcode 0x1000 -> `init_flag` 0 -> 1, asserted), so this is a real SYS_DRV.
* The CCP is brought up through the FIRMWARE's own routines (`ccp_base_set`,
`ccp_queue_reset`, `ccp_queue_config`) and the four queue globals
0xE257D0/D4/D8/DC are established, as `verify_real_image.py` does.
* All four of RAS's RPCs forward and execute. Three succeed:
opcode 0x0001 -> 0, opcode 0x0002 -> 0, opcode 0x1077 -> 0.
What does not
-------------
Opcode **0x000d returns 0xffff0010**. RAS's helper turns that into 0xa00d, which
is the value reaching `cbnz r5` at 0x402cac and skipping the flag store, so the
init gate stays shut and RAS command coverage is still not reportable.
That error is now traced to its instruction, and the earlier guess that "our
request for 0x000d is wrong" was WRONG -- the request looks fine; SYS_DRV-side
state is missing:
00e1d3a8 ldr.w r8, [pc, #0x34] ; r8 = 0xffff0010 (literal at 0xe1d3e0)
00e1d3ac cmp r0, r6
00e1d3ae bls 0xe1d3b4 ; not taken
00e1d3b0 mov r6, r8 ; error 0x10
...
00e1d3cc mov r0, r6
Measured at that compare: **r0 = 0x20, r6 = 0**. r0 is the 32 bytes RAS asks for
(request dword 3), which is a perfectly ordinary length. r6 is a capacity read
from the context at **r5 = 0xE575B8**, and that context is **all zero after our
init** -- so the check is "you asked for 32 bytes, capacity is 0".
So 0x000d is blocked by uninitialised SYS_DRV context, the same class of gap as
the CCP queue globals at 0xE257D0/D4/D8/DC that used to make opcode 0x0001 fault
(fixed by bringing the CCP up through the firmware's own routines). Note
`pspemu/sysdrv_env.py` already mentions init touching 0xE575D4, immediately
adjacent, so whatever populates this region is close to work already modelled.
P3 RESOLVED -- AND THIS HARNESS IS THE PROBLEM, NOT THE FIRMWARE.
In `harness/full_system.py` the very same bound check at 0xE1D3AC is hit 27
times and **passes every time**: r0 = 0x20 requested against r6 = 0x40 capacity.
So 0x000d's capacity is properly populated in a real boot, exactly as the
hardware result (`psp_out/HW-RAS-ON-CARD.md`) predicted, and the 0xffff0010
failure documented below is an artefact of THIS harness's synthetic SYS_DRV
environment -- not firmware behaviour and not a missing writer.
Consequence: RAS command coverage should be driven inside `full_system.py`,
where the state exists, rather than here. RAS's init gate byte at 0x4060E0 is
still 0 there only because nothing invokes RAS's open-session in that harness
yet; that is the remaining step, and it is a driving problem rather than a state
problem.
Mechanism, for the record (P3). 0xE575B8 is not a scalar capacity field: it
is a **32-entry x 84-byte table** (32*84 = 0xA80), zeroed at init by
`bzero2(0xE575B8, 0xA80)` at `irq_enable_group3+0x238` (0xE0CA5C) -- which is
exactly the all-zero state measured below.
Three functions read it, all checking a valid flag at record+0x20 and an id at
record+0x22: `dram_scratch_map` (indexes base + n*84, n < 0x20),
`opcode_group_index`, and `gp_cipher_update`.
The **writer** is `gp_cipher_update`'s free-slot allocation path at 0xE1CD76:
strb #1, [r4+0x20] ; mark the slot VALID
strb fp, [r4+0x22] ; store the id
str sl, [r4+0x48]
So the capacity RAS's 0x000d reads comes from a record that only exists once a
crypto operation has allocated a slot. The field is zero in our boot because no
allocation ever happens.
Measured limit on that: sweeping opcodes 0x01..0x5F through the real entry
reaches the allocation store **zero** times, so the allocation is not triggered
by a bare RPC -- it needs a prior cipher session/context (an init/update/final
sequence), which is the next thing to construct.
Earlier bounded negative, kept because it is still the evidence that nothing
*else* fills it:
* `sysdrv_env.run_init` writes the region, but only ZEROES it (memset at
0xE019F4/F8, 8 word writes of 0);
* none of RAS's four RPCs (0x0001, 0x0002, 0x1077, 0x000d) write it at all;
* no low opcode 0x01..0x40 changes it;
* none of 36 sampled high opcodes change it -- all 11 ASD-emitted opcodes,
each ACL-range start, and the harness's extras;
* it is still all-zero in the FULL system (`full_system.py`) after SYS_DRV has
executed ~548k blocks, so ordinary boot does not populate it either.
One hypothesis remains untested: that the capacity comes from the caller's
config block (request dword 18) rather than a global. It could not be checked
standalone -- 0x000d faults before reaching the bound check when replayed
outside the bridge, which is the unfaithfulness documented below -- so it has to
be tried with RAS driving.
Whatever the answer, do NOT write a capacity into 0xE575B8 by hand. That would
move every number downstream and mean nothing.
CONFIRMED ON SILICON. The "uninitialised state, NOT a defect" call above is no
longer just the careful reading -- it was checked on an RX 7900 XTX. The card's
own signed RAS 27.0.2.5 (byte-identical body to our ras2.elf) was loaded over
the PSP GFX ring and driven with psp_out/load_ta.py: command 1 returns
ras_status = 0, i.e. RAS processed it rather than short-circuiting on the init
gate at 0x4060E0. That gate only opens via the SYS_DRV RPC chain containing
opcode 0x000d, so on real hardware 0xE575B8 IS populated. The blocker is our
missing boot state, not firmware behaviour.
That run also yields an ORACLE for whenever the gate does open here. Hardware:
cmd 1 -> ras_status 0x0 (success)
cmd 2 -> 0xa002
cmd 3, 4 -> 0xa010
cmd 5..0x10 -> rejected at the ring with 0xFFFF000A
Emulated RAS results that disagree with that table are wrong. See
psp_out/HW-RAS-ON-CARD.md.
Methodology note for whoever picks up 0x000d: **do not replay these opcodes
standalone.** Hand-building a request that looks like RAS's is not faithful --
opcode 0x0001 built by hand returns 0xffff000a and a hand-built 0x000d faults at
0xFFFFFF1C, while the very same opcodes forwarded through this bridge from RAS's
real request return 0x0 and 0xffff0010 respectively. Whatever RAS puts in the
request beyond the fields dumped here matters, so 0x000d has to be investigated
*inside* the bridge, with RAS driving.
**Nothing here is evidence of a SYS_DRV defect.** We hand SYS_DRV a request
assembled from a TA-side capture; an error it returns is far more likely to mean
we built the request wrong than that the firmware is broken.
The region validators are no longer patched here, and measurement is why: with
them left real, all four RPCs return identical results. `sub_E0DEF4` accepted
every address tried, and `sub_E0AB44` is an *opcode allowlist* rather than an
address check -- it already permits 0x000d, the only one of the four with
pointer args. So this bridge runs against the real gates.
"""
import os
import struct
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from pspemu import Component, sysdrv_env # noqa: E402
from pspemu.ccp import CCP # noqa: E402
RAS_OPEN, RAS_CREATE, RAS_FLAG = 0x402C64, 0x402B08, 0x4060E0
SD_ENTRY = 0xE087E4
REQ_BYTES = 0x7C
PAIRS = [(2, 3), (4, 5), (6, 7), (8, 9)] # dword indices of {ptr,len}
RET1 = bytes([0x01, 0x20, 0x70, 0x47]) # MOVS R0,#1 ; BX LR
SYM_CCP_BASE_SET, SYM_CCP_QUEUE_RESET, SYM_CCP_QUEUE_CONFIG = 0xE05008, 0xE050F0, 0xE0503C
G_D0, G_D4, G_D8, G_DC = 0xE257D0, 0xE257D4, 0xE257D8, 0xE257DC
SCRATCH_A, SCRATCH_B, CCP_VA = 0xE50000, 0xE51000, 0x0CB00000
def make_sysdrv(verbose=True):
sd = sysdrv_env.make()
rv, fault, _blocks = sysdrv_env.run_init(sd)
flag = sysdrv_env.init_flag(sd)
if verbose:
print("SYS_DRV: run_init rv=%s fault=%s init_flag=%d" % (rv, fault, flag))
if flag != 1:
raise RuntimeError("SYS_DRV init gate did not open; every RPC would "
"bounce off sub_E087E4 and mean nothing")
# The region validators are NOT patched here. Measured: with them left
# real, every one of RAS's four RPCs returns exactly what it returns with
# them RET1'd (0x0 / 0xffff0010 / 0x0 / 0x0), so the patch bought nothing on
# this path. sub_E0DEF4 accepts every address tried, and sub_E0AB44 -- which
# is an OPCODE allowlist, not an address check -- already permits 0x000d,
# the only one of the four carrying pointer args.
# Bring the CCP up through the FIRMWARE's own routines. Traced: RPC opcode
# 0x0001 ends in `ldr r0,[r3] / add r0,#0x680 / memcpy` at 0xE04D40 with
# r3 = 0xE257D8, a CCP queue global. With it 0 the copy targets 0x680 and
# faults. verify_real_image.py establishes the same four globals the same
# way, after calling ccp_base_set / ccp_queue_reset / ccp_queue_config.
ccp = CCP(sd, base=CCP_VA)
assert sd.run_function(SYM_CCP_BASE_SET, (CCP_VA,)) == 0
assert sd.run_function(SYM_CCP_QUEUE_RESET) == 0
ring = sd.alloc(16 * 32, align=16 * 32)
assert sd.run_function(SYM_CCP_QUEUE_CONFIG, (0, ring, ring, 16)) == 0
sd.wr32(G_D0, SCRATCH_A); sd.wr32(G_D4, SCRATCH_B)
sd.wr32(G_D8, ring); sd.wr32(G_DC, ring)
if verbose:
print("SYS_DRV: CCP up, queue globals set (D8=%#x)" % ring)
return sd, ccp
def make_bridge(sd, log):
scratch, req_buf = sd.alloc(0x8000), sd.alloc(0x100)
cfg = sd.alloc(0x400)
sd.write(cfg, b"\x00" * 0x400)
def bridge(caller, req_ptr):
raw = caller.read(req_ptr, REQ_BYTES)
d = list(struct.unpack("<%dI" % (REQ_BYTES // 4), raw))
orig = list(d)
# Shadow every caller pointer, keyed by VALUE. RAS reuses one buffer in
# two slots -- e.g. opcode 0x000d passes 0x406040 at dword 2 with length
# 0x20 AND at dword 4 with length 0. Translating only the slots that
# carry a length left the second one pointing into RAS's address space,
# and SYS_DRV answered 0xffff0010.
shadows, off, mapped = [], 0, {}
for pi, li in PAIRS:
ptr, ln = d[pi], d[li]
if not ptr:
continue
if ptr in mapped:
d[pi] = mapped[ptr]
continue
n = ln if 0 < ln <= 0x1000 else 0x80 # len 0 => alias, copy a window
sd.write(scratch + off, caller.read(ptr, n))
shadows.append((ptr, scratch + off, n))
mapped[ptr] = scratch + off
d[pi] = scratch + off
off += (n + 0xF) & ~0xF
if d[sysdrv_env.REQ_CONFIG_DWORD] == 0:
d[sysdrv_env.REQ_CONFIG_DWORD] = cfg
sd.write(req_buf, struct.pack("<%dI" % (REQ_BYTES // 4), *d))
try:
rv = sd.run_function(SD_ENTRY, (req_buf, 2, 0, 0), count=20_000_000)
except Exception as e:
f = sd.faults[-1] if sd.faults else {}
log.append((orig[0], "FAULT %s pc=%s addr=%s" % (
f.get("kind"), hex(f.get("pc", 0)), hex(f.get("addr", 0)))))
return 0xFFFF0000
out = bytearray(sd.read(req_buf, REQ_BYTES))
for pi, _li in PAIRS: # never hand back our shadows
struct.pack_into("<I", out, pi * 4, orig[pi])
caller.write(req_ptr, bytes(out))
for o, dst, ln in shadows:
caller.write(o, sd.read(dst, ln))
log.append((orig[0], hex(rv)))
return rv
return bridge
def main():
sd, _ccp = make_sysdrv()
log = []
ras = Component("ras")
for s in (0xF0, 0xF1, 0x5E, 0x5F, 0x76, 0x7B, 0x7C, 0x93, 0x66):
ras.svc.register(s, lambda comp, n: 0)
ras.svc.register(0x8D, lambda comp, n: 0x31) # ASIC id, as fuzz_asd models
ras.set_rpc_services({1: make_bridge(sd, log), 2: lambda comp, req: 0})
try:
ras.run_function(RAS_CREATE, (0, 0, 0, 0))
except Exception:
pass
buf = ras.alloc(0x8000); ras.write(buf, b"\x00" * 0x8000)
p = ras.alloc(0x40); ras.write(p, b"\x00" * 0x40)
ras.wr32(p + 0, buf); ras.wr32(p + 4, 0x4000)
k = ras.alloc(8); ras.write(k, b"\x00" * 8)
before = ras.read(RAS_FLAG, 1)[0]
rv = ras.run_function(RAS_OPEN, (7, p, k, 0))
after = ras.read(RAS_FLAG, 1)[0]
print("\nRAS open_session rv=%#x init flag %d -> %d" % (rv, before, after))
print("SYS_DRV RPCs forwarded:")
for op, res in log:
print(" opcode %#06x -> %s" % (op, res))
print("\n== status ==")
print(" SYS_DRV real, init gate open : yes")
print(" RPCs forwarded : %d" % len(log))
print(" RAS init gate opened : %s" % ("yes" if after else "NO"))
if not after:
print("\n RAS COMMAND COVERAGE STILL NOT REPORTABLE. All four RPCs forward")
print(" and three succeed; opcode 0x000d returns 0xffff0010, which RAS")
print(" turns into 0xa00d and skips the flag store at 0x402cac.")
print(" Traced: bound check at 0xe1d3ac, r0=0x20 (RAS's length) vs r6=0")
print(" (capacity) read from the context at 0xE575B8, which is all-zero")
print(" after our init. Uninitialised SYS_DRV state, NOT a defect.")
return 0
if __name__ == "__main__":
sys.exit(main())
|