ELF emitter: EM_NDS32 ET_EXEC, PT_LOAD per region at true addresses; readelf-validated; tests (structure + segment fidelity); MTK_FW_DIR env override

This commit is contained in:
Zachery Aaron Shores-Chmielewski 2026-08-20 21:13:54 +04:00
parent 8c4a55e60a
commit c167f893b5
4 changed files with 182 additions and 2 deletions

View file

@ -2,7 +2,7 @@ BLOBS ?= $(wildcard /lib/firmware/mediatek/mt79*_wm.bin) \
$(wildcard /lib/firmware/mediatek/mt79*_wa.bin) \
$(wildcard /lib/firmware/mediatek/mt79*_rom_patch.bin)
.PHONY: help extract test
.PHONY: help extract test elf
help:
@echo "make extract - carve all MediaTek Connac2 blobs found on this host"
@ -13,3 +13,6 @@ extract:
test:
python3 -m unittest discover -s tests -v
elf:
python3 tools/mtk_fw_elf.py $(BLOBS) -o extracted

78
tests/test_elf.py Normal file
View file

@ -0,0 +1,78 @@
"""ELF emitter tests: structural validity + segment fidelity.
Validates with readelf where available and always with a programmatic
re-parse against the container's own region table.
"""
import os
import struct
import subprocess
import sys
import unittest
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO / 'tools'))
import mtk_fw_elf as E # noqa: E402
FW = Path(os.environ.get('MTK_FW_DIR', '/lib/firmware/mediatek'))
def blobs():
return sorted(FW.glob('mt79*_wm.bin')) + sorted(FW.glob('mt79*_wa.bin')) \
+ sorted(FW.glob('mt79*_rom_patch.bin'))
class Emitter(unittest.TestCase):
def test_structure_and_fidelity(self):
found = blobs()
if not found:
self.skipTest('no mediatek blobs')
for p in found:
with self.subTest(blob=p.name):
d = p.read_bytes()
if 'patch' in p.name:
regions = list(E.patch_regions(d))
else:
regions = list(E.ram_regions(d))
regions = [(a, dat) for a, dat in regions if dat]
elf = E.build_elf(regions)
# ehdr sanity
self.assertEqual(elf[:4], b'\x7fELF')
etype, mach, _, entry, phoff = struct.unpack_from('<HHIII', elf, 16)
self.assertEqual((etype, mach), (2, 167))
self.assertEqual(phoff, 52)
phentsize, phnum = struct.unpack_from('<HH', elf, 42)
self.assertEqual((phentsize, phnum), (32, len(regions)))
# segment fidelity
for i, (addr, dat) in enumerate(regions):
p_type, p_off, p_va, p_pa, p_filesz, p_memsz = \
struct.unpack_from('<IIIIII', elf, phoff + i * 32)
self.assertEqual((p_type, p_va, p_pa), (1, addr, addr))
self.assertEqual((p_filesz, p_memsz), (len(dat),) * 2)
self.assertEqual(elf[p_off:p_off + p_filesz], dat)
def test_readelf_accepts(self):
found = blobs()
if not found:
self.skipTest('no mediatek blobs')
if subprocess.run(['which', 'readelf'], capture_output=True).returncode:
self.skipTest('readelf unavailable')
import tempfile
for p in found[:4]:
with self.subTest(blob=p.name):
d = p.read_bytes()
regs = list(E.patch_regions(d) if 'patch' in p.name
else E.ram_regions(d))
elf = E.build_elf(regs)
with tempfile.NamedTemporaryFile(suffix='.elf') as f:
f.write(elf)
f.flush()
r = subprocess.run(['readelf', '-l', f.name],
capture_output=True, text=True)
self.assertEqual(r.returncode, 0, r.stderr)
self.assertIn('LOAD', r.stdout)
if __name__ == '__main__':
unittest.main()

View file

