<feed xmlns='http://www.w3.org/2005/Atom'>
<title>amd-psp-re.git/fw/gadgets, branch main</title>
<subtitle>AMD PSP reverse engineering, emulator and vulnerability research</subtitle>
<id>https://git.sl0p.foo/amd-psp-re.git/atom/fw/gadgets?h=main</id>
<link rel='self' href='https://git.sl0p.foo/amd-psp-re.git/atom/fw/gadgets?h=main'/>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/amd-psp-re.git/'/>
<updated>2026-10-07T18:54:05Z</updated>
<entry>
<title>verify Add.146's reopening condition 1 by hand instead of asserting it; closure stands</title>
<updated>2026-10-07T18:54:05Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-10-07T18:54:05Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/amd-psp-re.git/commit/?id=36f939dc435aec374ddbd15f8077f84757866ae2'/>
<id>urn:sha1:36f939dc435aec374ddbd15f8077f84757866ae2</id>
<content type='text'>
Add.146 closed the LSB-zero line with a structural argument and listed 'a host-aimed store inside a
pointer-returning function's subtree' as reopening condition 1, claiming none exists. That was
reasoning, not measurement.

First attempt at measuring it was bad and is recorded as such: ptr_return_store.py reported 69 hits
per build and classified 73 of 106 functions as pointer-returning, because its heuristic counts
ldr r0,[pc,#imm] literal loads, arithmetic on status values, and pop {r4,...} register restores as
pointer definitions. A detector that flags two thirds of a binary is not evidence. Kept the script
with a header marking it unreliable so its output is not cited later.

Replaced it with the query that actually bears on the claim: the only frames whose base is a return
value are those reaching a pivot with a caller-saved base, so enumerate every such (frame, pivot)
pair across all ten builds. 49 pairs, five recurring frame families, each epilogue read by hand --
all return status codes. Verified 0x403808 specifically, the last unchecked frame feeding one of the
two idx-0 pivots that load both pc and sp (which exist only in .62/.64): it returns 0x266, 0, or a
callee's status.

So condition 1 is measured absent for every pivot-reachable frame. Noted the scope limit honestly:
GCP does have pointer-returning functions, the three liveness-free lookups return slot pointers, but
none of them is ever the frame whose saved LR a host-aimed store can corrupt -- which is exactly the
tension Add.146 described.
</content>
</entry>
<entry>
<title>cross-build sweep closes the LSB-zero pivot line: 235 candidates across 10 builds, all dead for one structural reason</title>
<updated>2026-10-07T18:46:44Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-10-07T18:46:44Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/amd-psp-re.git/commit/?id=a518ce3b76f5b425675015a3ce91c68c9ee632ac'/>
<id>urn:sha1:a518ce3b76f5b425675015a3ce91c68c9ee632ac</id>
<content type='text'>
Acted on Add.145's cheapest lever -- the .77 failure rested on two compiler-layout accidents and
LOAD_TA lets us choose the container, so build selection was the obvious next move. New tool
build_sweep_pivot.py asks the joint question across every build: is there a host-aimed store in a
function F such that a frame live at that store carries a saved LR whose &amp;~0xFF block ARM-decodes to
a PC load off a non-stack base. Body header gives each build's code region exactly, so no guessing.

235 candidate triples. .62/.64 looked like the answer: two idx-0 pivots, landing instruction IS the
gadget, both loading pc and sp off r0, and 0x402f00 is reached by a call to 0x4038c0 -- which in
.50's map is gcp_GetCryptoSessionByDecodeHandle, a lookup that returns a SLOT POINTER, which would
put pc/sp far above the carve floor. Checked it instead of believing it: 0x4038c0 in .62 is a
different function, a wrapper that returns r4 = status 0 or 0x232, so pc=*(r0) reads near 0x232 in
the privileged page at VA 0 and faults, exactly as .77's 0x402b00 did.

That generalises and is the actual result. At a pop-return, r0-r3 are return value and scratch, and
every such function here returns a status code, so every r0-r3 pivot reads a small integer as a
pointer. r4-r11 are restored to the caller's values, which in practice is ctx -- the ctx+0x4f
carve-floor wall already closed in Add.132/133/134. Those two cases account for all 235. The only
exception anywhere is .77's r8 = auth[i], a callee-saved register pointing 0x300 above ctx, and it is
unreachable for the separate reason in Add.145.

The two requirements are in tension by construction: the trick corrupts the LR of a frame that must
contain the write, such frames are command handlers, and handlers return status codes. Getting a
pointer in r0 needs a leaf lookup's LR, but a leaf has no store in its subtree. So the line is closed
across all builds on a mechanism, not an exhausted search, and build selection is eliminated.
</content>
</entry>
<entry>
<title>LSB-zero ARM-mode re-entry: r8 is the one unswept gadget base, route 1 closed by call ordering</title>
<updated>2026-10-07T17:45:14Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-10-07T17:45:14Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/amd-psp-re.git/commit/?id=89b342ab8dd6e8ed16b952836f8f607772a00e6f'/>
<id>urn:sha1:89b342ab8dd6e8ed16b952836f8f607772a00e6f</id>
<content type='text'>
Add.6 proved the saved-LR overwrite on silicon but never evaluated its own aside that P=Q-3 yields
PC = LR &amp; ~0xFF with bit0 cleared, i.e. an ARM-state entry in the same 256-byte block. Evaluated it.

0x405000 ARM-decodes to ldmdaeq r8,{...,sp,...,pc} -- pc=*(r8), sp=*(r8-8), idx-0, executing under
the measured flags, the only aligned entry in the build that loads both pc and sp off a non-stack
base. It matters because Add.132/133/134 closed the ldm class only for bases {r4=ctx, r0/r5/sp} on
the ctx+0x4f carve floor; r8 was never swept, being 0 on the UNLOAD path, and an r8 base sources
from the host command buffer, which is below no floor.

The route does not close the gap, for a reason worth recording. A later PUSH rewrites the word, so
the write and the corrupted return must share a call chain. The FE09 zero-store's live frames yield
only blocks 0x404000 and 0x402a00, neither a pivot. The CodecToken stamp cannot help either, and not
for the frame-destruction reason the report gave: 0x404828 calls 0x404fe8 at 0x404962 and the stamp
fn 0x404b5c at 0x404988, so the frame holding the 0x4050xx call sites is a sibling and already dead.
The UNLOAD shortcut is separately closed -- no function on that path pushes or pops r8.

Qualified the report's 'nothing reads the memory we fully control' to the states we can reach, and
promoted re-entrancy to lead 0. Tooling note recorded: the first call-graph pass used recursive
descent, silently missed the 0x404962 -&gt; 0x404fe8 edge and returned a false EMPTY; rebuilt on a
global over-approximating scan validated against the direct disassembly.
</content>
</entry>
</feed>
