ELF: Take the word size from the machine, not the container - #791
Conversation
|
THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS Validation record for head
Known limitation, stated plainly. This models an x32 object's pointers as Addendum 2026-08-26 — what the 64-bit approximation costs, measured. The limitation stated at the end of this comment is sharper than "pointers are modelled too wide", and the detail matters for deciding whether to take this change as it stands. Resolving the architecture to Three outcomes, in order of how often they occur:
A minimal object built with None of this changes the recovery result above: 69,664 missed FDE-covered bytes to 2 across 17 objects, reproduced. It does change the trade being offered. A faithful x32 architecture — x86-64 instructions with 32-bit pointers — would fix the decoding and the relocation width together, and on this evidence that is the better change even though it is larger. If you would rather have that, this branch is the measurement to size it against; if you would rather take this now, the one-in-forty load failure is the cost. Re-keyed 2026-09-04, after a rebase onto cle master Hosted CI at Correction, 2026-09-04. An earlier version of the paragraph above read this same head at 16:24Z as 15 |
|
Corpus decompilation diffs can be found at angr/dec-snapshots@master...angr/cle_791 |
|
THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS This came out of assembling the angr/vibr preview snapshot (every green open PR merged together, then each component's full test suite run against the result with an Combined with #734, this PR fails Either PR alone passes; merged, x32 cores raise instead of loading. The auxv (and the rest of the note parsing) probably needs the container's word size even when the machine is 64-bit, rather than the arch detection reverting. Until this changes, this PR is excluded from the preview ( |
|
THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS Architecture resolution and the resulting disassembly of import cle
ld = cle.Loader("tests/x86_64/x32_relocatable.o",
main_opts={"backend": "elf"}, auto_load_libs=False)
m = ld.main_object
sec = m.sections_map[".text"]
data = ld.memory.load(sec.vaddr, sec.memsize)
for i in m.arch.capstone.disasm(data, sec.vaddr):
print(hex(i.address), i.bytes.hex(), i.mnemonic, i.op_str)Before — the ELFCLASS32 container resolves to 32-bit X86, and the same angr/cle master (a4fb800)After — with this changeThis fixture is small enough that both decoders produce output; what differs is |
|
THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS Resolved: the pull request this could not be rolled up withThis pull request has been excluded from every mono rollup because applied Resolving an x32 core to AMD64 is right for the instruction set and wrong for The entries that did fit decoded to nonsense: the first pair read cle 143abf3 on #734 takes the width from Verificationmaster + this branch + #734 at The one failure is this branch's own session: sharpen |
|
THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS An attempt to measure how much of a corpus's failure surface this removes, and Sample. 12,000 objects drawn uniformly at random, from a seeded permutation, Method. Each object is loaded with the catalogue's declared load recipe and What the sample can say. Nothing about the benefit: the sample contains no What it can say. That nothing else moves. On this head the 1,212 session: sharpen |
extract_arch resolves an ELF by its container class, which for EM_MIPS gives ArchMIPS32 for anything ELFCLASS32. The n32 and O64 ABIs break that: both hold a 64-bit MIPS instruction stream in an ELFCLASS32 container and say so in e_flags -- EF_MIPS_ABI2 for n32, an ABI nibble of E_MIPS_ABI_O64 for O64. Decoded as 32-bit MIPS, a non-leaf function stops at the `sd $gp` its prologue uses to spill the global pointer; capstone refuses it outright, and VEX quietly lifts the 64-bit store as a 4-byte one. The container class is still right about everything else, and this is where the x32 fix in #791 went wrong by taking the whole word size from the machine. Measured on real n32 objects, .rel.dyn has an sh_entsize of 8 (Elf32_Rel), .dynsym 16 (Elf32_Sym), .got a 4-byte slot, and DT_RELENT is 8. Resolving n32 to a plain 64-bit MIPS architecture reads an 8-byte implicit addend out of a 4-byte REL slot, writes 8 bytes back over the neighbouring word, strides the MIPS GOT by 8, and sends Elf32_Rel entries down the ELF64-MIPS three-type relocation path, which has no r_info_type2 to read. On a corpus of fourteen n32 and O64 objects that is nine hard load failures. So the word size stays with the class, and only the instruction set comes from the ABI: archinfo.ArchMIPSN32. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fc99806 to
0b2056d
Compare
|
THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS Rebased onto Worth raising, because The difference is availability, not principle: session: sharpen |
extract_arch passes reader.elfclass to arch_from_id as the width. The x32 ABI is where that is wrong: an ELFCLASS32 container holding EM_X86_64 code, so the class gives the pointer width while the machine gives the instruction set. Resolved by the class it becomes 32-bit X86 and none of the instruction stream decodes.
0b2056d to
7090300
Compare
THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS
Problem
ELF.extract_archtakes the machine name frome_machineand the width fromreader.elfclass, which agree for every ordinary object. The x32 ABI is wherethey do not: an ELFCLASS32 container holding
EM_X86_64code, so the classgives the pointer width and the machine gives the instruction set. Resolved by
the class,
arch_from_idreturns 32-bit X86 and the instruction stream isdecoded as x86. On
tests/x86_64/x32_relocatable.o:where the same bytes are
push rbpandlea eax, [rdi + rsi]. On a real x32object the divergence is not cosmetic — the effect on block and function
recovery is in the validation record.
Root cause
The width comes from the container:
reader.elfclassis the pointer width, which for x32 is genuinely 32. Theinstruction set is a property of
e_machine, and nothing consults it.Fix
Take the width from the machine for
EM_X86_64. That fixes the decode andleaves the pointer width modelled as 64-bit where the ABI has 32, which is a
real approximation with a real cost: relocation slots are then written eight
bytes wide into four-byte slots, which loses roughly one object in forty to a
failed load. That cost is measured in the validation record. A faithful x32
architecture — x86-64 instructions with 32-bit pointers — would fix both and is
a larger piece of work than this repair.
Testing
tests/test_arch_detect.py::TestArchDetect::test_elf_x32assertsisinstance(ld.main_object.arch, archinfo.ArchAMD64)on the fixture inangr/binaries#198 and fails that assertion on master. Across 17 x32 ELFs,
unwind-table function starts covered by no recovered block go from 69,664 to 2;
the population, the whole-suite results and the relocation-width measurement
are in the validation record.
Validation: #791 (comment)
sync: angr/binaries#198
session: sharpen