@ -5,13 +5,14 @@ Golden values observed from linux-firmware snapshot 2026-04-20; when a value
here fails against a newer snapshot, the blob changed — update the golden
value *and* note it in the changelog, don't silently absorb it.
"""
import os
import subprocess
import sys
import unittest
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
FW = Path('/lib/firmware/mediatek')
FW = Path(os.environ.get('MTK_FW_DIR', '/lib/firmware/mediatek'))
# filename -> (chip_id, eco, n_region, hidden_trailer_string_or_None)
GOLDEN_WM = {

98
tools/mtk_fw_elf.py Executable file
View file

@ -0,0 +1,98 @@
#!/usr/bin/env python3
"""Emit a spec-correct 32-bit LE ELF (e_machine=EM_NDS32) from a MediaTek
Connac2 WiFi firmware container, one PT_LOAD segment per region at true
load addresses.
Structure is validated by readelf; disassembly requires Ghidra (official
NDS32 module) or an Andes nds32le-elf toolchain — Debian binutils dropped
NDS32. Entry point is unknown pre-RE (e_entry=0); set it in your tool after
import.
NON_DL regions are included as loadable segments by default; they are
host-side data per the driver, not downloaded to the MCU.
"""
import struct
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from mtk_fw_extract import PATCH_HDR, PATCH_SEC, REGION, TRAILER # noqa: E402
EM_NDS32 = 167
EHDR = struct.Struct('<16sHHIIIIIHHHHHH') # 52 bytes
PHDR = struct.Struct('<IIIIIIII') # 32 bytes
PT_LOAD, PF_RWX = 1, 7
def ram_regions(d: bytes):
"""Yield (addr, data) for each region of a RAM container."""
t = TRAILER.unpack_from(d, len(d) - TRAILER.size)
n = t[2]
table = len(d) - TRAILER.size - n * REGION.size
off = 0
for i in range(n):
r = REGION.unpack_from(d, table + i * REGION.size)
addr, ln = r[4], r[5]
data = d[off:off + ln]
off += ln
yield addr, data
def patch_regions(d: bytes):
"""Yield (addr, data) for each section of a patch container."""
h = PATCH_HDR.unpack_from(d, 0)
n = h[9]
for i in range(n):
s = PATCH_SEC.unpack_from(d, PATCH_HDR.size + i * PATCH_SEC.size)
typ, offs, size = s[0], s[1], s[2]
addr = s[3]
yield addr, d[offs:offs + size]
def build_elf(regions) -> bytes:
regions = [(a, dat) for a, dat in regions if dat]
n = len(regions)
phoff = EHDR.size
data_off = phoff + n * PHDR.size
eh = EHDR.pack(
b'\x7fELF' + bytes([1, 1, 1, 0]) + b'\x00' * 8, # ELFCLASS32/LSB/v1
2, EM_NDS32, 1,
regions[0][0] if regions else 0, # e_entry (first region; see doc)
phoff, 0, 0,
EHDR.size, PHDR.size, n,
40, 0, 0)
out = bytearray(eh)
offset = data_off
for addr, dat in regions:
out += PHDR.pack(PT_LOAD, offset, addr, addr, len(dat), len(dat),
PF_RWX, 0x1000)
offset += len(dat)
for _, dat in regions:
out += dat
return bytes(out)
def main():
if len(sys.argv) < 2:
sys.exit(f'usage: {sys.argv[0]} <blob> [<blob>...] [-o outdir]')
args, outdir = [], Path('extracted')
it = iter(sys.argv[1:])
for a in it:
if a == '-o':
outdir = Path(next(it))
else:
args.append(a)
for path in args:
p = Path(path)
d = p.read_bytes()
regions = patch_regions(d) if 'patch' in p.name else ram_regions(d)
elf = build_elf(regions)
out = outdir / p.stem / f'{p.stem}.elf'
out.parent.mkdir(parents=True, exist_ok=True)
out.write_bytes(elf)
n_seg = struct.unpack_from('<H', elf, 44)[0] # e_phnum
print(f'{p.name} -> {out} ({len(elf)} bytes, {n_seg} PT_LOAD)')
if __name__ == '__main__':
main()