# Hand-derived TA structures, and the evidence for each **Status: every module covered for its host request, plus five internal structures. Per-function typing remains open and is the bulk of what is left.** 15 of 16 components have a derived request structure; TMZ needs none (its invoke entry is `return 0`). See the summary table below for which are closed against an enforced size and which are not. This file records how each one was obtained so a later reader can check it rather than trust it. Only seven structs existed in this tree before this work (`fw/ta_ida.py`: five GCP, two MERIT), so thirteen of sixteen components decompiled with every pointer typed `int`. The prototypes recovered in `fw/decomp` fixed arity and pointer-ness but not domain types, because none were ever recorded. ## Method 1. **Harvest** offsets and access widths with `fw/ta_struct_re.py`, which reads Hex-Rays output rather than re-implementing register dataflow. 2. **Read the code by hand** at the sites the harvest points to. The harvest is a lead, not a layout: see the caveats below for two ways its raw output misleads. 3. **Author** the struct in `fw/ta_ida.py`, with an expected size and member count. 4. **Read the layout back out of IDA** and compare against that expectation. A struct whose parsed size does not match the size the firmware itself uses is wrong. 5. **Propagate** by re-running `fw/ta_re.py --type `, which declares types before applying prototypes. ### Harvester caveats, both found the hard way * **Array bases are invisible to a naive scan.** They are not written against the argument. GCP computes `v6 = a1 + 80 * HIBYTE(v4)` and then reads `*(_DWORD *)(v6 + 768)`, so `0x300` exists only as an offset on a *derived* pointer. One level of alias tracking is required, and even then what is observed is `base + field`, never the bare base unless the field offset is zero. * **`a1[i]` is scaled by the pointer's declared element width.** Where Hex-Rays types an argument `_BYTE *`, reported offsets are byte-scaled and will not line up with dword fields. XGMI arg1 reports offsets `0x1/0x2/0x3` at 4-byte width from exactly this; they are not fields. The harvester is validated against GCP, whose layout is independently proven in `AUDIT-NEW-TAS-RE.md`: it rediscovers `auth[16]` at `0x300` stride 80, `crypto[64]` at `0x800` stride 88, and `asd_refcount` at `0x1E00`, from code alone, on a build with no types applied. ================================================================================ ## `xgmi_req` — the XGMI host request [VERIFIED, build 32.0.0.20] **Size `0xC20`. Declared in `fw/ta_ida.py` as `XGMI_DECLS`.** ### The mistake this avoided The obvious move is to type a command handler's second argument, since handlers look like `sub_403426(_BYTE *a1, int a2)` with `a2` checked for null and read at `+0` and `+8`. That would have been **wrong**. The dispatcher `sub_402A1C` does not pass the host buffer: ```c _DWORD v12[3]; // [sp+8h] [bp-C20h] BYREF int v15[258]; // [sp+18h] [bp-C10h] BYREF _QWORD v16[257]; // [sp+420h] [bp-808h] BYREF sub_4011B0(v12, 0xC20u); // zero a 0xC20 stack image v8 = *a4; if ( *a4 == 0 || (unsigned int)a4[1] < 0xC20 ) // reject an undersized declared buffer return -65536; sub_4010C4(v12, (char *)v8, 0xC20u, v7); // copy the host page in ... case 9: v11 = sub_4033A8(a1, (int)v16); // handlers get INTERIOR pointers case 0xA: v11 = sub_403426(a1, (int)v15); ``` So a handler's `a2` is `v15` or `v16`, an interior pointer into a staging copy. Typing it as the request base would have placed every field at the wrong offset. ### Derivation Offsets are the decompiler's own stack slots, taken relative to the staging base `v12` at `sp+8`: | local | stack | offset in request | size | |---|---|---|---| | `v12` | `sp+0x08` | `0x000` | — (base) | | `v13` | `sp+0x14` | `0x00C` | 1 | | `v14` | `sp+0x15` | `0x00D` | 1 | | `v15` | `sp+0x18` | `0x010` | `258 * 4 = 0x408` | | `v16` | `sp+0x420` | `0x418` | `257 * 8 = 0x808` | The layout **closes exactly**: `v15` spans `0x010..0x418`, `v16` begins at `0x418` and spans to `0xC20` — the same size used by the zero-fill, the copy, and the size check. `v16`'s offset `0x418` is 8-byte aligned, as its element type requires. Nothing is padded to make this work. The staging image is **1:1 with the host page**, which is what makes these host offsets rather than merely stack offsets: the writeback returns `v16` to its own offset in the host buffer, ```c loc_40114C(v8 + 1048, v16, 2056); // 1048 = 0x418, 2056 = 0x808 ``` and two scalars are written back individually at `v8+8` and `v8+13`, matching `status` at `0x08` and `flag0D` at `0x0D`. ### Verification IDA's parsed layout was read back and matched against the derivation, not assumed: ``` parsed size: 3104 (0xc20) EXPECT 0xC20 -> MATCH cmd_id @ 0x0 unk04 @ 0x4 status @ 0x8 unk0C @ 0xc flag0D @ 0xd pad0E @ 0xe argA @ 0x10 argB @ 0x418 ``` Applied, the dispatcher decompiles as `if ( v12.cmd_id <= 0xB && ... )` where it previously read `v12[0] <= 0xBu`. ### Host-reachability note `cmd_id` is validated `<= 0xB` against a bitmask `0xB6F`, so commands `4`, `7` and `9` are *not* in the accepted set despite having handlers — consistent with the reach table in `AUDIT-NEW-TAS.md` rather than contradicting it. The request requires a declared buffer of at least `0xC20`; a smaller one is refused with `-65536` before any copy. ### Scope: this struct is version-specific Propagation deliberately **refuses** to apply it to the other three XGMI builds, and the refusal is evidence rather than a failure: | build | result | |---|---| | 32.0.0.20 | applied | | 32.0.0.15 | dispatcher not recovered (no switch found within depth 2 of invoke) | | 32.0.0.13 | no local with frame size `0xC20`; its dispatcher uses `0x414`/`0x400` | | 32.0.0.2 | no local with frame size `0xC20`; uses `0x400` | The older builds stage a *different and smaller* request, so forcing `xgmi_req` onto them would misdescribe them. The staging local is located by frame size rather than by name precisely so that a size change shows up as a refusal instead of a silent mistyping — Hex-Rays numbers locals per build, so matching on `v12` would have applied the wrong layout happily. ================================================================================ ## `oc_req` — the OC host request [VERIFIED, build 50.0.0.1] **Size `0x1C`.** `oc_TA_InvokeCommandEntryPoint` is itself the dispatcher — there is no switch, it chains `if`/`else if` on the command, which is why the switch-based dispatcher recovery finds nothing for this component and the invoke entry has to be used directly. ```c if ( (unsigned int)a4[1] < 0x1C ) return ...; // declared size floor v5 = *a4; // the host buffer, used in place v6 = *v5; // command at +0x00 if ( *v5 == 2 ) v7 = oc_cmd02(ctx: (char *)v5 + 22, ...); // +0x16 else if ( v6 == 1 ) v7 = oc_cmd01(ctx: v5 + 3, ...); // int* step -> +0x0C else if ( v6 == 0 ) v7 = ...__h1(a1: (_BYTE *)v5 + 22); // +0x16 v5[1] = v8; // result written back at +0x04 ``` `0x16 + 6 = 0x1C`, so the trailing block ends exactly at the validated size. Unlike XGMI there is **no staging copy**: the handlers operate on the host buffer directly, so every offset here is a host offset by construction. Applied, the entry reads `if ( v5->cmd_id == 2 )` and Hex-Rays propagates the type outward to `struct oc_req **a4`. ## `wspe_req` — the WSPE host request [VERIFIED, build 30.0.4.27] **Size `0x14`.** Same shape as OC, same `if`/`else` dispatch: size floor `0x14`, `v5 = *a4`, command at `*v5`, result written back to `v5[1]` (`+0x04`), and both recovered handlers — `wspe_cmd04_request_dopp_active` and `wspe_cmd05_request_active_protected_session` — receive `v5 + 4`, an `int *` step giving `+0x10`. `0x10 + 4 = 0x14`, closing the request exactly. It applies to 2 of the 7 WSPE builds; the rest do not expose a local assigned from `*a4` in the same shape and are left untyped rather than forced. ================================================================================ ## A wrong signature this work introduced, and the lesson Applying a prototype harvested from `fw/pseudo` **by name** can be wrong. OC's dispatcher calls `oc_cmd01(ctx: v5 + 3, params: (struct ta_params *)7)` — the second argument is the literal `7`, not a pointer, so the `struct ta_params *` prototype inherited from the pseudo file misdescribes it. The cast in the decompiled output is the tell. The general rule this suggests, not yet enforced: a prototype should be checked against the arguments its call sites actually pass before being applied, and a scalar passed where the prototype wants a pointer should block application rather than produce a confident lie. ================================================================================ ## Command-id anchoring, and a mislabel it corrected Matching prototypes to functions by name reached only 8 of GCP's 24, because the tree carries two naming generations that **disagree about which command a handler serves**: | source | name for the handler at `0x402f90` | |---|---| | `fw/pseudo` | `gcp_cmdFE0C_CreateCryptoSession` | | `fw/libdumps` | `gcp_cmdFE0C_CreateAuthenticatedSession` | The hardware findings settle it — `FINDING-GCP-ACQUIRE-GATE-BYPASS.md` records `0xFE0C` as **CreateCryptoSession**, and `FINDING-GCP-FE0D-REFCOUNT-DESYNC.md` agrees — so the libdump name is wrong and had propagated into every artifact. Keying prototypes on the **command id** and resolving the address through the dispatcher avoids the disagreement entirely: the dispatcher is the only authority on which function serves which command. This raised GCP from 5-9 applied prototypes per build to 14-17, lifted its struct-typed signatures from 28 to 172, and relabels the handler at `0x402e94` from `gcp_cmdFE03_CreateAuthenticatedSession` to `gcp_cmdFE07_CreateAuthenticatedSession` on every build — the single conflict the dispatcher cross-check had been reporting all along. ================================================================================ ## Quality measured from the artifacts Judged by reading `fw/decomp`, not by effort spent: | metric | session start | now | |---|---|---| | struct-typed signatures | 309 / 10070 (3.1%) | 456 / 10070 (4.5%) | | modules with a derived request struct | 0 | 3 (xgmi, oc, wspe) | | modules with zero struct-typed signatures | 6 | 4 (dtm, hdcp, ras, tmz) | The rate is still low and the reason is structural: ASD alone is 6763 of the 10070 functions and has no recorded type information at all, so no amount of work on the small components moves the global percentage much. ASD is where the remaining value is. ================================================================================ ## Summary: all 16 components A structure is only as good as its bound. These four groups are deliberately distinguished, because a size that the firmware itself checks is evidence, and a size chosen to cover the furthest offset seen is a convention. ### Closed against a size the firmware enforces (8) | component | struct | size | the floor, in the firmware's own words | |---|---|---|---| | xgmi | `xgmi_req` | `0xC20` | `a4[1] < 0xC20` refused; zero-fill and copy use the same size | | hdcp | `hdcp_req` | `0x2818` | dispatches only when `params[1] > 0x2817` | | oc | `oc_req` | `0x1C` | `a4[1] < 0x1C` refused | | wspe | `wspe_req` | `0x14` | `a4[1] < 0x14` refused | | csu | `csu_req` | `0x24` | size below `0x24` refused | | dtm | `dtm_req` | `0x50` | `*(a4 + 4) < 0x50` refused | | merit | `merit_req` | `0x2024` | `a4[1] >= 0x2024` required | | opm | `opm_req` | `0x3D54` | `*(a4 + 4) < 0x3D54` refused | `opm_req`'s floor independently reproduces the `params_size >= 0x3D54` figure recorded in `AUDIT-NEW-TAS-RE.md`, derived from the entry rather than taken from that note. ### Bounded by internal evidence rather than a size check (1) | component | struct | size | why it still closes | |---|---|---|---| | rap | `rap_req` | `0x34` | the entry zero-fills exactly 24 bytes at `+0x18` and then stores at `+0x30`, so the layout closes without guesswork | ### Size is a maximum observed, NOT a bound (4) | component | struct | size | what is actually known | |---|---|---|---| | asd | `asd_req` | `0x18` | smallest 4-aligned size covering every observed access (max is a byte at `+0x14`) | | powerplay | `powerplay_req` | `0x84` | header hardware-corroborated; params extent is the furthest observed access | | auc | `auc_req` | `0x3BC` | ctx bounded exactly at `+0x10..+0x298`; params extent only observed | | fp | `fp_req` | `0x294` | interior pointers scale off a `_DWORD *`: `v4 + 3` is `+0x0C`, `v4 + 68` is `+0x110` | ### Pre-existing, and needing nothing (3) `gcp` (five structures, proven in `AUDIT-NEW-TAS-RE.md`), `merit`'s slot/table pair, and `tmz`, whose invoke entry is `return 0` -- confirming the "TMZ is a stub" note from the entry itself. ### The ack-word convention, and a negative that needed refining POWERPLAY writes `command | 0x80000000` back at `+0x04`, and the host-page dump in `AUDIT-NEW-TAS-RE.md` (`dw0-3: 0x4 0x80000004 0x415 0`) corroborates that on hardware. It looked like a shared TA ABI worth hoisting into one declaration for all sixteen components, and a scan of twelve entries for an ack at `+4` found none, so it was recorded as POWERPLAY-only. That was too strong. RAP writes the same ack at `+0x08` and CSU at `+0x04`. The convention is real and shared; only its OFFSET is component-specific. Hoisting a single header would still have been wrong, but for a different reason than first stated. ### Inherited prototypes that misdescribe their call sites Applying a `fw/pseudo` prototype by name can assert a type the call site contradicts. Two confirmed cases, both left recorded rather than quietly applied: * OC: `oc_cmd01(ctx: v5 + 3, params: (struct ta_params *)7)` -- the second argument is the literal `7`. * CSU: `csu_cmdNN(ctx: *a4, params: v5)` where `v5` is `a4[1]`, the declared **size**, an integer rather than a pointer. In both the cast in the decompiled output is the tell. A prototype should be checked against what its call sites pass before being applied; that check is not yet implemented. ================================================================================ ## Internal structures (beyond the host requests) | struct | size | reached as | basis | |---|---|---|---| | `tee_fmt_state` | `0x28` | a1 of the shared runtime helpers | byte-identical cluster across 5 modules; RAS names them `vsnprintf_*` | | `asd_link_cfg` | `0xB8` | a1 of `asd_program_link_by_signal*`, `asd_encoder_enable_seq*`, `asd_clear_link_outputs` | union across 18 builds; contiguous dwords `0x04..0xB4` | | `asd_link_caps` | `0x28` | a1 of `asd_get_link_caps_full*`, `asd_resolve_asic_handlers`, `asd_dio_read_block` | contiguous `0x04..0x24` | | `hdcp2_session` | `0x26C` | a1 of the `hdcp2_rx_*` / `hdcp2_tx_*` handlers | union of rx **and** tx handlers; revised twice | | `gcp_access_attrs` | `0x60` | a1 of `gcp_cmdFE0A_QueryAccessibilityAttributes__h8/__h10` | byte runs `0x00..0x07`, `0x40..0x43`, `0x5C..0x5F`, 8-byte read at `0x20` | ### Three ways a structure derived from a subset of its users came out wrong This is the single most repeated failure of this work, and read-back verification caught none of them -- it confirms that IDA parsed what was declared, never that what was declared was complete. 1. **`asd_link_cfg` was half its size.** Derived from the reference build, where the relevant functions never touch `0x10`, `0x1C`, `0x3C` or `0x50`, so those were declared as unused gaps and the structure stopped at `0x58`. Larger builds carry fuller versions of the same functions -- one is 6102 bytes with 692 dereferences -- and they touch every dword to `0xB4` with no gap. 2. **`hdcp2_session` was first refused entirely.** The three heaviest handlers were compared by INTERSECTION, which found two common offsets and suggested three unrelated message layouts. Intersection is the wrong test for a shared context: it shrinks as handlers are added. The union gives 56 offsets and a coherent structure. 3. **`hdcp2_session` was then wrong in two places.** Built from the rx handlers alone, it had byte padding at `0x2C` where the tx handlers read a dword, and a byte run at `0x246..0x24C` buried inside `pad235`. The guard that works is widening the evidence base and re-deriving; the guard that does not work is verifying the declaration against itself. ### Checks that found nothing, and are worth knowing are weak * **Truncation audit over all declared structures**: compares the maximum offset still reached by a raw argument dereference, across every build, against each declared size. Nothing exceeds its bound. But this check could not have caught `asd_link_cfg` either, because the functions that overran it were unnamed and therefore untyped, with no field accesses to compare against. A clean result here is weaker evidence than it appears. * Two apparent anomalies in that audit -- `gcp_ctx` at `0xC` and `merit_table` at `0x0` -- are defects in the audit script, which does not account for nested structure arrays, not in the structures. ### Typing without naming, and its controls Where the heaviest holders of raw dereferences are UNNAMED functions, no name-keyed rule reaches them, and guessing a name would assert an identity the evidence does not support. Typing only claims the pointer's shape, so it is applied on an offset signature instead, with the specificity of the signature checked rather than assumed: * ASD: a function qualifies only if its a1 offsets cover all fifteen core link offsets at once. On the reference's 1440 functions this selects exactly one, with zero false positives. * `tee_fmt_state`: the four-offset signature is too weak alone, so SIZE is required as a second discriminator -- a candidate must match the byte length of a confirmed cluster member. 86 of 88 candidates satisfy both. * HDCP: selects 3 unnamed functions, 135 dereferences, zero false positives among the named. ### A mistyping this avoided `hdcp2_tx_RepeaterAuth_Stream_Manage` holds 164 dereferences through a local, `v10`, which is a direct copy of `a1` -- so typing `a1` would propagate to it, and it was briefly treated as a `hdcp2_session` user. Checking its offsets first showed `0x1`, `0x2`, `0x3`, `0xD`, `0xF9`, `0x106`, which land in that structure's PADDING. It is a message byte buffer, not the session context, and typing it would have misdescribed 164 lines. ================================================================================ ## Quality, measured from the artifacts | metric | at the start | now | |---|---|---| | typed field accesses | 0 | **14020** | | raw pointer-arithmetic dereferences | ~19000 | **6756** | | field share, named functions only | ~0% | **75.6%** | | field share, including unnamed bodies | ~0% | **67.5%** | | struct-typed signatures | 309 / 10070 (3.1%) | **642+ / 10088 (6.4%)** | | modules with a request structure | 0 | **15 / 16** (TMZ needs none) | **The two field-share rows are a correction.** Every figure reported during this work came from globbing `fw/decomp/*.c`, which also matches the `*.anon.c` files holding the bodies of functions that have no name. Unnamed functions are mostly untyped by construction, so including them dragged the headline down: the measurement was of the whole corpus when the claim being made was about the readable, named artifacts. Measured correctly, the named decompilation is at **74.5%** (10,708 typed field accesses against 3,661 raw dereferences), and the unnamed bodies sit at 51.7%, which is higher than expected and shows argument typing reaches them too. Both numbers are now stated because both are true of something, but the named-only figure is the one that answers "are the artifacts readable". The signature percentage badly understates the change and is kept only for continuity: typing one dispatcher or one argument rewrites hundreds of lines while moving the signature count by one or two. The field share is the measure that tracks readability. ### What remains, and why it is not another pass of this method ### The per-function residue, measured per build An earlier version of this table summed each function's dereferences across all 17 builds, which made the residue look concentrated. Measured per *(build, function)* pair, which is what a reader actually opens, the distribution is flat: **median 2, mean 2.7, p90 4, max 33**, spread over 1,360 pairs, and half of all raw dereferences sit across the top 257 of them. The max of 33 is GCP's `start`, the entry stub, where raw pointer arithmetic is correct and typing would be wrong. So there is no large struct-typing lever left. The one real target the measurement did surface was `hdcp2_tx_RepeaterAuth_Stream_Manage` at 24 per build, and it is now done -- see below. After it, the largest remaining named holder is `asd_topology_update` at 24, then a long flat tail. ### hdcp2_tx_RepeaterAuth_Stream_Manage: a wrong note cost this one for a long time This handler was excluded from `hdcp2_session` with the recorded reason that its dereferences "go through a local (v10), not a1, so typing a1 would not reach them". The body says `v10 = a1`. The note was simply wrong, and because it read as a settled finding it was never rechecked. The real disqualifier was the documented padding rule: offsets `0x1-0x3`, `0xD`, `0xF9`, `0x106` and `0x250` all landed inside declared `pad` arrays. Per union-never-intersection those offsets belong in the layout, so `pad00`, `pad08`, `pad55` and `pad24E` were split to name them. The struct size is unchanged at `0x26C`, and all ten observed offsets now resolve to named fields. Result in the artifacts: **24 raw dereferences per build to 1**, with 22 typed field accesses, across all 7 HDCP builds. Corpus named-only field share 74.5% to 75.6%. The rest of the residue is dominated by: * ASD's long tail inside UNNAMED functions -- a naming problem, not a typing one; * accesses through LOCALS rather than arguments (GCP's `start` has 310 on one local), which `retype_arg1` cannot reach and which need `set_lvar_type` per function per build; * byte-buffer accesses, where naming a region adds little readability. Locals that are plain copies of an argument were measured as a possible rule: 380 dereferences in total, and the largest single case is the `Stream_Manage` mistype described above. That is not a multiplier. ================================================================================ ## Not yet derived `xgmi_ctx` (the `_BYTE *` first argument; only the byte flag at `+0` is confirmed, and the harvested arg1 offsets are contaminated by the `a1[i]` scaling caveat above), and all of ASD, HDCP, RAS, DTM, OPM, POWERPLAY, WSPE, AUC, CSU, FP, OC, RAP, TMZ. ASD is the largest prize — 1439 named functions and 681 recorded comments — and the one place a context structure would pay for itself most, but its 6800 prototypes are almost entirely primitive-typed, so there is no recorded type information to start from.