<feed xmlns='http://www.w3.org/2005/Atom'>
<title>amd-psp-re.git/fw/decomp, branch main</title>
<subtitle>AMD PSP reverse engineering, emulator and vulnerability research</subtitle>
<id>https://git.sl0p.foo/amd-psp-re.git/atom/fw/decomp?h=main</id>
<link rel='self' href='https://git.sl0p.foo/amd-psp-re.git/atom/fw/decomp?h=main'/>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/amd-psp-re.git/'/>
<updated>2026-10-01T18:57:17Z</updated>
<entry>
<title>regenerate all 16 modules on one decoder; align the RPC naming so diffs mean something</title>
<updated>2026-10-01T18:57:17Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-10-01T18:57:17Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/amd-psp-re.git/commit/?id=330786f61827747677ba83953a0e9a80c7ec70d3'/>
<id>urn:sha1:330786f61827747677ba83953a0e9a80c7ec70d3</id>
<content type='text'>
Finishes the rebuild that was interrupted earlier and leaves the corpus on a single decoder version
for the first time since the SVC-opcode work started. All 16 modules rebuilt clean, 16/16 self-port
gates PASS, no duplicate names in any of the 99 builds, no address-derived names.

The naming fix from the previous commit is now actually in the artifacts: 1,038 SVC stubs use the
libdump's sysdrv_rpc_&lt;N&gt; convention instead of my own rpc_&lt;N&gt;. Six names persisted in fw/cgnames from
when I wrote them by hand are converted too, otherwise they would have quietly reintroduced the
inconsistency on the next rebuild. The 85 remaining bare rpc_ names are of the form gcp_asd_rpc_202 --
ASD's libdump names ported into other modules through shared code, so that is the libdump's own
convention and correct to leave.

Effect on diffs, which is why this mattered: the 22.0.4.49 -&gt; 22.0.4.50 pair reported 8 functions
added and 8 removed with no firmware change behind it, and now reports 4. The remaining 4 are a
different and smaller problem, now documented in ta_diff.py: a function can carry a richer name in one
build than another, gcp_cmdFE0C against gcp_cmdFE0C_CreateCryptoSession, because the semantic half
comes from a pass that only reaches some builds.

Corpus-wide the diffs are now usable. Adjacent patch versions average 0.5 changed functions in ras,
2.5 in wspe, 3.3 in merit, 4.6 in opm, up to 26 in xgmi where the builds are genuinely far apart.
Added/removed should be read as "look at this" rather than ground truth; changed-function counts are
sound.

Metrics unchanged by the rebuild at 19,487 meaningful names (67.3%) and 75.8% field share, which is
the point -- it confirms the mixed-decoder tree was not inflating anything.
</content>
</entry>
<entry>
<title>replace the SVC-opcode heuristic with a structural rule: 940/940 against known names</title>
<updated>2026-10-01T18:37:34Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-10-01T18:37:34Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/amd-psp-re.git/commit/?id=e8d0e168563a7eb9ed08a1c66b3aba2cff62258a'/>
<id>urn:sha1:e8d0e168563a7eb9ed08a1c66b3aba2cff62258a</id>
<content type='text'>
The previous pass left 102 stubs unnamed because they load more than one in-range constant. The
tempting fix was to take the first immediate, which matches 4 of 4 multi-immediate stubs that carry an
established name -- and that number is worthless, because all four are the same XGMI function in four
builds. It is n=1 dressed up as n=4, and applying it to 102 functions would have been a guess with a
validation figure attached.

