ELF: keep the section header table when one section's sh_link is dangling - #802
ELF: keep the section header table when one section's sh_link is dangling#802zardus wants to merge 1 commit into
Conversation
|
THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS Loading import cle
path = "binaries/tests/i386/hash_without_dynstr"
obj = cle.Loader(path, auto_load_libs=False).main_object
print("sections:", [s.name for s in obj.sections])
print("function symbols:", sorted(s.name for s in obj.symbols if s.is_function))Before — cle masterAfter — the two sections that cannot be interpreted are kept uninterpreted with this changeThe |
|
THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS Validation record for head Complete workspace gate, in a per-feature instance adopting angr, cle and binaries:
SUITES SKIPPED (feature has not adopted, did NOT run): archinfo pypcode pyvex pysoot claripy angr-management. A green gate that skipped a suite is green over less than it appears to be. Focused, on the same head: Both of its assertions fail on The narrower fallback still fires where it should: No change to anything already in the corpus: loading every ELF file in On six u-boot 2026.07 images built for qemu targets on five architectures, 365 functions declared Landing #801 and #773 first matters: with the section table back but the relocation handling unfixed, this head leaves 60045 of the 558238 bytes of the x86 image's Not run here: the six skipped suites above, and CI's own matrix. CI disposition, head Every red check on the old head was inherited.
Still red, and not fixable here: |
|
Corpus decompilation diffs can be found at angr/dec-snapshots@master...angr/cle_802 |
…ling ELF.__init__ probes the section table with list(iter_sections()) and, on any exception, zeroes e_shoff, e_shentsize, e_shnum and e_shstrndx and rereads the file from the program headers alone. pyelftools validates sh_link when it builds a section, and u-boot keeps .hash while its linker script discards .dynstr and .dynsym, so the linker writes sh_link = 0 and the walk dies on ELFError. One section nothing needs therefore cost .symtab -- 11631 entries on the arm64 image, 3796 of them STT_FUNC -- along with the .debug_* sections and every section-derived memory region, leaving CFGFast to miss 365 functions across six u-boot objects on five architectures. A section pyelftools refuses to specialise is now handed back as a plain section, so the header and the data survive and only the interpretation is missing. The program-headers-only fallback stays for a table that cannot be read at all. A relocation section whose sh_link is then not a symbol table falls back to .symtab, extending the NullSection case already there, and the separate debug-file reader is made lenient for the same reason.
33c9a15 to
03316c1
Compare
THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS
Problem
One section CLE has no use for takes the whole section header table down with
it, silently. On
binaries/tests/i386/hash_without_dynstr, and on the u-boot2026.07 qemu images for x86, arm and arm64,
cle.Loader(path).main_object.sectionsis[]and.symbolsis[]although
.symtabis right there -- 11631 entries on a 2026.07qemu-arm64build, 3796 of them
STT_FUNC. The.debug_*sections and everysection-derived memory region go with them. The only trace is one line:
Downstream, CFGFast misses 365 functions across six u-boot objects on five
architectures --
bootmeth_read_allat0x9bc8,bootdev_get_bootflow,efi_var_collect_mem,enable_interrupts. u-boot reaches most of them onlythrough a linker list, so with no symbol and no call there is nothing left to
find them by. 299 of the 365 have a basic block starting at exactly the declared
address, so the code was decoded and no function was ever made there.
Root cause
ELF.__init__probes the table withlist(self._reader.iter_sections())and,on any exception, zeroes
e_shoff,e_shentsize,e_shnumande_shstrndxand rereads the file from the program headers alone. pyelftools validates
sh_linkwhen it builds a section, and u-boot keeps.hashwhile its linkerscript discards
.dynstrand.dynsym, so the linker writessh_link = 0andthe walk dies on
ELFError: Section points at section 0 of type SHT_NULL, expected SHT_SYMTAB/SHT_DYNSYM. The fallback is right for a section table thatcannot be read at all; it is much too broad for one section that cannot be
interpreted.
Fix
A section pyelftools refuses to specialise is handed back as a plain section
instead, so the header and the data survive and only the interpretation is
missing -- which is the correct answer for a section whose link is broken. The
program-headers-only fallback stays for a genuinely unusable table:
binaries/tests/i386/oxfoo1m3still takes it, unchanged. A relocation sectionwhose
sh_linkis then not a symbol table falls back to.symtab, extendingthe
NullSectioncase already there, and the debug-file reader is made lenientfor the same reason.
359 of the 365 functions come back: 134 on the x86 image, 119 on arm64, 106 on
arm. Four of the rest are ppce500 addresses where angr decodes no block at all,
which is a different question.
Restoring the table also makes CLE apply these images' relocation sections for
the first time, and it gets that wrong in two ways that rewrite
.text. #801fixes one and #773 the other, and this pull request needs both: on the x86 image
.textcomes back with 60045 of its 558238 bytes differing from the file onthis change alone, 20632 with #801 as well, 39413 with #773 as well, and 0 only
with both. They merge cleanly in every pairing, so merging in number order --
#773, #801, #802 -- is enough.
Testing
tests/test_elf_resiliency.pyloadsbinaries/tests/i386/hash_without_dynstr,a freestanding clang and GNU ld build carrying the same section table, and
asserts that
.symtabis present and that the three functions reachable onlythrough a pointer table have symbols. Both assertions fail on master. Loading
all 826 ELF files in angr/binaries and hashing each loaded image gives identical
results before and after this change.
Validation: #802 (comment)
sync: angr/binaries#219
session: sharpen