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*_wa.bin) \
$(wildcard /lib/firmware/mediatek/mt79*_rom_patch.bin) $(wildcard /lib/firmware/mediatek/mt79*_rom_patch.bin)
.PHONY: help extract test .PHONY: help extract test elf
help: help:
@echo "make extract - carve all MediaTek Connac2 blobs found on this host" @echo "make extract - carve all MediaTek Connac2 blobs found on this host"
@ -13,3 +13,6 @@ extract:
test: test:
python3 -m unittest discover -s tests -v 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 here fails against a newer snapshot, the blob changed — update the golden
value *and* note it in the changelog, don't silently absorb it. value *and* note it in the changelog, don't silently absorb it.
""" """
import os
import subprocess import subprocess
import sys import sys
import unittest import unittest
from pathlib import Path from pathlib import Path
REPO = Path(__file__).resolve().parent.parent 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) # filename -> (chip_id, eco, n_region, hidden_trailer_string_or_None)
GOLDEN_WM = { 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()