aboutsummaryrefslogtreecommitdiffstats
path: root/fw/ta_ida.py
blob: a1b49a58317f6a3653af69b48d5d866f3c088f8c (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
#!/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 "_<hash8>_" 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 "<decompile failed: %s>" % exc
        if isinstance(r, dict):
            if r.get("failed"):
                return "<decompile failed: %s>" % 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("<decompile failed"):
                absent.append(n)
                continue
            cb, nb = score(before[n])
            ca, na = score(after)
            tb += cb; ta += ca; ab += nb; aa += na
            print("   %-46s casts %2d -> %-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())