|
| 1 | +#!/usr/bin/env python3 |
| 2 | +"""#677 — bulk-memory operand-register clobber differential (thumb-2). |
| 3 | +
|
| 4 | +The #374 memory.copy/memory.fill lowering mutated its popped operand registers |
| 5 | +in place (dst/src as walking loop pointers, len as the byte buffer). A popped |
| 6 | +register is NOT always a dead temp: `local.get` of a register-homed local |
| 7 | +(AAPCS param r0-r3, promoted local r4-r8) pushes the HOME register itself, so |
| 8 | +a local reused AFTER the op read a wild `mem_base + <loop cursor>` pointer (for |
| 9 | +a dest/src) or the last byte copied (for a len). Const operands were unaffected. |
| 10 | +The fix copies any still-live popped operand into a scratch register before the |
| 11 | +loop mutates it (#193 reservation discipline, `bulk_mutable_operand`). |
| 12 | +
|
| 13 | +**wasmtime is ground truth**; **unicorn** runs synth's Thumb-2 output. All |
| 14 | +vectors are in-bounds — this gate is about the RETURN VALUE (the reused local) |
| 15 | +and the memory image, not traps. Symbols are read from the ELF symtab |
| 16 | +(pyelftools), not `synth disasm` text (host-dependent — see PR #489). |
| 17 | +
|
| 18 | +Run: |
| 19 | + SYNTH=./target/release/synth python scripts/repro/bulk_local_clobber_677_differential.py |
| 20 | +Exits nonzero on any mismatch. RED on pre-fix main, GREEN post-#677. |
| 21 | +""" |
| 22 | + |
| 23 | +import os |
| 24 | +import subprocess |
| 25 | +import sys |
| 26 | +import tempfile |
| 27 | +from pathlib import Path |
| 28 | + |
| 29 | +import wasmtime |
| 30 | +from elftools.elf.elffile import ELFFile |
| 31 | +from unicorn import UC_ARCH_ARM, UC_ERR_INSN_INVALID, UC_MODE_THUMB, Uc, UcError |
| 32 | +from unicorn.arm_const import ( |
| 33 | + UC_ARM_REG_LR, |
| 34 | + UC_ARM_REG_R0, |
| 35 | + UC_ARM_REG_R1, |
| 36 | + UC_ARM_REG_R10, |
| 37 | + UC_ARM_REG_R11, |
| 38 | + UC_ARM_REG_SP, |
| 39 | +) |
| 40 | + |
| 41 | +WAT = Path(__file__).with_name("bulk_local_clobber_677.wat") |
| 42 | +SYNTH = os.environ.get("SYNTH", "./target/release/synth") |
| 43 | + |
| 44 | +MEM_BYTES = 0x10000 # 1 page = 64 KiB (matches the wat `(memory 1)`) |
| 45 | +CODE, LIN, STK, RET = 0x200000, 0x400000, 0x90000, 0x300000 |
| 46 | + |
| 47 | +# (fn, arg0, arg1) — arg0 = the local under test (dst/src), arg1 = len. |
| 48 | +VECTORS = [ |
| 49 | + ("cpy_dst", 32, 4), # dst local read back: expect 67305985 |
| 50 | + ("cpy_dst", 4096, 8), # different dst, len |
| 51 | + ("fil_dst", 32, 4), # fill dst local read back: expect 66 |
| 52 | + ("fil_dst", 100, 1), |
| 53 | + ("cpy_len", 0, 4), # len local reused: expect 4 |
| 54 | + ("cpy_len", 0, 0), # len 0: no-op copy, local must still read 0 |
| 55 | + ("cpy_src", 16, 4), # src local reused: expect 16 |
| 56 | + ("cpy_const", 0, 4), # control (const dest): green pre-fix too |
| 57 | +] |
| 58 | + |
| 59 | + |
| 60 | +def load_text_and_syms(elf_path): |
| 61 | + with open(elf_path, "rb") as f: |
| 62 | + ef = ELFFile(f) |
| 63 | + text = ef.get_section_by_name(".text") |
| 64 | + code, base = text.data(), text["sh_addr"] |
| 65 | + syms = {} |
| 66 | + # synth emits .symtab as an unnamed SHT_SYMTAB section, so match by |
| 67 | + # TYPE not name (#489 pattern — get_section_by_name returns None here). |
| 68 | + for sec in ef.iter_sections(): |
| 69 | + if sec.header.sh_type == "SHT_SYMTAB": |
| 70 | + for s in sec.iter_symbols(): |
| 71 | + if s.name: |
| 72 | + syms[s.name] = s["st_value"] |
| 73 | + if not syms: |
| 74 | + # Self-contained images carry no symtab — fall back to `synth disasm` |
| 75 | + # labels (same-arch decode, the established #374-harness pattern). |
| 76 | + import re |
| 77 | + dis = subprocess.run([SYNTH, "disasm", elf_path], |
| 78 | + capture_output=True, text=True).stdout |
| 79 | + syms = {m.group(2): int(m.group(1), 16) |
| 80 | + for m in re.finditer(r"^([0-9a-f]{8}) <(\w+)>:", dis, re.M)} |
| 81 | + return code, base, syms |
| 82 | + |
| 83 | + |
| 84 | +def main(): |
| 85 | + elf = tempfile.NamedTemporaryFile(suffix=".elf", delete=False).name |
| 86 | + subprocess.run( |
| 87 | + [SYNTH, "compile", str(WAT), "-o", elf, "--target", "cortex-m4", |
| 88 | + "--all-exports", "--safety-bounds", "software"], |
| 89 | + check=True, |
| 90 | + ) |
| 91 | + code, base, syms = load_text_and_syms(elf) |
| 92 | + |
| 93 | + eng = wasmtime.Engine() |
| 94 | + mod = wasmtime.Module(eng, WAT.read_bytes()) |
| 95 | + |
| 96 | + def wasmtime_run(fn, a, b): |
| 97 | + store = wasmtime.Store(eng) |
| 98 | + inst = wasmtime.Instance(store, mod, []) |
| 99 | + ret = inst.exports(store)[fn](store, a, b) |
| 100 | + mem = inst.exports(store)["memory"] |
| 101 | + img = bytes(mem.read(store, 0, MEM_BYTES)) |
| 102 | + return ret & 0xFFFFFFFF, img |
| 103 | + |
| 104 | + def unicorn_run(fn, a, b): |
| 105 | + fa = syms[fn] & ~1 # thumb bit |
| 106 | + mu = Uc(UC_ARCH_ARM, UC_MODE_THUMB) |
| 107 | + mu.mem_map(CODE, 0x10000) |
| 108 | + mu.mem_map(LIN, MEM_BYTES) |
| 109 | + mu.mem_map(STK - 0x8000, 0x10000) |
| 110 | + mu.mem_map(RET, 0x1000) |
| 111 | + mu.mem_write(CODE, code) |
| 112 | + mu.mem_write(LIN, b"\x00" * MEM_BYTES) |
| 113 | + mu.reg_write(UC_ARM_REG_SP, STK) |
| 114 | + mu.reg_write(UC_ARM_REG_R11, LIN) # linear-memory base |
| 115 | + mu.reg_write(UC_ARM_REG_R10, MEM_BYTES) # memory size (bytes) |
| 116 | + mu.reg_write(UC_ARM_REG_R0, a & 0xFFFFFFFF) |
| 117 | + mu.reg_write(UC_ARM_REG_R1, b & 0xFFFFFFFF) |
| 118 | + mu.reg_write(UC_ARM_REG_LR, RET | 1) |
| 119 | + try: |
| 120 | + mu.emu_start((CODE + fa - base) | 1, RET, count=500000) |
| 121 | + except UcError as e: |
| 122 | + # No vector here should trap; UDF or an unmapped access both mean |
| 123 | + # a wild pointer escaped — report as ERR, never a match. |
| 124 | + return f"ERR:{e}", b"" |
| 125 | + return mu.reg_read(UC_ARM_REG_R0), bytes(mu.mem_read(LIN, MEM_BYTES)) |
| 126 | + |
| 127 | + fails = 0 |
| 128 | + for fn, a, b in VECTORS: |
| 129 | + gt_ret, gt_img = wasmtime_run(fn, a, b) |
| 130 | + sy_ret, sy_img = unicorn_run(fn, a, b) |
| 131 | + if isinstance(sy_ret, str): |
| 132 | + ok, detail = False, sy_ret |
| 133 | + else: |
| 134 | + ok = sy_ret == gt_ret and sy_img == gt_img |
| 135 | + detail = f"ret synth={sy_ret} wasmtime={gt_ret}" |
| 136 | + if sy_img != gt_img: |
| 137 | + diff = next(i for i in range(MEM_BYTES) if sy_img[i] != gt_img[i]) |
| 138 | + detail += (f"; mem differs @{diff}: synth=0x{sy_img[diff]:02x} " |
| 139 | + f"wasmtime=0x{gt_img[diff]:02x}") |
| 140 | + fails += 0 if ok else 1 |
| 141 | + print(f"{fn}({a},{b}): {'OK' if ok else 'MISMATCH'} [{detail}]") |
| 142 | + print(f"\n{len(VECTORS) - fails}/{len(VECTORS)} match") |
| 143 | + print("ORACLE: PASS" if fails == 0 else f"ORACLE: FAIL ({fails})") |
| 144 | + sys.exit(1 if fails else 0) |
| 145 | + |
| 146 | + |
| 147 | +if __name__ == "__main__": |
| 148 | + main() |
0 commit comments