Conversation
u-boot's x86-64 image cannot be loaded at all. cle.Loader raises
ELFError("SHT_SYMTAB section points at section 0 of type SHT_NULL, expected
SHT_STRTAB") out of MetaELF._get_relro, so nothing else in the file is ever
read. The load dies while CLE is working out whether the file has RELRO, and it
does not: the file has three program headers, PT_LOAD, PT_DYNAMIC and
PT_GNU_STACK.
_get_relro asked iter_segments() whether a PT_GNU_RELRO header was there.
pyelftools builds each segment before its type can be read, and building the
PT_DYNAMIC one walks the whole section table looking for the matching .dynamic
section. The walk reaches .hash, whose sh_link is .dynsym, whose sh_link is 0
because u-boot's linker script discards .dynstr and GNU ld leaves the link
dangling. Whether a program header of a given type is present is a question
about the program headers, so read those instead. The .dynamic lookup on the
next line still walks the section table, so a file that has a PT_GNU_RELRO
header and the same broken link fails there instead; nothing measured does
both.
__register_dyn then failed a second time on the same file. pyelftools resolves a
tag's string as it hands the tag back, so it wants the dynamic string table for
every tag rather than for the four whose value is a string offset. This object
has no DT_STRTAB and no .dynstr, so there is nothing to build one from:
pyelftools 0.33 asserts, and 0.29 through 0.32 raise. CLE already handled a
missing dynamic string table three lines further down; the tag loop above it
just ran first. Take the table before the loop, treat its absence as an answer
rather than an error, and record every tag's value either way.
|
THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS Validation record for head The complete workspace gate could not be run: six corpus sweep lanes are live on this machine and pin four compiled libraries by content, so entering the workspace shell would reinstall the editables underneath them. What ran instead, on an isolated worktree of this head with the interpreter chosen by
The three failures are the same three on both sides and are not this change: SUITES SKIPPED (did NOT run): angr Python, angr Rust, the pre-commit hook set, the angr-agentic workspace checks, and CI's own matrix. A gate this narrow is green over much less than the usual one. Merge-base lint comparison, as No change to anything already in the corpus: loading every ELF file in Both hunks run on that corpus rather than being skipped: 530 of the 858 have a The objects that do reproduce it are in a corpus sweep here. A snapshot of every ledger file in all 101 epochs, taken at 2026-09-04T02:41Z, gives 910 rows over 785 distinct objects in three groups that share no object:
The sweep is still writing, so read those as lower bounds; a rescan forty minutes later had the second group at 688 objects and the third at 140. The first group is the u-boot image in the description. 644 of the 645 in the second group match a megabench catalog row, and all 644 are The third group is a different call site with a different trigger -- a One limit this change does not remove: Not run here: the skipped suites above, and CI's own matrix. session: sharpen |
|
THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS Loading the u-boot v2026.07 import sys, traceback, logging, cle
logging.basicConfig(format="%(levelname)s:%(name)s:%(message)s")
path = sys.argv[1]
print(">>> cle.Loader(path, auto_load_libs=False)")
try:
ld = cle.Loader(path, auto_load_libs=False)
except Exception:
traceback.print_exc(file=sys.stdout)
raise SystemExit
o = ld.main_object
print(o)
print("relro ", o.relro)
print("entry ", hex(o.entry))
print("sections ", len(o.sections))
print("symbols ", len(o.symbols))
print("function symbols", sum(1 for s in o.symbols if s.is_function))Before -- the constructor raises, so there is no object at all: cle master 3812052After -- the file loads. Its sections are still gone, because the same dangling with this changeAfter, with #802 merged in as well -- the section table survives too, and the with this change and #802 at 03316c1 |
|
Corpus decompilation diffs can be found at angr/dec-snapshots@master...angr/cle_815 |
THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS
Problem
CLE cannot load u-boot's x86-64 image at all.
cle.Loader(path, auto_load_libs=False)raises:
The object is u-boot v2026.07 for
qemu-x86_64_defconfig, gcc 13.3.0,-Os -gdwarf-4(sha256
8224b8886308b6b00282815df304b0f75831de3516df77406a05b85c40838a43). It is 24 MB and carriesa good
.symtab: 13,273 entries, 4,372 of themSTT_FUNC, and 4,243 functions withDWARF ranges.
board_init_fat0x113b65d,board_init_rat0x113bbb9anddo_bootmat0x1123650are all in it and CLE sees none of them, because theexception comes out of the constructor and there is no object at all.
Both readelfs read the file: GNU readelf 2.46 prints its dynamic section with no
complaint, llvm-readelf 21 warns about the same dangling link and carries on, both
exit 0.
Root cause
The file has no dynamic string table, and pyelftools will not go near a dynamic
segment without one. CLE walks into that twice.
_get_relroasks whether aPT_GNU_RELROprogram header is present. It asks throughiter_segments(), and pyelftools builds each segment before its type can be read;building the
PT_DYNAMICone walks the entire section table for the matching.dynamicsection. That table, read straight out of the file:
There is no
.dynstr. u-boot's linker script discards it and GNU ld leaves.dynsym'slink pointing at section 0, so the walk goes
.hash->.dynsym-> string table andraises. The whole load is lost to a question whose answer is
Relro.NONE: the file hasthree program headers,
PT_LOAD,PT_DYNAMICandPT_GNU_STACK, and no RELRO at all.__register_dynis the second. pyelftools resolves a tag's string as it hands the tagback, so
iter_tags()wants the dynamic string table for every tag rather than for thefour whose value is a string offset. This object's dynamic array is
DT_DEBUG,DT_RELA,DT_RELASZ,DT_RELAENT,DT_FLAGS_1,DT_RELACOUNT,DT_NULL-- noDT_STRTAB, and not one string among them. pyelftools 0.33 asserts, 0.29 through 0.32raise. CLE already handles a missing dynamic string table three lines further down:
The tag loop above it just runs first.
Fix
_get_relroreads the program headers rather than materialising the segments, which iswhat the question was.
__register_dyntakes the string table before the loop rather thanafter it, treats "there is none" as an answer rather than an error, and records every
tag's value either way, skipping only the four string-valued tags when there is no table
to resolve them with. Objects that have one behave exactly as before.
It does not close the next line.
get_section_by_name(".dynamic")walks the sectiontable too, so a file with a
PT_GNU_RELROheader and a table pyelftools refuses stillfails in
_get_relro. Nothing I can measure has both.It is also not the whole of
_get_relro: the open #731 guards theDT_FLAGSread furtherdown and
MetaELF.extract_soname, which fail the same way. The two do not overlap andmerge cleanly in either order. #731 says an object like this "still will not load, since
ELF.__register_dyn()genuinely needs the strings"; it does not, when no tag carriesone.
It also does not restore the sections. On master the load now
completes with zero sections and zero symbols, because a second, independent failure --
the same dangling
sh_linkseen fromELF.__init__-- makes CLE discard the sectionheader table. That is #802, which at
03316c16does not overlap this diff andmerges with it cleanly. With both in, the object loads with its 43 sections, 13,273 symbols and 4,372
function symbols, and the loaded image is byte-identical to the file's
PT_LOADcontents.
Testing
No regression test comes with this. Nothing tracked in
angr/binariesreproduces it: ofits 858 ELF files, none raises out of
_get_relro, and every one of the 606 that has aPT_DYNAMICsegment yields a dynamic string table, so there is nothing already in thetree to write a test against. The image above is 24 MB and does not belong in a test
repository. Objects that do reproduce the second site are public and small -- the
smallest I have is a 6,912-byte Alpine debug ELF -- so a fixture is available and I will
add one on request; it is left out here so this change does not depend on an open
angr/binariespull request.Loading all 858 before and after gives identical results on every field recorded, down to
a sha256 over every backer of the loaded image and the traceback frames of the ten that
fail on both sides. Both hunks run on that corpus, and the 598 files carrying a
DT_NEEDED,DT_SONAME,DT_RUNPATHorDT_RPATHresolve every one of those strings tothe same bytes. The cle suite is 3 failed, 257 passed, 9 skipped either way, the three
being pre-existing missing Mach-O fixtures.
Beyond that image, a corpus sweep here has at least 645 further ELF objects failing on
master in
__register_dynfor want of a dynamic string table. I re-ran eight: all eightload, one giving 38 sections and 8,359 symbols where master raised.
Validation: #815 (comment)
session: sharpen