The structural rule earns the names instead: track MOV/MOVW/MOVS immediates per register, then return
whichever register is stored to [SP,#0], which is the first word of the frame R1 is pointed at before
the supervisor call. Against the 940 stubs the libdump already names *_rpc_N it matches 940 and
disagrees on 0, with no abstentions, and it resolves all 102 stubs the count heuristic could not.

So the weaker rule is superseded rather than patched, and the audit records both along with why the
4/4 figure was rejected -- that reasoning is the part worth keeping.

Regenerated gcp, hdcp, merit, opm and asd. Corpus 19,405 -&gt; 19,433 meaningful names (67.1%), no
duplicate names in any of the 99 builds, address-derived still 0.
</content>
</entry>
<entry>
<title>generalise SVC-opcode naming corpus-wide after validating it: 501 stubs, corpus to 67.0%</title>
<updated>2026-10-01T18:28:07Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-10-01T18:28:07Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/amd-psp-re.git/commit/?id=7437f776849d117d27ba4e0eedc7438debeab652'/>
<id>urn:sha1:7437f776849d117d27ba4e0eedc7438debeab652</id>
<content type='text'>
The opcode decode that closed dtm and csu last commit is not module-specific, so I measured its reach
and validated it before applying it anywhere.

Validation came free: the libdump already names 299 single-immediate SVC stubs *_rpc_N, which is
independent ground truth for exactly this decode. The decoded immediate matches the established
number in 296 of 299. The three exceptions are one XGMI function across three builds whose libdump
name uses a different numbering scheme, not a bad decode.

Applied as a pipeline pass, keyed on bytes rather than addresses so it survives database rebuilds,
and only where exactly one in-range immediate survives -- 102 ambiguous stubs stay unnamed rather
than guessed. 501 stubs named: hdcp 87 -&gt; 96, fp 92 -&gt; 95, gcp 87 -&gt; 91, opm 62 -&gt; 75, merit 76 -&gt; 79,
xgmi 75 -&gt; 76, ras 35 -&gt; 38, asd 64 -&gt; 65, powerplay and wspe smaller. Corpus 19,134 -&gt; 19,405
meaningful names, 66.1% -&gt; 67.0%.

A second corroboration turned up after the fact: HDCP's newly decoded hdcp_rpc_161 carries the same
service number as ASD's independently libdump-named asd_rpc_161, so two modules calling the same
SYS_DRV entry agree without either having been told about the other.

Names are uniquified per build; no build contains a duplicate name, checked across all 99.

Worth noting what made this possible: the claim being overturned was my own, that these stubs "carry
no semantic information whatsoever". It was right about the shared thunks and wrong about the
per-module ones, and I had written it down as settled.
</content>
</entry>
<entry>
<title>decode the RPC opcode out of SVC stubs: csu, oc, tmz and dtm reach 100% named</title>
<updated>2026-10-01T18:17:11Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-10-01T18:17:11Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/amd-psp-re.git/commit/?id=d2a60bfe45543113d383b5448223467d398d81e9'/>
<id>urn:sha1:d2a60bfe45543113d383b5448223467d398d81e9</id>
<content type='text'>
Earlier in this work I recorded that the SVC 0xF2 stubs "carry no semantic information whatsoever",
since the operation is chosen by caller-set registers. That holds for the shared thunks and I
over-generalised it: in the per-module stubs the opcode is loaded by the stub itself.

DTM's three unnamed functions all decompile to the same four lines, "SVC 0xF2; return 2". The bytes
say otherwise. MOV.W R0,#0x1C0, MOVW R0,#0x101 and MOV.W R0,#0x100, each stored as the first word of
the request frame before R0 is set to the selector and R1 pointed at the frame. So the first stored
word is the opcode, and 0x100 independently matches a comment already in this tree reading
"Opcode 0x100 - DTM TOPOLOGY_UPDATE", which is the corroboration that makes this more than a plausible
decode.

Named &lt;ta&gt;_rpc_&lt;hex&gt;, following the existing asd_rpc_NNN convention, and only where exactly one
in-range immediate survives -- otherwise the stub stays unnamed rather than guessed.

Also named by reading the bodies: oc_memcmp_ct, whose loop accumulates "v3 |= *a1++ ^ *a2++" over a
counted range with no early exit and is therefore a constant-time comparison worth labelling;
csu_rpc_309 and csu_rpc_305, the same stub shape with the opcode differing across builds; and
auc_sysdrv_rpc_101C_fwd, an 8-byte forwarder that adds a2+80 as a fourth argument.

csu, oc, tmz and dtm are now 100% named. Corpus 19,126 -&gt; 19,134 meaningful names.

What remains in the small modules is an AUC body of several hundred lines, a RAP command handler and
five FP functions. That is per-function reverse engineering rather than pattern work, which is the
honest characterisation of the rest of the corpus too.
</content>
</entry>
<entry>
<title>name all five GP entry-point slots from the image header: TMZ reaches 100%</title>
<updated>2026-10-01T18:14:27Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-10-01T18:14:27Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/amd-psp-re.git/commit/?id=be8f302f9c02216f7b3d754fcb5b23744737852c'/>
<id>urn:sha1:be8f302f9c02216f7b3d754fcb5b23744737852c</id>
<content type='text'>
ta_entry_points read the trampoline's +0xA0 (Create) and +0xB0 (Invoke) slots and threw away the
three between them. Those are TA_DestroyEntryPoint, TA_OpenSessionEntryPoint and
TA_CloseSessionEntryPoint. That is the whole reason TMZ sat at 50% named: it has six functions and
three of the unnamed ones were entry stubs the header identifies exactly.

The slot order is read off GCP rather than assumed from the GP spec. GCP has all five named
independently and its table reads Create, nullsub(destroy), OpenSession, CloseSession, Invoke, with
WSPE and HDCP agreeing and the types agreeing too -- slots 0, 2, 4 return a status, slots 1 and 3 are
void. Running the new derivation against GCP reproduces GCP's own names exactly, which is the check
that makes this evidence rather than convention.

Applied to every module, only where porting left the address unnamed. Per-module: tmz 50 -&gt; 100,
rap 76 -&gt; 98, csu 70 -&gt; 92, oc 90 -&gt; 97, dtm 75 -&gt; 89. Corpus 19,038 -&gt; 19,126 meaningful names
(65.8% -&gt; 66.1%), field share 75.5% -&gt; 75.8%.

RAS disagrees and is deliberately left alone. Its libdump names TA_CreateEntryPoint and
TA_DestroyEntryPoint at addresses the header assigns to OpenSession and CloseSession, a systematic
two-slot shift across all 18 builds. My first run wrote the header names anyway, which left RAS
asserting two different Create entry points -- a contradiction I introduced and then caught by
checking for duplicated roles. A guard now refuses to add a second holder of a role, so the conflict
is reported on every build and nothing is written. No build has a duplicated entry-point role.

Which source is right for RAS stays open: the obvious tiebreaker, argument count, is not decisive on
functions IDA never prototyped.
</content>
</entry>
<entry>
<title>make the artifacts agree with themselves: 1,018 banner/body name mismatches down to 7</title>
<updated>2026-10-01T18:05:33Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-10-01T18:05:33Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/amd-psp-re.git/commit/?id=94bcea2807457eca208c10456d636a00a5df054c'/>
<id>urn:sha1:94bcea2807457eca208c10456d636a00a5df054c</id>
<content type='text'>
The thunk naming in the previous commits set row["name"] in this script's metadata only, never in the
database. So the emitted banner read j_asd_clear_link_outputs while the signature inside the body
still read j_sub_D07E10_16. Checking for it found 1,018 functions disagreeing with their own banner,
most introduced by the earlier repair pass rather than the newest one. A reader who sees the two
differ cannot tell which to trust, which is worse than leaving the function anonymous -- and I had
claimed in the last commit message that no j_sub_ names remained in any artifact, which was false.

ta_re.py now pushes every derived name into the database and re-decompiles whatever still disagrees.
Two details were needed, both found by measuring the result rather than trusting the step:

Names must be uniquified first. Several distinct thunks branch to the same target, so j_&lt;target&gt; is
not unique and a colliding rename is silently refused -- on the ASD reference only 55 of 786
reconciled until duplicates were suffixed. Those suffixes are honest, unlike the address-derived _N
names dropped earlier: these are genuinely separate functions with a shared destination.

And the resolution has to run after address-derived names are dropped, since before that point the
rows still carry a stale j_sub_&lt;addr&gt; label and look named, so the pass skipped all of them.

Regenerated asd, gcp, powerplay, xgmi, wspe and ras. Mismatches 1,018 -&gt; 7 corpus-wide; the 7 are ASD
functions where the decompiler emits no signature line this check can read. Meaningful names hold at
19,038 (65.8%), address-derived 0, field share 75.5%.
</content>
</entry>
<entry>
<title>resolve 909 ASD thunks by decoding their Thumb B.W branch</title>
<updated>2026-10-01T17:38:41Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-10-01T17:38:41Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/amd-psp-re.git/commit/?id=a834e6790c434ea1ac212d8862fa0537903e9689'/>
<id>urn:sha1:a834e6790c434ea1ac212d8862fa0537903e9689</id>
<content type='text'>
Dropping the false names last commit left a clean question: of ASD's ~4,500 four-byte anonymous
functions, how many are thunks whose target already has a name? The existing repair pass could not
answer it, because it only rewrites names that already read j_sub_&lt;addr&gt; and these rows had no name
at all.

Decoding the branch from the ELF answers it directly. My first attempt decoded them as ARM B and got
nonsense -- 41 words with top byte 0xB8, 37 with 0xBE, 34 with 0xBD. Those are the second halfword of
a 32-bit Thumb-2 B.W, which is how the wrong assumption announced itself rather than passing quietly.

With the correct decode, across 4,530 such functions: 3,402 target a function that is itself unnamed,
909 target a properly named function, 116 target an address IDA never defined, 103 do not decode as
B.W. The 909 are now named j_&lt;target&gt;. The branch displacement says where control goes, so this is a
resolution and not an inference -- the same justification the existing THUNK tier rests on.

One ordering detail cost a run. The pass has to come after the address-derived names are dropped:
before that point those rows still carry a stale j_sub_&lt;addr&gt; label, so they look named and the pass
skipped all of them and reported zero. The comment in the code says so, since the correct order is
not obvious from reading it.

Measured with fw/ta_metrics.py: meaningful names 18,129 -&gt; 19,038 (62.6% -&gt; 65.8%), address-derived
still 0, field share unchanged at 75.6%. No j_sub_ or j_j_ names remain in any artifact.
</content>
</entry>
<entry>
<title>put the name/readability predicates in one module; stop admitting address defaults as names</title>
<updated>2026-10-01T17:25:41Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-10-01T17:25:41Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/amd-psp-re.git/commit/?id=03788cbf4cb0285c3189cd86d55fb6129369c85d'/>
<id>urn:sha1:03788cbf4cb0285c3189cd86d55fb6129369c85d</id>
<content type='text'>
Four metric-definition errors in four commits, each from re-writing the same predicate at a new use
site and getting it slightly too strict. fw/ta_metrics.py now holds the definitions once -- is_addr_name,
is_meaningful, and a measure() that prints every denominator -- and ta_re.py imports them instead of
carrying its own regexes. All four errors are documented in its docstring with the reported and actual
figures, because the pattern is the useful part.

The root cause was in the pipeline, not just the audit. The decompiler-fallback step accepted any
candidate that did not match ^sub_[0-9A-Fa-f]+$, so sub_D07364_12 was admitted as a real name and
1,852 such entries propagated into the artifacts and into every coverage number I quoted.

ta_re.py now drops them, in an order that matters: a j_sub_* candidate is still kept at the fallback
step, because the re-resolution pass needs its target address to recover a real name from it, and
only what remains address-derived afterwards is dropped. Those rows become anonymous and move to the
.anon.c file rather than inflating the count.

Measured with the shared code: address-derived entries 1,295 -&gt; 0, meaningful names unchanged at
18,129 (62.6%), field share unchanged at 75.6%. The drop removes false coverage without losing a
single real name, which is the point -- the artifacts no longer assert that sub_D07364_12 is a name.
</content>
</entry>
<entry>
<title>the thunk fix missed IDA's collision suffix: 713 more names, and the metric was 6 points high</title>
<updated>2026-10-01T17:18:10Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-10-01T17:18:10Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/amd-psp-re.git/commit/?id=827c8abf2731cb02a9971493e57fae4d0d6c4229'/>
<id>urn:sha1:827c8abf2731cb02a9971493e57fae4d0d6c4229</id>
<content type='text'>
Measuring collision suffixes in the artifacts showed names like j_sub_D07364_0 through
j_sub_D07364_20. My address-derived regex anchored on $ immediately after the hex digits, so none of
the suffixed forms matched it -- and j_sub_D07364_12 is exactly as address-derived as j_sub_D07364.

1,852 ASD functions were being counted as meaningful names on that basis. The honest figure was
60.2%, not the 66.6% reported one commit ago. The repair pass added in that same commit skipped them
for the identical reason, so 713 thunks with properly named targets sat fixable and untouched.

Both regexes now accept the suffix. In the artifacts: 713 more names recovered, address-derived
entries 2,008 -&gt; 1,295, meaningful names 17,416 -&gt; 18,129 (60.2% -&gt; 62.6%). The remaining 1,272
thunks point at targets that are not functions in their own build, so j_sub_* stays, which is
correct.

This is the fourth metric-definition error in four commits and the largest, and it is worse than the
others because I made it in code written immediately after describing the problem it repeats. Finding
one instance of a class of error is not evidence the class is clear. A regex that defines a quality
metric needs the same scrutiny as one that drives a rename: an over-strict pattern inflates the
number and hides the work it should have exposed.
</content>
</entry>
<entry>
<title>re-resolve stale thunk names after their targets get named: +87 real names</title>
<updated>2026-10-01T17:11:09Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-10-01T17:11:09Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/amd-psp-re.git/commit/?id=2f2805dc428be6e16a88ccfdb6bc9a08769fcef8'/>
<id>urn:sha1:2f2805dc428be6e16a88ccfdb6bc9a08769fcef8</id>
<content type='text'>
Correcting the headline count last commit exposed 243 functions whose name field was set to an IDA
address default. Looking at what they actually were, rather than treating them as a rounding error,
found a real defect: 220 were j_sub_&lt;addr&gt; thunk names, and 87 of those pointed at targets that by
now carry a proper name.

The cause is ordering. The j_&lt;target&gt; label comes from whatever Hex-Rays printed in the signature at
the moment that thunk was decompiled, so a thunk decompiled before its target was renamed keeps the
stale address forever. Nothing ever revisited it.

decompile_all now re-resolves them from `assigned` after the naming pass. That keeps it a pure
lookup -- the target name is one the pipeline already decided on, so no new inference enters -- and
thunks whose target is still unnamed keep j_sub_*, which is the honest answer rather than a
cosmetic upgrade.

In the artifacts: 87 names recovered across 5 ASD builds, address-derived entries 243 -&gt; 156,
meaningful names 19,181 -&gt; 19,268 (66.3% -&gt; 66.6%). Names like j_asd_build_link_map_full and
j_asd_clear_link_outputs now appear where j_sub_D06CE4 used to.

Worth recording why this was invisible for so long: these functions were being counted as named, so
every coverage number I looked at said they were fine. The metric correction is what made the defect
visible, which is the one case here where fixing a measurement directly produced real improvement
rather than just a more accurate claim.
</content>
</entry>
</feed>
