Skip to content

test(#850): harden 9 differential oracles to read ELF .symtab, not disasm text#853

Merged
avrabe merged 1 commit into
mainfrom
fix/oracle-symtab-hardening-850
Jul 23, 2026
Merged

test(#850): harden 9 differential oracles to read ELF .symtab, not disasm text#853
avrabe merged 1 commit into
mainfrom
fix/oracle-symtab-hardening-850

Conversation

@avrabe

@avrabe avrabe commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

What

Nine sibling differential/oracle scripts under scripts/repro/ located function offsets by regex-parsing synth disasm TEXT. That is host-dependent — the exact latent pattern that made the v0.50 rv32 memory.size/grow oracle fail 100% on the Linux CI runner (empty syms → "no symbol for export sz") while passing on macOS (#850). This converts each to read the ELF symbol table via pyelftools instead (host-independent), copying the rv32_mem_size_grow_242_differential.py reference fix.

Scripts converted (9)

script regime address handling
add_imm_large_differential.py ARM reloc mask & ~1 (Thumb bit)
bulk_memory_374_differential.py ARM linked mask & ~1also fixes trap_addr
div_const_differential.py ARM reloc, positional mask + set() dedupe + STT_FUNC
i64_large_offset_382_differential.py ARM reloc mask & ~1
load_store_big_offset_382_differential.py ARM linked mask & ~1
uxth_fold_differential.py ARM reloc mask & ~1
control_step_riscv_differential.py RV32 reloc st_value direct (no mask)
controller_step_riscv_differential.py RV32 reloc st_value direct
signed_div_const_riscv_differential.py RV32 reloc st_value direct

Key findings / correctness

  • synth emits the symtab as an UNNAMED SHT_SYMTAB section for the ARM builds (get_section_by_name(".symtab") returns None), so every script iterates by sh_type == "SHT_SYMTAB" and filters st_shndx == text_idx.
  • ARM Thumb function symbols carry the Thumb bit (bit 0); st_value = clean disasm addr + 1. Masking & ~1 at map construction makes the map byte-identical to the old disasm map, so every downstream (CODE + fa - base) | 1 formula is unchanged. In bulk_memory_374 the mask is load-bearing for trap_addr too (the hook compares the even executing PC — an unmasked odd trap_addr would silently never fire, a vacuous green).
  • div_const is positional (sort addrs, zip with PAIRS): the symtab carries two names per address (func_N + export alias), so dedupe with set() and filter STT_FUNC before the len == PAIRS assert.
  • RV32 objects name the section .symtab, st_value is the .text offset with no Thumb bit → used directly.
  • No disasm fallback added: every target build carries a symtab; none are the stripped self-contained images that motivated 677/679's fallback.

Verification

Captured each script's baseline output before editing, then re-ran after with a freshly-built binary — all 9 PASS with identical match counts (before == after):

control_step 5/5 · controller_step 5/5 · signed_div 5/5 · add_imm 8/8 · div_const 338/338 · uxth 7/7 · i64_lo 5/5 · ls382 5/5 · bulk_memory 16/16

Cross-checked masked symtab addresses == disasm addresses across all ARM ELFs and RV32 (st_value=0, base=0) regimes before deleting the disasm path.

bulk_mask_679 and bulk_local_clobber_677 already read the symtab by type and mask & ~1 at use — verified their symtab branch fires (fixtures carry a SHT_SYMTAB), so left as-is (already hardened).

Test-harness only — byte-invisible to codegen.

Advances #850.

🤖 Generated with Claude Code

https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L

…sasm text

Nine sibling differential scripts located function offsets by regex-parsing
`synth disasm` TEXT — host-dependent, the exact latent pattern that made the
v0.50 rv32 memory.size/grow oracle fail 100% on the Linux CI runner (empty
syms) while passing on macOS (#850). Convert each to read the ELF symbol table
via pyelftools instead (host-independent), copying the rv32_mem_size_grow_242
reference fix.

synth emits the symtab as an UNNAMED SHT_SYMTAB section (get_section_by_name
(".symtab") returns None for the ARM builds), so all scripts iterate by
sh_type == "SHT_SYMTAB" and filter st_shndx == text_idx.

Address semantics locked per regime, verified before==after against the
captured baselines (same match counts):
- ARM (add_imm_large, bulk_memory_374, div_const, i64_large_offset_382,
  load_store_big_offset_382, uxth_fold): ARM Thumb function symbols carry the
  Thumb bit (bit 0), so mask `& ~1` at map construction — the masked value
  equals the clean disasm address exactly, making every downstream
  `(CODE + fa - base) | 1` formula byte-identical. In bulk_memory_374 the mask
  is load-bearing for trap_addr too (the hook compares the EVEN executing PC;
  an unmasked odd trap_addr would silently never fire).
- div_const is positional (sort addrs, zip with PAIRS): the symtab carries two
  names per address (func_N + export alias), so dedupe with set() and filter
  STT_FUNC before the len==PAIRS assert; masked-sorted set equals the old
  sorted disasm addresses.
- RV32 (control_step_riscv, controller_step_riscv, signed_div_const_riscv):
  --relocatable, st_value is the .text offset with no Thumb bit — use directly
  (CODE + fa), no mask, no base subtract.

No disasm fallback added: every target build carries a symtab and none are the
stripped self-contained images that motivated 677/679's fallback — a fallback
would just re-import the host-dependent path #850 removes.

bulk_mask_679 and bulk_local_clobber_677 already read the symtab by type and
mask `& ~1` at use — verified their symtab branch fires (fixtures carry a
SHT_SYMTAB), so they were left as-is (already hardened).

Test-harness only — byte-invisible to codegen. All 9 re-run PASS with identical
results to their pre-conversion baselines.

Advances #850.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
@avrabe
avrabe merged commit 8d46593 into main Jul 23, 2026
39 of 40 checks passed
@avrabe
avrabe deleted the fix/oracle-symtab-hardening-850 branch July 23, 2026 17:46
@codecov

codecov Bot commented Jul 23, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant