#!/usr/bin/env python3 """Headless IDA type/prototype work on the TA databases, via idatui's Nexus client. Rationale: the TUI's struct editor is the wrong layer for this. It is a TextArea driven by injected keystrokes, so a stray focus change silently types a declaration into the filter box, `ctrl+s` then saves nothing, and the only symptom is "save failed - IDA couldn't parse it". Worse, a half-applied redeclare truncated three structs to their first member and the damage was invisible until pseudocode regressed. `NexusClient` leases the same idalib worker the TUI uses and exposes all of `remote_ops`, including the three things that matter here and are NOT reachable over the TUI's RPC: declare_type parse a whole set of C declarations in one unit type_inspect read a type's members back <- real verification, not screen scraping set_type apply a prototype by ADDRESS (no cursor/view dependency) Stop any TUI pane on the same binary first; the worker lease is exclusive. Usage: ./ta_ida.py --prefix gcp [--apply] [--verify-only] """ from __future__ import annotations import argparse import json import re import sys from pathlib import Path HERE = Path(__file__).resolve().parent REPO = HERE.parent sys.path.insert(0, str(Path.home() / "dev" / "ida-tui-maybe")) ELF = {k: v["elf"] for k, v in json.loads((HERE / "syms" / "index.json").read_text()).items()} # One parse unit, dependency-ordered. Declaring these separately is what made the TUI # route reject the nested members: each save was its own parse and the referenced types # were not visible to it. GCP_DECLS = r""" struct gcp_drm_session { unsigned int handle; unsigned int state; unsigned char body[16]; }; struct gcp_auth_session { unsigned int handle; unsigned int state; unsigned char body[72]; }; struct gcp_crypto_session { unsigned int handle; unsigned int unk04; unsigned long long key; unsigned long long field10; unsigned long long decode_handle; unsigned int state; unsigned char rest[52]; }; struct gcp_ctx { struct gcp_drm_session drm[32]; struct gcp_auth_session auth[16]; struct gcp_crypto_session crypto[64]; unsigned int asd_refcount; unsigned int unk1E04; unsigned int handle_counter; }; struct ta_params { unsigned int cmd_id; unsigned int unk04; unsigned int unk08; unsigned int unk0C; unsigned long long arg10; unsigned long long arg18; unsigned char data20[6440]; unsigned int out1948; unsigned int unk194C; unsigned int out1950; }; """ # expected size and member count, from the layout proofs in fw/AUDIT-NEW-TAS-RE.md EXPECT = { "gcp_drm_session": (0x18, 3), "gcp_auth_session": (0x50, 3), "gcp_crypto_session": (0x58, 7), "gcp_ctx": (0x1E10, 6), # 0x1E0C of members, tail-padded to 8-byte alignment "ta_params": (0x1958, 10), # 8-byte args force 8-byte alignment } # XGMI's host request, derived in fw/AUDIT-TA-STRUCTS.md rather than assumed. # # The dispatcher sub_402A1C does NOT hand handlers the host buffer: it zeroes a 0xC20-byte # stack image (sub_4011B0(v12, 0xC20)), copies the host page into it # (sub_4010C4(v12, *a4, 0xC20)) after rejecting a declared size below 0xC20, and passes # handlers v15 or v16 -- interior pointers. Treating a handler's second argument as the # request base would therefore have produced a wrong layout. # # Offsets are the decompiler's own stack slots relative to v12 at sp+8, and the layout closes # exactly: v15 at +0x10 spans 258*4 = 0x408 and ends at 0x418, where v16 begins and spans # 257*8 = 0x808, giving 0xC20 -- the same size the copy and the size check use. v16's offset # is 8-byte aligned, as its element type requires. # # The staging image is 1:1 with the host page: the writeback # loc_40114C(v8 + 1048, v16, 2056) returns v16 to host offset 0x418, its own offset. XGMI_DECLS = r""" struct xgmi_req { unsigned int cmd_id; unsigned int unk04; unsigned int status; unsigned char unk0C; unsigned char flag0D; unsigned char pad0E[2]; int argA[258]; unsigned long long argB[257]; }; """ # A structure shared by EVERY TA, because the helper that uses it is literally the same code in # each: the "__" names come from ta_libmatch.py, which only accepts a cluster when the # function bytes match exactly across modules. So unlike the ack-word guess recorded against # powerplay_req, declaring one shared type here rests on byte identity rather than on a # resemblance. # # The same offsets appear through a1 in gcp, powerplay, wspe and xgmi (0x08, 0x18, 0x1C, 0x20), # and RAS -- where the cluster carries real names, vsnprintf_fmt_int / vsnprintf_pad / # vsnprintf_prefix / vsnprintf_fmt_ll -- adds 0x10, 0x14 and the tail 0x24 byte, 0x25 byte, # 0x26 word. That identifies it as the TEE runtime's string-formatting state and closes the # layout at 0x28. # # CAVEAT: as for the other internal structures, no code validates a size; 0x28 covers every # observed access. TEE_RUNTIME_DECLS = r""" struct tee_fmt_state { unsigned int unk00; unsigned int unk04; unsigned int field08; unsigned int unk0C; unsigned int field10; unsigned int field14; unsigned int field18; unsigned int field1C; unsigned int field20; unsigned char field24; unsigned char field25; unsigned short field26; }; """ # Applied to any module whose function NAME contains one of these fragments. Keyed on the # content-hash fragment so it follows the cluster across modules and versions. HDCP2_ARG1 = { "hdcp2_rx_AKE_Send_Pairing_Info": "struct hdcp2_session *", "hdcp2_rx_RepeaterAuth_ReceiverID_List": "struct hdcp2_session *", "hdcp2_rx_AKE_Send_Cert": "struct hdcp2_session *", # Transmit-side handlers over the SAME context, added after a second union pass. Their # offsets were part of the union that produced the layout above, so they are covered by # construction. "hdcp2_tx_AKE_Init": "struct hdcp2_session *", "hdcp2_tx_SKE_Send_Eks": "struct hdcp2_session *", "hdcp2_rx_AKE_Send_H_prime": "struct hdcp2_session *", # Added after correcting a wrong note. The old comment said this handler's dereferences go # through a local "not a1" so typing a1 would not reach them. The body says "v10 = a1", so # v10 IS a1 and typing it does reach them. The real disqualifier was the documented padding # rule: offsets 0x1-0x3, 0xD, 0xF9, 0x106 and 0x250 all landed inside declared pad arrays. # Per the union-never-intersection rule those offsets belong in the layout, so pad00, # pad08, pad55 and pad24E are now split to name them and the handler is covered. "hdcp2_tx_RepeaterAuth_Stream_Manage": "struct hdcp2_session *", } # Local-variable names for the handlers that carry recorded hardware findings. These are the # functions a reader actually goes to, and vN names make the defect hard to see even once the # structures are typed. # # Each name below is read off the code, not inferred from the finding text: # # gcp_cmdFE0C_CreateCryptoSession -- the acquire-gate bypass # slot_handle_bits accumulates 0x1000000 per slot and stops at 0x40000000, i.e. the slot # index in bits 24+ of the handle, over crypto[64]. Matches the finding's # "handles carry the slot index in the high bits, idx << 24". # refcount_before *p_asd_refcount read BEFORE the increment; the acquire runs only when # this is zero, and is not rolled back when it fails. This single value # is the bug. # handle_counter ctx->handle_counter, whose low 24 bits form the rest of the handle. # acquire_status the gcp_asd_rpc_305 result, returned as status | 0x20000000. # status the handler's return status. # # Applied through fw/ta_re.py so they survive a database rebuild, like every other derived name. LOCAL_NAMES = { "hdcp": { # hdcp1_first_part_authentication carries the hardware-confirmed HDCP overflow # (AUDIT-TA-MEMSAFETY-HUNT records it at 0x402cde, length from a byte struct field). # session_idx is the request dword that indexes the stride-652 session table; status is # the value returned. Note the register allocation differs between builds, so the # documented r5 is not v12 in every one -- the names are keyed on what each local does # in this build, and misses are reported rather than forced. "hdcp1_first_part_authentication": { "v5": "session_idx", "v12": "status", }, }, "gcp": { "gcp_cmdFE0C_CreateCryptoSession": { "v4": "slot_handle_bits", "v7": "status", "v8": "refcount_before", "v9": "handle_counter", "v12": "acquire_status", }, # gcp_cmdFE0D_DestroyCryptoSession -- the refcount desync. # arg10 is the caller's handle; the only bound check is "arg10 >> 30 != 0". # slot is (char *)ctx + 88 * HIBYTE(arg10): stride 88 over the crypto array, # which is why *((_DWORD *)slot + 512) is ctx+0x800, the array base, and # +520 is that slot's state field at +0x20. # status is 0 only when the slot's stored handle matches; 256 otherwise. # The refcount is decremented and the ASD release issued REGARDLESS of that status, # which is the defect, so refcount_after and release_status are named to make the # ordering legible. "gcp_cmdFE0D_DestroyCryptoSession": { "arg10": "handle", "v5": "status", "v6": "slot", "v8": "refcount_after", "v9": "release_status", }, # gcp_cmdFE0B_DestroyAuthenticatedSession -- the correct counterpart to FE0D, and the # reason FE0D is a defect. Same shape: handle bounded by "handle >> 28 == 0" (16 auth # slots), slot = (char *)ctx + 80 * HIBYTE(handle) with stride 80, so slot+192 dwords is # ctx+0x300 (the auth array base) and +198 is that slot's state at +0x18. # # The difference that matters: here the refcount decrement and the ASD release sit # INSIDE the branch where the stored handle matched. FE0D performs them regardless of # its status. Naming both identically makes the asymmetry legible by comparison rather # than by reading the finding. "gcp_cmdFE0B_DestroyAuthenticatedSession": { "arg10": "handle", "v5": "status", "v6": "slot", "v8": "refcount_after", "v9": "release_status", }, # gcp_cmdFE02_GetObfKeyCryptoSession -- the key-use oracle lead. Only the two values # whose role is unambiguous are named; this handler is long and most of its locals are # not established, so the rest are deliberately left as vN rather than guessed. "gcp_cmdFE02_GetObfKeyCryptoSession": { "arg10": "handle", "v7": "slot_idx", }, # gcp_cmdFE0F_GetCryptoSessionForDevice -- the stale slot lookup. The loop walks # all 64 slots comparing decode_handle without ever testing the slot's state, so # remaining is named to show the walk is unconditional. "gcp_cmdFE0F_GetCryptoSessionForDevice": { "i": "remaining", "v6": "status", }, }, } # Content-hash placeholder names that ASD's symbol set already knows by name. The equivalence # is byte identity, so it is a fact rather than an inference: each hash below names a body # that appears verbatim in both a TA and in ASD, where it has a real name. # # Applied BY NAME and allowed to override the porting result. That ordering matters: the # previous attempt renamed these in the database only, and the emitted artifacts kept showing # the placeholder because decompile_all uses the porting-assigned name. OPM was the only # module that appeared to work, because its libdump happens not to carry the placeholder. LIB_RENAME = { "lib_18cd5c37_72": "asic_name_lookup", "lib_3853955a_44": "vsnprintf_fill", "lib_52b900d8_88": "memcmp_be", "lib_5487ced7_696": "aeabi_fcmp_sign", "lib_5da1e11f_220": "aeabi_lshift_merge", "lib_7fca7c43_82": "vsnprintf_out_str", "lib_836f1b02_34": "vsnprintf_pad_left", "lib_897f4feb_48": "strcmp_bounded", "lib_918f1510_28": "vsnprintf_fmt_hex", "lib_a446f54e_228": "aeabi_d2f_pack", "lib_a7faaa9b_72": "vsnprintf_wide_char", "lib_b56825b4_392": "aeabi_uidiv", "lib_bcf5b308_48": "aeabi_dbl_classify", "lib_d3aec194_44": "udiv10_32", "lib_d83c24bf_152": "udiv10", "lib_db5482d6_100": "memcpy_aligned", "lib_db876c77_580": "aeabi_dcmp_sign", "lib_e2ae1a48_42": "aeabi_dbl_op_a", "lib_e53fbaaa_32": "vsnprintf_setup", "lib_eeee582a_42": "aeabi_dbl_op_b", "lib_f6e388c1_24": "vsnprintf_fmt_octal", # Named by reading the body, not by analogy. Each is small enough to be unambiguous: # isdigit "(unsigned)(a1 - 48) < 0xA" # vsnprintf_getc reads the byte at state->field10 and advances it # vsnprintf_emit_char "*(*a2) = c; *a2 += 1" -- unbounded store # vsnprintf_emit_char_bounded the same with "cur < end" checked first # vsnprintf_narrow_signed flag 0x400 -> (char), 0x100 -> (__int16), else int # vsnprintf_narrow_unsigned same shape with unsigned casts # The pairing of the last two, and of the two emit variants, is what makes them certain: # they differ only in signedness and in the bound check. "lib_cede31a8_14": "isdigit", "lib_3f07c814_10": "vsnprintf_getc", "lib_672a6323_10": "vsnprintf_emit_char", "lib_852e460a_16": "vsnprintf_emit_char_bounded", "lib_5f016438_18": "vsnprintf_narrow_signed", "lib_3cf46bff_18": "vsnprintf_narrow_unsigned", # Second round, named from their bodies after the seed-consistency fix made applying them # safe. Reasons, since a name is an identity claim: # vsnprintf_out_str_guarded "if (state->field14 == 0) vsnprintf_out_str(a1,a2,a3)" -- # a guard on the same field, calling the already-named writer # vsnprintf_out_guarded the same shape against *(a1+20), which IS field14 # vsnprintf_fmt_integer switches on the conversion char (a2 == 117, 'u') and calls # vsnprintf_narrow_unsigned # call_once_init "if (*g == NULL) { SVC 0xF3; *g = a1; } return (*g)(...)" -- # lazy one-time init through a supervisor call # runtime_globals returns the fixed global off_4022CC # runtime_globals_field1 returns runtime_globals() + 1 "lib_71f66102_20": "vsnprintf_out_str_guarded", "lib_78974a43_20": "vsnprintf_out_guarded", "lib_e4cd8bbc_96": "vsnprintf_fmt_integer", "lib_c457fbe2_30": "call_once_init", "lib_06936d05_4": "runtime_globals", "lib_a1b8c8fb_10": "runtime_globals_field1", # Still unnamed on purpose. lib_33b2ae33_16 writes 2*a2 into an uninitialised-looking # register base; lib_e6824d52_24 and lib_1bc49253_28 are two near-identical conversion # arms I cannot separate; lib_366a1ceb_178 and lib_8680f995_112 are the two large # formatters and I can see they handle width/precision but not which is which; # lib_d732abea_240 and lib_2cd87843_108 are arithmetic/format bodies whose role I would be # guessing at. 111 instances left placeholder rather than mislabelled. # entry_trampoline -> ta_main_loop. The body is __noreturn and reads # while (1) { SVC 0xF0; v6 = *a1; switch (v6) { ... } v7(); } # -- it yields to the OS, dispatches on the request code at *a1, calls the handler and # loops forever. That is a main loop. A trampoline does not loop, so entry_trampoline is # simply the wrong word for it; DTM, RAS, HDCP and ASD already call the byte-identical # function ta_main_loop, and the 54 instances under the other name are the odd ones out. "entry_trampoline": "ta_main_loop", # Vendor log strings overrule a derived name. fw/libdumps/gcp.json seeds 0x403a88 as # cmdFE0F_GetCryptoSessionByDecodeHandle, but that function's own log string reads # "AMD_TA_GCP: GetCryptoSessionForDevice", and the string that says # GetCryptoSessionByDecodeHandle is referenced by a different function entirely # (0x4038d8/0x4038d0, which the libdump had filed as a fragment of cmdFE0A). A string # inside the body is the vendor naming the function; my name came from reasoning about a # dispatcher arm. The vendor wins. "cmdFE0F_GetCryptoSessionByDecodeHandle": "cmdFE0F_GetCryptoSessionForDevice", } # PENDING, not applied. CAUSE STILL UNKNOWN -- two explanations have been offered and both were # wrong, so this comment records what is established and nothing more. # # Established: the names are correct. Each body is small enough to be unambiguous, and the # pairings confirm them (the two emit variants differ only in the bound check, the two narrow # variants only in signedness). A single rename in isolation works: on a fresh database for # build 22.0.0.53, 0x405d70 is sub_405D70, renaming it returns ok=1, and no other name changes. # # Established: applying the six as part of LIB_RENAME upgrades 162 placeholder instances and # costs 138 names across gcp, powerplay, wspe and xgmi, with the six addresses coming out # UNNAMED in non-reference builds while the same entries apply cleanly in the reference. # # Ruled out: a mechanism defect in the rename path (works in isolation); forced-overwrite # stealing a name from a prior holder (splitting into forced and collision-refused batches # changed nothing); and collision with an existing symbol -- there is NO pre-existing # gcp_isdigit anywhere in the artifacts or in any libdump, which is what the previous version of # this comment claimed and it was false. # # They stay out because a net loss of 138 names is not worth 162 cosmetic upgrades, not because # the reason is understood. # Semantic name corrections, applied BY NAME so they hold across every build. # # The libdump symbol set mislabels two GCP handlers, and the mislabels have been propagating # into every artifact. Both are contradicted by three independent sources: # # * the code. gcp_cmdFE0C iterates ctx->crypto, zero-fills a 0x58 gcp_crypto_session and # sets crypto->state/crypto->key; gcp_cmdFE0D computes "(char *)ctx + 88 * HIBYTE(arg10)", # stride 88, the crypto array at ctx+0x800, while gcp_cmdFE0B uses stride 80, the auth # array at ctx+0x300. # * AUDIT-NEW-TAS-RE.md, which states exactly that split: "FE0B is DestroyAuthenticatedSession # (stride-80 array at ctx+0x300) and FE0D is DestroyCryptoSession (stride-88 array at # ctx+0x800)". # * the hardware findings. FINDING-GCP-ACQUIRE-GATE-BYPASS records 0xFE0C as # CreateCryptoSession and FINDING-GCP-FE0D-REFCOUNT-DESYNC agrees. # # The duplicate name was the tell: FE0B and FE0D were both called DestroyAuthenticatedSession. # # Note this is NOT the same defect as the FE07 relabel, which the dispatcher cross-check caught # because that name claimed the wrong COMMAND. These two claim the right command and the wrong # semantics, so no command-id check can see them -- only reading what the code touches. RENAME_FIX = { # fp_cmd03__h28 is byte-identical to RAS's aeabi_uldivmod, a 64-bit division helper. The # cmd03__h28 name is a mis-split: the libdump broke cmd03 into fragments and labelled this # one as a piece of the command handler, when the body is the division routine. The # arithmetic body settles it -- a command handler does not consist of shift-subtract # division. "fp": { "fp_cmd03__h28": "fp_aeabi_uldivmod", }, "gcp": { "gcp_cmdFE0C_CreateAuthenticatedSession": "gcp_cmdFE0C_CreateCryptoSession", "gcp_cmdFE0D_DestroyAuthenticatedSession": "gcp_cmdFE0D_DestroyCryptoSession", }, } # Per-module name-keyed arg1 typing, consulted generically by fw/ta_re.py. ARG1_BY_TA = {"asd": "ASD_ARG1_STRUCT", "hdcp": "HDCP2_ARG1", "gcp": "GCP_ARG1"} # Body hashes of the tee_fmt_state cluster, keyed on CONTENT rather than on name. # # The previous rule matched name fragments (lib_e53fbaaa_32, vsnprintf_, ...), which tied the # typing to the very names LIB_RENAME upgrades: enabling that rename moved these functions # out from under the rule and cost ~1175 typed field accesses. Content identity is what the # cluster was established on in the first place, so it is what the typing should key on, and # it is immune to any renaming. # # 71 hashes, derived from every function the fragment rule used to match across all 99 # builds, so the switch cannot narrow coverage. TEE_FMT_HASHES = ( "00d6a99096cd42d127c070954effc77898201629b00128baee0035c7f9b822ca", "07d4446985446c00117bc0a11cd1704eb4956a91e79c05e62487ebb0cc802378", "094fdd7d2a802302ce3dd09a8292f9023d712dc7c570b1c4ae5b85d76c062d2f", "16aa06d99f67d8e2cc926761044d58623efed78d6b0071e5160dbac418c9b41a", "179ab9f4c1515f0ca55f96f1d9ff7e36aef31138002e8d8f7eb126fb4e0ee1f7", "1aa24120f7dab13825a080c26221c5a94150eb90c787ee9739795cdccc5efab6", "1e16ecc908ba6a1ee9e52865a0d7dc5e4c3578c1d0ea4f6c4f756d04409541e7", "21c2dd0bb7173308e251d0a93b689d00a19c71fd1149550e872339990f1e76f0", "24979c85aee02f2ab1fb7929b3e5bc857b0c826be235290ca60bf60e6b24a266", "28df36146b6e669cf230819e082f280b8168949739cddfb9fdbd636cb499c965", "2eb4a1c858de9a5ee86f58a794102bedb61b37c8a097ca4a5dc2c8c625fccf63", "30d654e491609fcf04176beb76de22e3b83b13866bee07a71f2820415406e8eb", "318cd212928007a52c034d985a9bcf17b72133bbd4bf4e29ad381ee621346749", "3332341496e9f44368e2daa1345a78da9b5e3509b66e43f5221ff0af2e68b930", "34e3fb2ebda6469fa6141e6e9a6dc910ff4981198111034ff20f33e2cab901d1", "366a1cebd6f1e481f5d77fc1def8b3d27f74055d3f55cc0ab56e23e3f979657b", "3853955a021a13b485fc22bd1b84670e2ff95ae8ad9d74a2d4e2212c0666c93d", "3cf46bff2aaba4c22833b6d11fe9eaf3b965faef9bee986b5292fdd487ed6634", "3f07c814dbdcfd26473df43452c69e4b770f0437de75db36cc37796498b9714e", "43ad0ffdd3f27f8584858512e2c84b449800257f3d1cccb49938fd19d58bc068", "532bc9d6ddcddb1e191e0b9b9f57aa3252fa0e77ccd76d3662e38eaf8a60747e", "5a21f0efa577d45ef593e1377c755b9e75fdc595bb66f3fe63923c523a166676", "5ab3a36eba3b3212aed4ca306d66dad31151348b4f42807638e464eb26a52c68", "5b01919597b7beb7b2312f7c5238d7b4c3e641576cd02f9b38ba52ff5455193a", "5cafa94c9b7143c299e8a75f9369c9485c0a7fc859b507677ac89cf3433a9370", "5f016438f075aa23d3fc07316a4ac552503bbe2e267a4b8090b4189679c8dcaa", "672a63239992c561e7ed97727a7876ac47e2654d081efb9640920a6a8346c54c", "6d880b6578c95d8c6d440f9c99678d906b7bbea56bad4144381ba25d90775427", "7134bdc4cdca40506b53359ae1f258c68b4b522c4444e01d3da3770ad1a0b17b", "724e6ba96580c09904b2c5f57f706e0b7ee1033e76edbd6bb32d15cbd2bdbe16", "77b70240d6537c583ff30448c816927920e25c9506a57f53870fa6ccb8f6a926", "7a1de03fea4edaac39d3015c6f36692ddff9b77cb0056b99e3dd3326707999ea", "7fca7c43246efe935ead5531c52a8056a741129ba843acfcd4ba13a4b5234053", "802b1dc74899a151207d7cf96f4328acb60fab57641509ba15046ea566efcf34", "836f1b0217fe4f25f7dc98c92a839d561b2dcb7c4143e9c0c2498ff3e89f85bb", "846b4491ad3a9a2425729f5ab697a8e17d4f4d7a06e4432e3cb21dd7fa4cbb81", "852e460a54cc23652953d8c2bdbb7715df1868b604fce3e7c9f9ee34d83e133b", "8680f995484160494ff3f5e6c7dade75cd60ae86bea8c00e6c43087a6512a9f9", "8802a73b618885135e7e04611a85428976a8114a6dfa963ad064673ebb7cae80", "918f1510322ca3035a785bb82e2bc2f5f324db4ad87964c4c580f247342b859e", "95017fe0d8ed4e2c89b5f302424002fdd5ae5328b9fc9bc8edec88f06267e370", "97e73c622d7a446fba930a47e8e73dacc1b1e67fa2981e0b10cbe6c71578bef3", "9cf2cf32b2bff131119354641523fcd6f34d7175f2ff1cd52fb11e0ad1ac440d", "a7faaa9b2050fdc0d9c2c07b5dc391f58f364719feccf801a0ca39b1fc4561a4", "af12371ecf9ae3ef87095840a4a11bc2553d2a321fbef62488b51f3ade5a2a6c", "b1a18d118c239da05431d51eb8f3b399653f94e5e2345a34bad2bdf17a36505b", "b209ec10a2746f494a1a5614f1c0a687db2fc2a883a238b2ae352f4e659593c8", "b4060464eea402fa6466670dfc72aa58c89437fc673bda4d51e548c4a0f41721", "b66f32102894418e390f33fb34ad13fd5ceee3dd6f384262d0f52ceb17ec3610", "bcc36cd9aa71b01c852f4ea88f99759964cff23866fc38d9c85699bffd112e77", "c0b7ee18110f3ca2275af45160a04719c1c1125e7db80504aafd6715f6d37522", "c122c8b647e93cfc904da6a9f3a480a17fb53110b7ef308ed7388286bd458c4a", "c2be825f2f1415933eb09391f314ba57ddb96eb8a5cd86a8fd385a1904f95bd8", "c3b0ce82587879abb4ae8fefe4fc544ca4b725bd6c3df81489d55dddd60737f4", "c784eeaa106d4c2ccd72dce499fb1ea6b271592eefce987c70212b230d47017f", "d448f19c895e95a03cd628c96223831476ef1a47a6464d03319245d88a9485d7", "d5a46a938e11362552c8c26af17cdac4056a39340fa2cf4d31728ac3c9c6f437", "d6cc29f6cf492ca3a6e9863b38f59675075df06a02c51337470dff1188f1defe", "dbf5d9887143199856468cead0aaf09c0f7913ba7879e033cc81073f5492457d", "e07deb7a25c703c47f650e091bc5bf0feaf57fd44d4e3ae86130bd4619260e01", "e1b98cc5c55aca948c7055031372a6b4f1313294e7c681ae7b69b9a93fcca7f3", "e53fbaaa91c8c1d619fdf964aa5688811d4bc836d42d8276c82ef40863b0c0a7", "e74c746b0b1d9756d673323d4a0130a9e527e70ae0c6f38799da954042cf273c", "e8ec7dc2a51fd0a77239826c894e2548209c2246910b219017edc6f8ce3527a1", "ee9d1928fb42773e739f4096ce361426db3c0c6ed8d8a7b304737904a715e2c9", "f2eee990c27d5b3d1b905acfd566559ad86fe64a045b3f189199c196ae13330b", "f30120b45931c04fc20b8fe92ffce44d96fb9eef77793c1b9aabab1382c5cb6f", "f5d189db1d10a5848f603ade7e4e885416c6837fd0ac02f4812203d90f4570b5", "f6dcdcc20e480cedd9552d87e46c2ee47bd90d48db024d4a0e7306e5a50dc573", "f6e388c198c32f5a61201235d46f83804b35fb3f7cad11bce078305927d9277f", "f9e78ab4ed5ab2a0ad31b06efd18e0f70986432afba4deea9b99c02a2dffcc51", ) # Both the hash-placeholder names and the semantic names they are being upgraded to, because # the upgrade is applied through the durable name map and the fragments must keep matching # either way. The semantic names come from ASD's symbol set, which knows these byte-identical # helpers by name while the TA side only knew them by content hash. SHARED_ARG1 = [ ("lib_e53fbaaa_32", "struct tee_fmt_state *"), ("lib_7fca7c43_82", "struct tee_fmt_state *"), ("lib_8680f995_112", "struct tee_fmt_state *"), ("lib_366a1ceb_178", "struct tee_fmt_state *"), ("vsnprintf_", "struct tee_fmt_state *"), ] # Structures reached through an argument other than the first. The arg1 machinery cannot type # these, so hdcp2_process_auth_message_v2 kept 161 raw dereferences and hdcp1_create_session 49 # purely because their structure arrives as a2. # # hdcp2_auth_msg: contiguous dwords 0x04..0x24 then an 8-byte read at 0x28, which is 8-aligned, # so the layout closes at 0x30. # # hdcp1_session_req: sparser. The 8-byte read at 0x04 is NOT 8-aligned, so it is declared as two # dwords rather than a 64-bit field -- declaring it as one would force padding and shift every # later offset, which is the mistake asd_link_cfg's 0x14 qword read nearly introduced. HDCP_ARGN_DECLS = r""" struct hdcp2_auth_msg { unsigned int unk00; unsigned int field04[9]; unsigned long long field28; }; struct hdcp1_auth_req { unsigned int session_idx; unsigned int field04; unsigned char field08; unsigned char field09; unsigned char field0A; unsigned char field0B; unsigned char field0C; unsigned char field0D; unsigned char field0E; unsigned char pad0F; unsigned short field10; unsigned short field12; }; struct hdcp1_session_req { unsigned int unk00; unsigned int field04; unsigned int field08; unsigned int unk0C; unsigned char unk10; unsigned char field11; unsigned short field12[4]; unsigned char unk1A[5]; unsigned char field1F; }; """ # hdcp1_auth_req is the block the invoke entry passes as v7 + 12, i.e. hdcp_req.argA. Its # first dword is named session_idx on direct evidence: the handler reads it and then indexes # the HDCP1 session table with "dword_40237C + 652 * that value", a stride-652 array. # Offsets 0x00..0x12 observed across all 7 builds, closing at 0x14. # (function -> (argument index, type)), consulted generically by fw/ta_re.py. ARGN_BY_TA = { "hdcp": { "hdcp1_first_part_authentication": (1, "struct hdcp1_auth_req *"), "hdcp2_process_auth_message_v2": (2, "struct hdcp2_auth_msg *"), "hdcp1_create_session": (2, "struct hdcp1_session_req *"), }, } # Per-module signature typing, consulted generically by fw/ta_re.py. # (required_offsets, struct, min_distinct, distinctive_offsets) # A candidate qualifies only if ALL its a1 offsets lie inside the structure's declared fields, # it touches at least min_distinct of them, and at least one is a distinctive offset. HDCP's # rule was measured when hdcp2_session was derived -- 3 unnamed functions, 135 dereferences, # zero false positives among the named -- and then never wired in, so those dereferences stayed # raw in the three builds where the function is unnamed and the name-keyed rule cannot see it. SIG_BY_TA = { "hdcp": ("hdcp2_session", "struct hdcp2_session *", 10, (0x107, 0x192, 0x225, 0x254)), } # Signature-based typing, for functions the name-keyed list cannot reach. # # The heaviest holders of ASD's remaining raw dereferences are UNNAMED functions in the larger # builds -- the top two are 6102 and 3906 bytes with 692 and 562 dereferences. No name-keyed # rule can touch them, and guessing a name would assert an identity the evidence does not # support. Typing is different: it only claims the pointer's shape. # # The predicate is deliberately narrow. A function qualifies only if its a1 offsets COVER all # fifteen of the core link offsets below. Requiring fifteen specific offsets simultaneously is # strong: on the reference's 1440 functions it selects exactly one, asd_program_link_by_signal, # with zero false positives. A looser rule such as "subset of a contiguous dword range" would # match almost anything and is not used. ASD_SIG_CORE = (0x0C, 0x14, 0x18, 0x20, 0x24, 0x28, 0x2C, 0x30, 0x34, 0x38, 0x40, 0x44, 0x48, 0x4C, 0x54) ASD_SIG_TYPE = "struct asd_link_cfg *" ASD_SIG_MIN_SIZE = 256 # skip small functions; the holders are all large # GCP's accessibility-attribute block, a1 of gcp_cmdFE0A_QueryAccessibilityAttributes__h8 # (176 dereferences) and __h10. Observed as byte runs at 0x00..0x07, 0x40..0x43 and 0x5C..0x5F # with a single 8-byte read at 0x20, so it is a tag/blob structure rather than a field record. # Declared as named byte arrays: the readability gain is smaller than for the other structures # here -- a1->tag40[1] instead of *(a1 + 0x41) -- but the regions get names and extents. GCP_INTERNAL_DECLS = r""" struct gcp_access_attrs { unsigned char hdr[8]; unsigned char pad08[24]; unsigned long long val20; unsigned char pad28[24]; unsigned char tag40[4]; unsigned char pad44[24]; unsigned char tag5C[4]; }; """ GCP_ARG1 = { "gcp_cmdFE0A_QueryAccessibilityAttributes__h8": "struct gcp_access_attrs *", "gcp_cmdFE0A_QueryAccessibilityAttributes__h10": "struct gcp_access_attrs *", } # HDCP2's receiver-side session context, reached as a1 by the hdcp2_rx_* handlers. This is a # SECOND structure for HDCP, distinct from the hdcp_req host buffer above. # # An earlier pass looked at the three heaviest handlers -- AKE_Send_Pairing_Info, # RepeaterAuth_ReceiverID_List and AKE_Send_Cert, 590 dereferences between them -- took their # INTERSECTION, found only two common offsets, and concluded they were three unrelated message # layouts. That was the wrong test, and the same mistake that made asd_link_cfg come out half # size: handlers over a shared context each touch different fields, so the intersection shrinks # with every handler added while the union is what describes the structure. They do in fact # share 0x04, 0x10, 0x18 and 0x24, and two of them share the byte run at 0x107..0x10E. # # The union across all builds gives 56 observed offsets from 0x04 to 0x268, with widths taken # from the accesses themselves: dword runs at 0x18..0x28, 0x30..0x34, 0x3C..0x48 and # 0x254..0x268, byte runs at 0x4F..0x54, 0x107..0x10E and 0x225..0x234, halfwords at # 0x192..0x198, and single flag bytes at 0x2D, 0x38 and 0x24D. Unobserved spans are padding # rather than invented fields. # # REVISED once already: the first version of this layout came from the three rx handlers alone # and got two things wrong that the tx handlers then exposed -- 0x2C is read as a DWORD where it # had been byte padding, and 0x246..0x24C is a byte run that fell inside pad235. Both are now # fields. The union therefore has to include every handler over the context, not just the # heaviest ones, which is the third time this pass that a layout derived from a subset of its # users came out wrong. # # CAVEAT: size 0x26C is the extent of what is observed, not a bound the firmware checks. The # padding runs are genuinely unknown, not known-unused -- exactly the situation that understated # asd_link_cfg, so a richer build may well extend this. HDCP_INTERNAL_DECLS = r""" struct hdcp2_session { unsigned char pad00; unsigned char field01; unsigned char field02; unsigned char field03; unsigned int field04; unsigned char pad08[5]; unsigned char field0D; unsigned char pad0E[2]; unsigned int field10; unsigned char pad14[4]; unsigned int field18[8]; unsigned char field38; unsigned char pad39[3]; unsigned int field3C[4]; unsigned char field4C; unsigned char pad4D[2]; unsigned char field4F[6]; unsigned char pad55[164]; unsigned char fieldF9; unsigned char padFA[12]; unsigned char field106; unsigned char field107[8]; unsigned char pad10F[131]; unsigned short field192[4]; unsigned char pad19A[136]; unsigned short field222; unsigned char field224; unsigned char field225[16]; unsigned char pad235[17]; unsigned char field246[8]; unsigned char pad24E[2]; unsigned int field250; unsigned int field254[6]; }; """ # Signature typing for the TEE formatting state, for instances the name-keyed rule misses. # # 88 functions across gcp, powerplay, wspe and xgmi dereference a1 over exactly the subset # {0x08, 0x18, 0x1C, 0x20} of tee_fmt_state and are not reached by the hash-name fragments -- # they are the same helpers in builds where the cluster name was not applied, so they appear # as sub_*/ANON. 802 dereferences in total. # # Four offsets alone would be a weak signature, so SIZE is required as a second discriminator: # a candidate must be the same byte length as a function that IS a confirmed cluster member. # 86 of the 88 satisfy that jointly, and the four sizes involved (112, 178, 606, 758) all occur # among confirmed members. Offset-set-only matching is deliberately not used. FMT_SIG_OFFSETS = (0x08, 0x10, 0x14, 0x18, 0x1C, 0x20, 0x24, 0x25, 0x26) FMT_SIG_SIZES = (112, 178, 606, 758) FMT_SIG_TYPE = "struct tee_fmt_state *" FMT_SIG_MIN_DEREFS = 8 # Two ASD INTERNAL structures, as opposed to the host request asd_req above. They matter # because ASD's unreadability is extremely concentrated: of its 8446 raw pointer-arithmetic # dereferences, 6314 go through a1, 86% of those sit in just 20 functions, and those fall into # two families that share an offset set. # # Family 1, the link/encoder descriptor, from asd_program_link_by_signal{,_v2..v5}, # asd_encoder_enable_seq{,_v2,_v3} and asd_clear_link_outputs. # # CORRECTED: the first version of this structure stopped at 0x58 and declared 0x10, 0x1C, # 0x3C and 0x50 as unused gaps, because the reference build's functions never touch them. # Larger builds carry fuller versions of the same functions (one is 6102 bytes with 692 # dereferences) and those touch EVERY dword from 0x04 to 0xB4 with no gap at all -- so the # gaps were an artefact of the reference's narrower coverage, not a property of the structure. # It is therefore a contiguous dword array, 45 observed offsets, size 0xB8. Dwords throughout # rather than 64-bit fields at the handful of offsets also read as _QWORD (0x08, 0x10, 0x80, # 0x88, 0x90, 0xA8), since several of those are not 8-aligned and would shift everything after. # # Family 2, the link capability / handler block, from asd_get_link_caps_full{,_v2,_v3}, # asd_resolve_asic_handlers and asd_dio_read_block. Offsets 0x04 through 0x24, contiguous. # # CAVEAT: these are internal structures reached through a function argument, so unlike the # eight host requests with enforced floors there is no size check anywhere. Both sizes cover the # furthest observed access and are maxima observed, not bounds. ASD_INTERNAL_DECLS = r""" struct asd_link_cfg { unsigned int unk00; unsigned int field04; unsigned int field08; unsigned int field0C; unsigned int field10; unsigned int field14; unsigned int field18; unsigned int field1C; unsigned int field20; unsigned int field24; unsigned int field28; unsigned int field2C; unsigned int field30; unsigned int field34; unsigned int field38; unsigned int field3C; unsigned int field40; unsigned int field44; unsigned int field48; unsigned int field4C; unsigned int field50; unsigned int field54; unsigned int field58; unsigned int field5C; unsigned int field60; unsigned int field64; unsigned int field68; unsigned int field6C; unsigned int field70; unsigned int field74; unsigned int field78; unsigned int field7C; unsigned int field80; unsigned int field84; unsigned int field88; unsigned int field8C; unsigned int field90; unsigned int field94; unsigned int field98; unsigned int field9C; unsigned int fieldA0; unsigned int fieldA4; unsigned int fieldA8; unsigned int fieldAC; unsigned int fieldB0; unsigned int fieldB4; }; struct asd_link_caps { unsigned int unk00; unsigned int field04; unsigned int field08; unsigned int field0C; unsigned int field10; unsigned int field14; unsigned int field18; unsigned int field1C; unsigned int field20; unsigned int field24; }; """ # The functions whose FIRST argument is one of the two structures above. Applied by rewriting # the first parameter of each function's live prototype, so the rest of the signature -- which # came from the recorded handover prototypes -- is preserved. ASD_ARG1_STRUCT = { "asd_program_link_by_signal": "struct asd_link_cfg *", "asd_program_link_by_signal_v2": "struct asd_link_cfg *", "asd_program_link_by_signal_v3": "struct asd_link_cfg *", "asd_program_link_by_signal_v4": "struct asd_link_cfg *", "asd_encoder_enable_seq": "struct asd_link_cfg *", "asd_encoder_enable_seq_v3": "struct asd_link_cfg *", "asd_get_link_caps_full": "struct asd_link_caps *", "asd_get_link_caps_full_v2": "struct asd_link_caps *", "asd_get_link_caps_full_v3": "struct asd_link_caps *", "asd_resolve_asic_handlers": "struct asd_link_caps *", "asd_dio_read_block": "struct asd_link_caps *", # Later-version family members, found by enumerating the families across all 18 builds # instead of across the reference alone. Each was checked to use only offsets that are a # strict SUBSET of the structure it is being given, rather than assumed from its name. "asd_program_link_by_signal_v5": "struct asd_link_cfg *", "asd_encoder_enable_seq_v2": "struct asd_link_cfg *", "asd_clear_link_outputs": "struct asd_link_cfg *", "asd_get_link_caps_full_v4": "struct asd_link_caps *", "asd_get_link_caps_full_v5": "struct asd_link_caps *", # asd_topology_update is deliberately NOT here: it dereferences a2, not a1, over a # different offset set, so neither structure describes it. } # MERIT's host request. The entry refuses a declared size below 0x2024, takes v6 = *a4, # dispatches on *v6, and passes handlers the whole buffer rather than an interior pointer. MERIT_REQ_DECLS = r""" struct merit_req { unsigned int cmd_id; unsigned char body[8224]; }; """ # DTM's host request. Floor of 0x50 enforced at the entry (*(a4 + 4) < 0x50 is refused), host # buffer v5 = *(int **)a4, command at *v5, and both recovered handlers # (dtm_cmd_topology_update_v2, dtm_cmd_set_flag) receive v5 + 4 -- an int* step, so +0x10. # 0x10 + 0x40 = 0x50, closing exactly against the floor. DTM_DECLS = r""" struct dtm_req { unsigned int cmd_id; unsigned int unk04; unsigned int unk08; unsigned int unk0C; unsigned char args[64]; }; """ # OPM's host request. The entry enforces a real floor -- it refuses when # *(_DWORD *)(a4 + 4) < 0x3D54 -- which independently reproduces the "invoke entry requires # params_size >= 0x3D54 (15700)" already recorded in AUDIT-NEW-TAS-RE.md. The host buffer is # v6 = *(unsigned int **)a4, the command is *v6, and v6[4] (+0x10) is read on the cmd==11 path. # Handlers receive v6 itself rather than an interior pointer. OPM_DECLS = r""" struct opm_req { unsigned int cmd_id; unsigned int unk04; unsigned int unk08; unsigned int unk0C; unsigned int field10; unsigned char body[15668]; }; """ # CSU's host request, and the cleanest of the small ones: the entry refuses a declared size # below 0x24, takes v6 = *a4, dispatches on *v6 over commands 1..3, and writes back # v6[1] = *v6 | 0x80000000 and v6[2] = status. 0x0C + 0x18 = 0x24 closes against the floor. # # Note the handler calls are csu_cmdNN(ctx: *a4, params: v5) where v5 is a4[1] -- the declared # SIZE, an integer, not a pointer. The 'struct ta_params *' prototype inherited from fw/pseudo # misdescribes that second argument, the same defect already recorded for OC. CSU_DECLS = r""" struct csu_req { unsigned int cmd_id; unsigned int cmd_ack; unsigned int status; unsigned char body[24]; }; """ # AUC's host request, from auc_TA_InvokeCommandEntryPoint: v4 = *a4, command at +0x00, # status written back to *(v4 + 8), handlers given v4 + 16 (ctx) and v4 + 664 = 0x298 (params). # ctx is bounded exactly by the next block; the params extent is the furthest offset observed # through handler second arguments (+0x120 dword), so the total is a maximum observed and not # an enforced bound -- nothing here validates a size. AUC_DECLS = r""" struct auc_req { unsigned int cmd_id; unsigned int unk04; unsigned int status; unsigned int unk0C; unsigned char ctx[648]; unsigned char params[292]; }; """ # FP's host request, from fp_TA_InvokeCommandEntryPoint. v4 is a _DWORD *, so the interior # pointers it hands out scale: v4 + 3 is +0x0C (ctx) and v4 + 68 is +0x110 (params). The # command is at *v4 and the status is written back to v4[1] (+0x04). Same size caveat as AUC: # the params extent is the furthest observed access (+0x180 dword), not a validated bound. FP_DECLS = r""" struct fp_req { unsigned int cmd_id; unsigned int status; unsigned int unk08; unsigned char ctx[260]; unsigned char params[388]; }; """ # RAP's host request, from rap_TA_InvokeCommandEntryPoint, and the best bounded of this group # because the entry zero-fills a block of a stated length: # # loc_401000(v4 + 6, 24); // zero 24 bytes at +0x18 # rap_dispatch(*v4, v4[1], v4 + 4, v4 + 6, &v8) // cmd, arg1, +0x10, +0x18 # v4[12] = v8; v4[2] = *v4 | 0x80000000; v4[3] = status; // +0x30, +0x08, +0x0C # # The zero-fill bounds scratch to 0x18..0x30 and v4[12] sits immediately after it at +0x30, so # the layout closes at 0x34 without any guesswork. # # This also REFINES the negative recorded for powerplay_req. The claim there was that # POWERPLAY is the only entry writing an ack word; the test looked for it at +4. RAP writes the # same 'command | 0x80000000' at +0x08. The ack convention is therefore not unique to # POWERPLAY, it merely sits at a different offset per component -- which is still a reason not # to hoist one shared header, but for a different reason than first stated. RAP_DECLS = r""" struct rap_req { unsigned int cmd_id; unsigned int arg1; unsigned int cmd_ack; unsigned int status; unsigned char block10[8]; unsigned char scratch[24]; unsigned int out_ptr; }; """ # POWERPLAY's host request, from powerplay_TA_InvokeCommandEntryPoint. It takes the host # buffer as v4 = *a4, switches on *(_DWORD *)v4 over cases 0..6, and gives every handler the # same pair of interior pointers: v4 + 16 as ctx and v4 + 72 as params. # # The three-dword header is the strongest evidence here, because it is corroborated on # HARDWARE. The entry writes back *(v4+4) = cmd | 0x80000000 and *(v4+8) = status, and the # host-page dump recorded in AUDIT-NEW-TAS-RE.md shows exactly that shape for a command 4: # "dw0-3: 0x4 0x80000004 0x415 0" -- command, command with bit 31 set, status, zero. # # Tested and found NOT general: only POWERPLAY writes the ack at +4. Of twelve TA entries # checked, no other reproduces it, so this header is not a shared TA ABI and is declared # per-component rather than hoisted. # # CAVEAT, as for asd_req: nothing validates a size. 0x84 covers the params block to the # furthest offset observed through handler second arguments (+0x38, dword), so it is a # maximum observed rather than an enforced bound. POWERPLAY_DECLS = r""" struct powerplay_req { unsigned int cmd_id; unsigned int cmd_ack; unsigned int status; unsigned int unk0C; unsigned char ctx[56]; unsigned char params[60]; }; """ # ASD's RPC request. ASD is driver-class, not a TA: there is no TA entry trampoline, and its # dispatcher is reached through the trampoline's +0x1C slot (psp_emu/pspemu/ta_entries.py # resolve_asd, whose stub shape is checked rather than the offset assumed). That resolves to # 0xD013B8 in all 17 Windows builds, matching the figure psp_emu validated independently. # # The dispatcher switches on *(_DWORD *)a1 over the range 0x180..0x1C0 and reads its arguments # at fixed offsets. Offsets and widths observed across all 359 lines of it: # # +0x00 dword rpc id # +0x04 dword, also u8 at +0x04/+0x05 and u16 at +0x06 # +0x08 dword, also u8 at +0x08 and u16 at +0x08/+0x0A # +0x0C dword # +0x10 dword # +0x14 byte # # CAVEAT, stated because it is weaker than the other structs here: nothing validates a size, # so 0x18 is not an enforced bound. It is the smallest 4-aligned size covering every observed # access (max read is a byte at 0x14). Unlike xgmi_req/oc_req/wspe_req, this size is a # convention rather than evidence; no code arrays this structure, so the risk is contained. ASD_DECLS = r""" struct asd_req { unsigned int rpc_id; unsigned int arg1; unsigned int arg2; unsigned int arg3; unsigned int arg4; unsigned char arg5; unsigned char pad15[3]; }; """ # HDCP's host request, from TA_InvokeCommandEntryPoint (0x4013B4 in hdcp2.elf). The entry # takes the host buffer as v7 = *params and switches on *(_DWORD *)v7 over 19 cases, handing # handlers two interior blocks: v7 + 12 and v7 + 5136 (0x1410). # # The size floor is the dispatch guard itself: it proceeds only when params[1] > 0x2817, so # the request is at least 0x2818 bytes. Both blocks then close exactly against that bound -- # 0x0C + 0x1404 = 0x1410 and 0x1410 + 0x1408 = 0x2818. # # NOTE, a recorded comment in psp_out/handover/symbols/hdcp.json is WRONG about this and is # being propagated into the artifacts: it says the entry "validates that params[0].size <= # 0x2817", while the code dispatches when the size EXCEEDS 0x2817. The guard is a floor, not # a ceiling. HDCP_DECLS = r""" struct hdcp_req { unsigned int cmd_id; unsigned int status; unsigned int unk08; unsigned char argA[5124]; unsigned char argB[5128]; }; """ # OC's host request. Derived from oc_TA_InvokeCommandEntryPoint, which is itself the # dispatcher: it rejects a declared size below 0x1C, takes the host buffer as v5 = *a4, reads # the command from *v5, writes the result back to v5[1], and hands handlers interior pointers # -- oc_cmd01 gets v5 + 3 (an int* step, so +0x0C) and oc_cmd02 gets (char *)v5 + 22 (+0x16). # 0x16 + 6 = 0x1C, so the trailing block runs to the end of the validated request. OC_DECLS = r""" struct oc_req { unsigned int cmd_id; unsigned int status; unsigned int unk08; unsigned int arg0C; unsigned char unk10[6]; unsigned char arg16[6]; }; """ # WSPE's host request. Same shape, from wspe_TA_InvokeCommandEntryPoint: declared size must be # at least 0x14, v5 = *a4, command at *v5, result written back to v5[1], and both handlers # (cmd04 request_dopp_active, cmd05 request_active_protected_session) receive v5 + 4, an int* # step giving +0x10. 0x10 + 4 = 0x14, closing the request exactly. WSPE_DECLS = r""" struct wspe_req { unsigned int cmd_id; unsigned int status; unsigned int unk08; unsigned int unk0C; unsigned int arg10; }; """ MERIT_DECLS = r""" struct merit_slot { unsigned char body[257]; }; struct merit_table { struct merit_slot slot[65]; }; """ DECLS = {"gcp": GCP_DECLS, "merit": MERIT_DECLS + MERIT_REQ_DECLS, "xgmi": XGMI_DECLS, "oc": OC_DECLS, "wspe": WSPE_DECLS, "asd": ASD_DECLS + ASD_INTERNAL_DECLS, "hdcp": HDCP_DECLS, "powerplay": POWERPLAY_DECLS, "auc": AUC_DECLS, "fp": FP_DECLS, "rap": RAP_DECLS, "opm": OPM_DECLS, "csu": CSU_DECLS, "dtm": DTM_DECLS} # size and member count for the structs derived later, checked by reading the layout back EXPECT_XGMI = {"xgmi_req": (0xC20, 8)} EXPECT_BY = {"gcp": EXPECT, "merit": {"merit_slot": (257, 1), "merit_table": (0x4141, 1)}} CTX = {"gcp": "struct gcp_ctx *"} def proto_for(name: str, ctx: str) -> str | None: if re.search(r"_cmd[0-9A-Fa-f]{2,4}(_|$)", name): return "int %s(%sctx, struct ta_params *params)" % (name, ctx) if re.search(r"_asd_rpc_[0-9a-fA-F]+$", name): return "int %s(unsigned int arg)" % name if name.endswith("_log"): return "int %s(int level, const char *fmt)" % name if "zero_fill" in name: return "void *%s(void *dst, unsigned int len)" % name if "memcmp" in name: return "int %s(const void *a, const void *b, unsigned int len)" % name if name.endswith("_mac_over_host_data"): return "int %s(%sctx, struct ta_params *params)" % (name, ctx) if name in ("TA_CreateEntryPoint", "TA_DestroyEntryPoint"): return "int %s(void)" % name m = re.match(r"^\w+_(Get|Query|Configure|Generate|Verify)\w+$", name) if m: return "int %s(%sctx, struct ta_params *params)" % (name, ctx) return None CAST_RE = re.compile(r"\*\((?:_[A-Z0-9]+|unsigned \w+|\w+) \*\)\((?:a\d|v\d)") ANON_RE = re.compile(r"\ba[1-9]\d?\b") def score(t: str) -> tuple[int, int]: """(raw pointer-arithmetic casts, anonymous parameters in THIS function's signature). The anon count deliberately looks only at the signature line. Counting `aN` tokens across the whole body counts Hex-Rays' call-site annotations for callees that lack a prototype (`v2(a1: ctx, a2: params)`), which made a strict improvement -- a call that now knows it passes ctx and params -- score as a regression. """ sig = "" for line in t.splitlines(): if line.strip() and "(" in line and not line.strip().startswith(("//", "/*", "*")): sig = line break return len(CAST_RE.findall(t)), len(set(ANON_RE.findall(sig))) def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--prefix", required=True) ap.add_argument("--apply", action="store_true") ap.add_argument("--sample", type=int, default=10) ap.add_argument("--baseline", action="store_true", help="measure 'before' in a pristine database (honest comparison)") args = ap.parse_args() from idatui import remote_ops from idatui.nexus_client import NexusClient binpath = REPO / ELF[args.prefix] c = NexusClient(str(binpath)) c.connect() print("leased worker for %s" % binpath.name) # Read the function list from the DATABASE, not from fw/libdumps/*.json. The dump # goes stale the moment a rename lands, and applying a prototype built from a stale # name is how a rename silently gets undone. # list_funcs returns {"result":[{"data":[{addr,name,size}...],"next_offset","total"}]} # -- paginate until total is covered. raw = [] off = 0 while True: rr = c.call(remote_ops.list_funcs, queries=[{"count": 500, "offset": off}]) blks = rr.get("result", []) if isinstance(rr, dict) else [] if not blks: break blk = blks[0] raw += blk.get("data") or [] total = int(blk.get("total") or 0) off = int(blk.get("next_offset") or 0) if not off or len(raw) >= total: break fns = {} for it in raw: ea = int(str(it.get("addr")), 16) fns[it.get("name") or ("sub_%X" % ea)] = {"ea": ea, "name": it.get("name"), "size": it.get("size", 0)} print("functions in database: %d" % len(fns)) def pseudo_of(client, ea: int) -> str: """Return decompiled CODE only. remote_ops.decompile wraps it in an envelope {ea,name,failed,error,code}; writing the envelope to a .c file makes the artifact useless for review, which is how the first batch of these got saved.""" try: r = client.call(remote_ops.decompile, addr=hex(ea), include_addresses=False) except Exception as exc: # noqa: BLE001 return "" % exc if isinstance(r, dict): if r.get("failed"): return "" % r.get("error") return r.get("code") or r.get("text") or "" return str(r) def pseudo(ea: int) -> str: return pseudo_of(c, ea) sample = [n for n in fns if re.search(r"_cmd[0-9A-Fa-f]{2,4}", n)][: args.sample] # The honest baseline is a PRISTINE database: measuring "before" in the same database # that already carries names and types compares typed output against typed output and # reports a meaningless delta. Analyse a copy of the ELF from scratch instead. before = {} if args.baseline: import shutil scratch = REPO / "psp_out" / "scratch" scratch.mkdir(parents=True, exist_ok=True) copy = scratch / ("baseline_" + binpath.name) if not copy.exists(): shutil.copy2(binpath, copy) b = NexusClient(str(copy), new_database=True) b.connect() print("pristine baseline database: %s" % copy.name) for n in sample: before[n] = pseudo_of(b, fns[n]["ea"]) else: before = {n: pseudo(fns[n]["ea"]) for n in sample} if args.apply and args.prefix in DECLS: # ONE parse unit, dependency-ordered: declaring these one at a time is what made # the TUI route reject every nested member. r = c.call(remote_ops.declare_type, decls=DECLS[args.prefix]) print("declare_type -> %s" % json.dumps(r)[:300]) # VERIFY the layout by reading members back. A declaration that "succeeds" but yields # a 4-byte struct is exactly the failure mode that went unnoticed before. print("\ntype verification (size/members read back from IDA):") allgood = True EXPECT = EXPECT_BY.get(args.prefix, {}) if EXPECT: q = c.call(remote_ops.type_inspect, queries=[{"name": n} for n in EXPECT]) rows = q.get("result", q) if isinstance(q, dict) else q for row in rows if isinstance(rows, list) else []: nm = row.get("name") if nm not in EXPECT: continue want_sz, want_n = EXPECT[nm] got_sz = row.get("size") mem = row.get("members") or [] ok = (got_sz == want_sz and len(mem) == want_n) allgood &= ok print(" %-20s size %-8s members %-3s %s" % ( nm, hex(got_sz) if isinstance(got_sz, int) else got_sz, len(mem), "OK" if ok else "MISMATCH (want %#x / %d)" % (want_sz, want_n))) if not ok and row.get("error"): print(" error: %s" % row["error"]) print(" ALL TYPES CORRECT: %s" % allgood) else: print(" (no struct layout established for this TA to the required standard)") if args.apply: ctx = CTX.get(args.prefix, "void *") ok = fail = 0 errs = [] for name, f in sorted(fns.items(), key=lambda kv: kv[1]["ea"]): p = proto_for(name, ctx) if not p: continue r = c.call(remote_ops.set_type, edits=[{"addr": hex(f["ea"]), "signature": p + ";"}]) rows = r.get("result", []) if isinstance(r, dict) else r good = bool(rows and rows[0].get("ok")) if good: ok += 1 else: fail += 1 errs.append((name, rows[0].get("error") if rows else "?")) print("\nprototypes: %d applied, %d failed" % (ok, fail)) for n, e in errs[:8]: print(" FAILED %-44s %s" % (n, e)) tb = ta = ab = aa = 0 absent = [] outdir = REPO / "fw" / "pseudo" outdir.mkdir(exist_ok=True) print("\nreadability, before -> after:") for n in sample: after = pseudo(fns[n]["ea"]) # A function IDA never found in the pristine database has no baseline, so # scoring it 0 makes the totals look like a regression (powerplay showed # "anon args 0 -> 8" purely from this). Count those separately: the fact that # the function only exists because we defined it is a WIN, not a regression. if before[n].startswith(" %-2d anon args %d -> %d" % (n, cb, ca, nb, na)) (outdir / ("%s.%s.before.c" % (args.prefix, n))).write_text(before[n]) (outdir / ("%s.%s.after.c" % (args.prefix, n))).write_text(after) print(" TOTAL casts %d -> %d anon args %d -> %d (over %d comparable)" % (tb, ta, ab, aa, len(sample) - len(absent))) if absent: print(" %d function(s) absent from a pristine database (we defined them): %s" % (len(absent), ", ".join(absent[:6]))) # save_database is a CLIENT method, not a remote_ops op -- calling it through # c.call() raises and, if that exception is swallowed, the work sits only in the # unpacked .id0/.id1 scratch files and is lost if the worker dies uncleanly. try: print("save_database -> %s" % json.dumps(c.save_database())[:160]) except Exception as exc: # noqa: BLE001 print("SAVE FAILED: %s" % exc) try: c.close() except Exception: # noqa: BLE001 pass return 0 if __name__ == "__main__": raise SystemExit(main())