COFF: Extend the object over the whole image it maps - #775
Conversation
|
THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS Validation record for head
Corpus reproducers, four relocatable COFF objects that fail on the baseline and load on the head (private dataset, cited by digest):
Each was loaded with Caveats: the suites that did not run are angr, angr-management, archinfo, claripy, pypcode, pyvex, the Rust and GUI suites, and the angr-agentic workspace checks — a live corpus sweep pins the shared virtualenv, so this is the scoped cle-only equivalent rather than the complete workspace gate. Re-keyed 2026-08-28. The figures above were measured at |
|
Corpus decompilation diffs can be found at angr/dec-snapshots@master...angr/cle_775 |
The backend maps the entire object file at rva 0 and leaves max_addr to the default, which derives the extent from the section table. An object file's relocation, symbol and string tables sit after the last section's raw data, so the object claimed less address space than it backed. The loader hands the space above max_addr to the next object, so it placed the extern object inside the tail of the image and Clemory refused the backer with "Address ... is already backed!". Four COFF objects in a corpus sweep failed to load this way. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
85c9359 to
76c6f05
Compare
|
THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS The extent a COFF object reports beside the memory it actually backs. The fixture is already on binaries master. the reproducerimport logging, os
logging.getLogger("cle").setLevel(logging.CRITICAL)
import cle
BIN = os.environ["BINARIES"] # a checkout of angr/binaries
T = lambda *p: os.path.join(BIN, "tests", *p)
path = T("x86_64", "fauxware.obj")
ld = cle.Loader(path, auto_load_libs=False)
obj = ld.main_object
mapped_end = obj.min_addr + max(s + len(b) for s, b in obj.memory.backers())
print(f"object extent {obj.min_addr:#x}-{obj.max_addr:#x}")
print(f"memory it backs {obj.min_addr:#x}-{mapped_end - 1:#x}")
print(f"{'covered' if obj.max_addr >= mapped_end - 1 else 'SHORT by ' + hex(mapped_end - 1 - obj.max_addr) + ' bytes'}")
probe = 0x403000
print(f"address {probe:#x} is inside the mapped image and reads as "
f"{ld.memory.load(probe, 8).hex()}")
print(f"find_object_containing({probe:#x}) -> {ld.find_object_containing(probe)}")
print(f"describe_addr({probe:#x}) -> {ld.describe_addr(probe)}")
for o in ld.all_objects:
if o is not obj:
print(f"next object {type(o).__name__} at {o.min_addr:#x}-{o.max_addr:#x}"
f"{' OVERLAPS the mapped image' if o.min_addr < mapped_end else ''}")Before — the object stops 0x1106 bytes short of its own image, so addresses it backs belong to no object: cle master at
|
|
THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS What this changes on a real corpus, measured. It changes the extent a loaded Sample. 12,000 objects drawn uniformly at random, from a seeded permutation, Method. All 468 are loaded with the catalogue's declared recipe against What changes. Every one of the 432 loaded objects. What does not change. Recovered functions and CFG nodes are identical on Overlap with the other open COFF changes. #724, #764 and #761 each touch The corpus is not redistributable, so its objects are described by architecture, session: sharpen |
THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS
Problem
A COFF object claims less address space than it backs. On
binaries/tests/x86_64/fauxware.obj:0x1106bytes of mapped memory belong to no object. Every consumer that askswhich object owns an address --
describe_addr, symbol attribution, and theloader's own placement of the next object, which starts from
max_addr-- is toldthat range is free.
Root cause
The backend maps the whole object file at rva 0:
but leaves
max_addrtoBackend's default, which derives it from the sectiontable. An object file's relocation, symbol and string tables sit past the last
section's raw data, and they are inside the image because the image is the whole
file. So the two disagree by exactly the size of those trailing tables.
Fix
Report the extent of the image the backend actually maps; the backend is the only
thing that knows how much of the file went into memory.
Testing
tests/test_coff.py::TestCoff::test_the_object_extends_over_everything_it_mapsloads the fixture and asserts
max_addrreaches the end of the highest backer; itis short by
0x1106on the merge base. The fixture is already on binaries master.#764 rewrites the same lines of this constructor, and #761 and #724 append to
tests/test_coff.pyat the same point, so whichever of them lands first theothers will want a trivial rebase.
Validation: #775 (comment)
session: sharpen