2026-08-20 17:09:43 +00:00
|
|
|
|
# Findings log
|
|
|
|
|
|
|
|
|
|
|
|
One entry per claim. Every claim carries evidence (kernel source reference or
|
|
|
|
|
|
blob offset + observed bytes). Claims that cannot be evidenced are registered
|
|
|
|
|
|
in the Unknowns section instead. Blobs referenced by filename are the
|
|
|
|
|
|
linux-firmware copies listed in `manifest.json` (SHA-256 TBD in fixtures).
|
|
|
|
|
|
|
|
|
|
|
|
Blobs observed: mt7981/mt7915/mt7916/mt7986 × {wm, wa, rom_patch}
|
|
|
|
|
|
(linux-firmware snapshot 2026-04-20, Debian).
|
|
|
|
|
|
|
|
|
|
|
|
## F1 — Connac2 RAM container layout
|
|
|
|
|
|
|
|
|
|
|
|
`[region data, packed from offset 0 in table order][optional hidden trailer
|
|
|
|
|
|
(see F2)][n_region × 40-byte region table][36-byte trailer at EOF]`
|
|
|
|
|
|
|
|
|
|
|
|
Evidence: `mt76_connac_mcu.c` `mt76_connac2_load_ram()` +
|
|
|
|
|
|
`mt76_connac2_send_ram()` (region data streamed sequentially from offset 0;
|
|
|
|
|
|
table and trailer located from EOF); `mt76_connac_mcu.h`
|
|
|
|
|
|
`struct mt76_connac2_fw_region` / `_fw_trailer`. Confirmed by exact layout
|
|
|
|
|
|
match on 8 RAM blobs (sum(region.len) + table + trailer (+ hidden trailer) ==
|
|
|
|
|
|
file size).
|
|
|
|
|
|
|
|
|
|
|
|
Region fields: `decomp_crc/decomp_len/decomp_blk_sz` (MCU-side decompression,
|
|
|
|
|
|
unused when decomp_len==0 — all observed blobs), `addr`, `len`,
|
|
|
|
|
|
`feature_set`, `type`.
|
|
|
|
|
|
|
|
|
|
|
|
## F2 — Hidden second trailer carries the full build string
|
|
|
|
|
|
|
|
|
|
|
|
Between region data and the kernel-parsed table sits a trailer the upstream
|
|
|
|
|
|
loader never reads. WM blobs: 100 bytes; WA blobs: 36 bytes (no string).
|
|
|
|
|
|
Observed family-wide (all 4 chips).
|
|
|
|
|
|
|
|
|
|
|
|
WM hidden trailer = 16×'#' + `40 00 00 00` + 1-byte string length + flags +
|
|
|
|
|
|
full build string + '#'-padding. Recovered strings:
|
|
|
|
|
|
|
|
|
|
|
|
- mt7981_wm: `t-neptune-main-mt7915-1953-MT7981_MP2111_IMP-20240823161204`
|
|
|
|
|
|
- mt7916_wm: `t-neptune-main-mt7915-1953-MT7916_MP2111_IMP-20240823170147`
|
|
|
|
|
|
- mt7986_wm: `t-neptune-main-mt7915-1953-MT7986_MP2111_IMP-20240823160608`
|
|
|
|
|
|
- mt7915_wm: `t-neptune-mp-mt7915-2045-MT7915_MP_7_4_2045-20220929103802`
|
|
|
|
|
|
|
|
|
|
|
|
Significance: the kernel-visible trailer's `fw_ver[10]` is stripped
|
|
|
|
|
|
(`____000000` WM / `DEV_000000` WA); the hidden trailer restores branch
|
|
|
|
|
|
("main"/"mp"), internal build number (1953/2045), MP tag, and build
|
|
|
|
|
|
timestamp. This is the version metadata the changelog tooling keys on.
|
|
|
|
|
|
|
|
|
|
|
|
Evidence: bytes at offset `sum(region.len)` in each blob (see
|
|
|
|
|
|
`manifest.json` → `hidden_trailer.raw_head`).
|
|
|
|
|
|
|
|
|
|
|
|
## F3 — region `type` is a content tag used by some loaders
|
|
|
|
|
|
|
|
|
|
|
|
All parsed mt79-family blobs: `type == 0` (`FW_TYPE_DEFAULT`). It is not
|
|
|
|
|
|
ignored upstream: mt7921's loader selects
|
|
|
|
|
|
`feature_set & FW_FEATURE_NON_DL && type == FW_TYPE_CLC` regions as
|
|
|
|
|
|
country/location-calibration data instead of downloading them to the MCU.
|
|
|
|
|
|
|
|
|
|
|
|
Evidence: `mt7921/mcu.c` CLC handling; `FW_TYPE_CLC` definition in
|
|
|
|
|
|
`mt76_connac_mcu.h`.
|
|
|
|
|
|
|
|
|
|
|
|
## F4 — Patch container format
|
|
|
|
|
|
|
|
|
|
|
|
92-byte big-endian header (16-byte build date, 4-byte platform tag —
|
|
|
|
|
|
"ALPS" on all four — hw_sw_ver, patch_ver, checksum, descriptor), 64-byte BE
|
|
|
|
|
|
section table, plaintext section data at explicit offsets. All observed
|
|
|
|
|
|
patch sections: `enc_type == 0` (plaintext).
|
|
|
|
|
|
|
|
|
|
|
|
Evidence: `mt76_connac2_load_patch()` + `struct mt76_connac2_patch_hdr/_sec`
|
|
|
|
|
|
in `mt76_connac_mcu.h`; exact parse of 4 patch blobs.
|
|
|
|
|
|
|
2026-08-20 17:11:06 +00:00
|
|
|
|
Section counts: mt7915 patch = 2 sections, all others = 1; section `type`
|
|
|
|
|
|
constant `0x30002` on every observed section (loader downloads all sections
|
|
|
|
|
|
regardless; field semantics otherwise unobserved).
|
|
|
|
|
|
|
2026-08-20 17:51:36 +00:00
|
|
|
|
## F5 — mt7981_rom_patch internal structure (first disassembly, 2026-08-20)
|
|
|
|
|
|
|
|
|
|
|
|
The 9.7KB patch section (downloaded to 0x00900000) is a boot-ROM function
|
|
|
|
|
|
replacement table + code + strings, little-endian NDS32:
|
|
|
|
|
|
|
|
|
|
|
|
| offset | content |
|
|
|
|
|
|
|---|---|
|
|
|
|
|
|
| 0x000 | `0x000003ff`, 0 |
|
|
|
|
|
|
| 0x008 | 10 LE pointers into boot ROM (`0x00801xxx–0x0082bxxx`) |
|
|
|
|
|
|
| 0x108 | 10 LE pointers into patch RAM (`0x009002xx–0x009012xx`) — replacements |
|
|
|
|
|
|
| ~0x1e0 | NDS32 code (Ghidra: 49 functions + entry marker) |
|
|
|
|
|
|
| 0x901000+ | string table |
|
|
|
|
|
|
|
|
|
|
|
|
Decompilation is viable end-to-end (Ghidra 12 `NDS32:LE:32`, official
|
|
|
|
|
|
module; our ELF imports directly — deferred acceptance check now PASSED).
|
|
|
|
|
|
Evidence: `ghidra-proj/patch_decomp.txt` (unpublished, blob-derived).
|
|
|
|
|
|
|
|
|
|
|
|
Verified observations:
|
|
|
|
|
|
- Assert anchors: `patch/wf/wm/sys_patch_mcu.c` (lines 0x426, 0x5de...),
|
|
|
|
|
|
`common/sys_patch_common_mcu.c` (0x20f, 0x248...) via ROM `func_0x008004b8`.
|
|
|
|
|
|
- 20+ distinct direct calls into boot ROM (`func_0x0080xxxx`) + GP-relative
|
|
|
|
|
|
indirect calls (`unaff_gp - 0x116xx`) — ROM provides the runtime library.
|
|
|
|
|
|
- Named-entry strings: `MCU_Patch_init`, `ENTRY_wsysMboxSendMsg`,
|
|
|
|
|
|
`ENTRY_wsysMboxRcvAllMsg` (mailbox IPC), `From_CCIF__host_cpu_sw_interrupt`
|
|
|
|
|
|
(CCIF = host↔MCU channel), `WDT_to_Host` / `WDT_to_N9` (confirms the MCU
|
|
|
|
|
|
core is called N9), `Patch_dic_handler_extend`, `WF_Lt_Sec_handler`.
|
|
|
|
|
|
- Diagnostics suite strings: `AXI_Bus_monitor_detect`, `APB_AHB_bus_timeout`,
|
|
|
|
|
|
`IDLM_monitor`, `CPU_UTLZ_CNT_*` (utilization counters),
|
|
|
|
|
|
`cache_miss_ratio`, register-dump prints (`0x8800_0430` etc.).
|
|
|
|
|
|
- `FUN_009002a6`: bus-register writer — LE dword writes to offsets
|
|
|
|
|
|
0x110/0x114/0x118/0x11c on bus 5, RMW `| 0x400000c0`, busy-poll bit
|
|
|
|
|
|
`0x40000000`. `FUN_0090044e`: bounded delay loop via ROM timer reads
|
|
|
|
|
|
(`func_0x00801e20`/`func_0x00801e16`).
|
|
|
|
|
|
|
|
|
|
|
|
Interpretation (labelled): the patch extends boot ROM with host-comm hooks
|
|
|
|
|
|
(CCIF interrupt, mailboxes, watchdog-to-host) and bus diagnostics, plus
|
|
|
|
|
|
download-plumbing register programming — the glue the ROM needs before
|
|
|
|
|
|
WM/WA firmware arrives.
|
|
|
|
|
|
|
2026-08-20 17:53:39 +00:00
|
|
|
|
## F6 — mt7981_wa survey (2026-08-20)
|
|
|
|
|
|
|
|
|
|
|
|
Ghidra project `wa` imported + analyzed: 120 functions, 450 strings.
|
|
|
|
|
|
Strings self-identify WA's role — host command handling and TX bookkeeping
|
|
|
|
|
|
(`MCU_EXT_CMD` protocol), matching the driver's separate WA MCU queue:
|
|
|
|
|
|
|
|
|
|
|
|
- `cmdEventParserCmd` with `ucCID` printing; `EXT_CMD_ID_STAREC_UPDATE`,
|
|
|
|
|
|
`DevInfo Update Command`, `BssInfo Update Command` (own-MAC/BSS record
|
|
|
|
|
|
management), `staRec with invalid wandidx` (station records)
|
|
|
|
|
|
- Per-STA loss accounting: `PKTLOSS[%d]times/cnt[tot_tx,drop_tx,seq]`,
|
|
|
|
|
|
`lost seq`, `dup seq`, `dlycnt/maxdly`, plus `[proto,port,src_ip,dest_ip]`
|
|
|
|
|
|
flow dump formatting
|
|
|
|
|
|
|
|
|
|
|
|
Next: match `MCU_EXT_CMD_*` enum ids (mt76 headers) to `cmdEventParserCmd`
|
|
|
|
|
|
dispatch — the ABI anchor map (P1-C/D).
|
|
|
|
|
|
|
2026-08-21 14:46:42 +00:00
|
|
|
|
## F7 — WA EXT_CMD dispatch table located (2026-08-20)
|
|
|
|
|
|
|
|
|
|
|
|
Method: string xrefs fail on this code (GP-relative data addressing;
|
|
|
|
|
|
decompiler shows `unaff_gp + imm`) — dispatch found structurally instead:
|
|
|
|
|
|
`tools/scan_tables.py` scans for consecutive LE pointers into ILM
|
|
|
|
|
|
(0x10200000–0x10233370).
|
|
|
|
|
|
|
|
|
|
|
|
- **65-entry table @ ILM+0x1304 (vaddr 0x10201304)** = `MCU_EXT_CMD`
|
|
|
|
|
|
dispatch, indexed by command id. Two default stubs: 0x10200278
|
|
|
|
|
|
(unhandled) / 0x10200270 (reserved). Verified alignments with mt76 enum:
|
|
|
|
|
|
[0x07] PM_STATE_CTRL→0x10201934, [0x08] CHANNEL_SWITCH→0x10201910,
|
|
|
|
|
|
[0x11] SET_TX_POWER_CTRL→0x1020164c, [0x25] STA_REC_UPDATE→0x10201530,
|
|
|
|
|
|
[0x26] BSS_INFO_UPDATE→0x1020155c, [0x2a] DEV_INFO_UPDATE→0x102015c4,
|
|
|
|
|
|
[0x32] WTBL_UPDATE→0x10201688, [0x49] BCN_OFFLOAD→0x10201674.
|
|
|
|
|
|
- ~30 non-default handlers → immediately nameable; ~15 non-default entries
|
|
|
|
|
|
at ids beyond mt76's public enum (MTK-internal commands — discovery list).
|
|
|
|
|
|
- Additional tables: 36-entry @ILM+0x330 (sparse; likely basic
|
|
|
|
|
|
MCU_CMD/mailbox dispatch), 16-entry @ILM+0x8248.
|
|
|
|
|
|
- Coverage prerequisite: forced linear disassembly (`ForceDisasmPost.py`)
|
|
|
|
|
|
28,540 → 125,621 instructions — default analysis leaves most firmware
|
|
|
|
|
|
code undisassembled (no entry graph reaches it).
|
|
|
|
|
|
|
|
|
|
|
|
Next: same scan on WM; handler-by-handler decompile + naming pass.
|
|
|
|
|
|
|
2026-08-21 14:53:30 +00:00
|
|
|
|
## F8 — mt7981_wm survey (2026-08-20)
|
|
|
|
|
|
|
|
|
|
|
|
Ghidra project `wm`: forced disassembly to **555,385 instructions / 4,422
|
|
|
|
|
|
functions** (import-time analysis alone: 226K/4,355).
|
|
|
|
|
|
|
2026-08-21 14:58:57 +00:00
|
|
|
|
- ~~Code model claim: "only 27 GP-relative refs"~~ CORRECTED in F9: WM is
|
|
|
|
|
|
GP-relative; the operand-object scan undercounted (missed memory-operand
|
|
|
|
|
|
forms). GP value still unsolved, but the dispatcher's GP-relative
|
|
|
|
|
|
constants identified the registration array structurally.
|
|
|
|
|
|
- Structural scan found no *absolute-pointer* dispatch table — resolved in
|
|
|
|
|
|
F9: dispatch is a runtime registration list, and the static registration
|
|
|
|
|
|
arrays use GP-reachable data in region r5.
|
2026-08-21 14:53:30 +00:00
|
|
|
|
- Regions: code 0xe003b000 (398KB) + 0xe009c400 (473KB mixed);
|
|
|
|
|
|
data 0x0231dc00 (205KB), 0x0041xxxx pair; 0xf0xxxxxx block (feat 0x80)
|
|
|
|
|
|
still uncharacterized (U1).
|
2026-08-21 14:53:56 +00:00
|
|
|
|
|
2026-08-21 14:58:57 +00:00
|
|
|
|
## F9 — WM dispatch solved: runtime registration list (2026-08-20, U4 resolved)
|
|
|
|
|
|
|
|
|
|
|
|
WM's command parser found via string xref (`EXT_CMD_ID:0x%02x, SEQ:%u...`
|
|
|
|
|
|
@0xe00a64b0 → **FUN_0xe00412e6**): walks a handler registration array
|
|
|
|
|
|
`{u8 cmd_id; code* handler}` (stride 8) at `gp+0x1b6a0`, count at
|
|
|
|
|
|
`gp+0x227bc`; match → `handler(msg)`; miss → `Not_handled_ucExtenCID`.
|
|
|
|
|
|
A top-level class array (r5 @0x02238f74) maps class 0xed → this parser.
|
|
|
|
|
|
|
|
|
|
|
|
Region r5 (0x02229800) holds the *static* registration arrays:
|
|
|
|
|
|
**15 arrays / 407 `{id,fn}` entries** (`tools/scan_registrations.py` →
|
|
|
|
|
|
`dataset/wm_registrations.txt`). Enum join names **130 handlers** from
|
|
|
|
|
|
mt76's ABI (`dataset/wm_handlers_named.txt`) — e.g. 0x08
|
|
|
|
|
|
CHANNEL_SWITCH→0xe003ff5c, 0x25 STA_REC_UPDATE, 0x32 WTBL→0xe00402fa,
|
|
|
|
|
|
0x81 SER_TRIGGER→0xe005d480, 0x94 TWT→0xe0040de4, 0x9f
|
|
|
|
|
|
MURU_CTRL→0xe0045f3c; 277 entries carry MTK-internal ids beyond the public
|
|
|
|
|
|
enum (discovery list).
|
|
|
|
|
|
|
|
|
|
|
|
Labels applied (`LabelHandlers.py`): WM **371 functions** (115 created +
|
|
|
|
|
|
256 renamed), WA **29** (from F7's table). r5 additionally carries 192
|
|
|
|
|
|
function-name strings (`wsysWfdmaCmdHandle`, `muruExtCmdSetMuruStatistic`,
|
|
|
|
|
|
...) — `__FUNCTION__`-style assert anchors for the next naming pass.
|
|
|
|
|
|
|
2026-08-20 17:09:43 +00:00
|
|
|
|
## Unknowns registry
|
|
|
|
|
|
|
|
|
|
|
|
- **U1 — `feature_set` bit 7 (0x80):** observed only on WM regions at
|
|
|
|
|
|
`0xf0xxxxxx` addresses (mt7981 r7–r10, mt7986 equivalents). Upstream
|
|
|
|
|
|
defines bits 0–6 only; vendor-SDK search found no definition (2026-08-20).
|
|
|
|
|
|
Correlation with address family noted; semantics undetermined. May relate
|
|
|
|
|
|
to a second download destination or content class.
|
|
|
|
|
|
- **U2 — hidden-trailer non-string fields:** the `40 00 00 00` and flag
|
|
|
|
|
|
bytes before/around the string (F2) are uninterpreted.
|
|
|
|
|
|
- **U3 — WO container:** `mt7981_wo.bin` uses the mtk_wed loader
|
|
|
|
|
|
(`mtk_wed_mcu.c`), a different format; not yet parsed.
|
2026-08-21 14:58:57 +00:00
|
|
|
|
- **U4 — WM command dispatch mechanism: RESOLVED (F9)** — runtime
|
|
|
|
|
|
registration list at gp+0x1b6a0; static arrays in region r5; parser
|
|
|
|
|
|
0xe00412e6.
|
2026-08-20 17:09:43 +00:00
|
|
|
|
|
|
|
|
|
|
## Related verified facts
|
|
|
|
|
|
|
|
|
|
|
|
- NDS32LE target: `GCC: (2018-10-23_nds32le-elf-mculib-v3) 4.9.4` string in
|
|
|
|
|
|
mt7981_wa region data; FreeRTOS v10.0.0 + `portable/GCC/NDS32/port.c`
|
|
|
|
|
|
strings in mt7981_wo.
|
|
|
|
|
|
- No per-region encryption in any observed blob (`FW_FEATURE_SET_ENCRYPT`
|
|
|
|
|
|
clear; patch `enc_type == 0`). Optional protocol support exists in the
|
|
|
|
|
|
loader (key from EFUSE path) but is not exercised by shipped images.
|