9.1 KiB
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.
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).
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 ROMfunc_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_0430etc.). FUN_009002a6: bus-register writer — LE dword writes to offsets 0x110/0x114/0x118/0x11c on bus 5, RMW| 0x400000c0, busy-poll bit0x40000000.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.
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:
cmdEventParserCmdwithucCIDprinting;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).
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_CMDdispatch, 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.
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).
- Code model differs from patch/WA: only 27 GP-relative references in the whole image → WM uses (near-)absolute global addressing, so GP-base inference is not the unlock it would otherwise be.
- Structural dispatch scan (scan_tables.py) across every region pairing found no absolute function-pointer table (unlike WA's 65-entry table, F7). WM command dispatch must use another mechanism — FP/register-relative tables, switch jump tables, or runtime handler registration (unknown U4).
- Regions: code 0xe003b000 (398KB) + 0xe009c400 (473KB mixed); data 0x0231dc00 (205KB), 0x0041xxxx pair; 0xf0xxxxxx block (feat 0x80) still uncharacterized (U1).
Unknowns registry
- U1 —
feature_setbit 7 (0x80): observed only on WM regions at0xf0xxxxxxaddresses (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 00and flag bytes before/around the string (F2) are uninterpreted. - U3 — WO container:
mt7981_wo.binuses the mtk_wed loader (mtk_wed_mcu.c), a different format; not yet parsed.
Related verified facts
- NDS32LE target:
GCC: (2018-10-23_nds32le-elf-mculib-v3) 4.9.4string in mt7981_wa region data; FreeRTOS v10.0.0 +portable/GCC/NDS32/port.cstrings in mt7981_wo. - No per-region encryption in any observed blob (
FW_FEATURE_SET_ENCRYPTclear; patchenc_type == 0). Optional protocol support exists in the loader (key from EFUSE path) but is not exercised by shipped images.