Skip to content

COFF: Extend the object over the whole image it maps - #775

Open
zardus wants to merge 1 commit into
masterfrom
feature/coff-image-extent
Open

COFF: Extend the object over the whole image it maps#775
zardus wants to merge 1 commit into
masterfrom
feature/coff-image-extent

Conversation

@zardus

@zardus zardus commented Aug 23, 2026

Copy link
Copy Markdown
Member

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:

object extent   0x400000-0x4028d4
memory it backs 0x400000-0x4039da
SHORT by 0x1106 bytes
address 0x403000 is inside the mapped image and reads as 00000000005d0100
find_object_containing(0x403000) -> None
describe_addr(0x403000) -> not part of a loaded object

0x1106 bytes of mapped memory belong to no object. Every consumer that asks
which object owns an address -- describe_addr, symbol attribution, and the
loader's own placement of the next object, which starts from max_addr -- is told
that range is free.

Root cause

The backend maps the whole object file at rva 0:

        self.memory.add_backer(0, bytes(self._image_vmem))

but leaves max_addr to Backend's default, which derives it from the section
table. 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.

object extent   0x400000-0x4039da
memory it backs 0x400000-0x4039da
covered
address 0x403000 is inside the mapped image and reads as 00000000005d0100
find_object_containing(0x403000) -> <Coff Object fauxware.obj, maps [0x400000:0x4039da]>
describe_addr(0x403000) -> .chks64+0x883 in fauxware.obj (0x3000)

Testing

tests/test_coff.py::TestCoff::test_the_object_extends_over_everything_it_maps
loads the fixture and asserts max_addr reaches the end of the highest backer; it
is short by 0x1106 on 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.py at the same point, so whichever of them lands first the
others will want a trivial rebase.

Validation: #775 (comment)

session: sharpen

@zardus

zardus commented Aug 23, 2026

Copy link
Copy Markdown
Member Author

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Validation record for head 76c6f0535f2157f4079a4616ff5e09a93db3a449 against baseline 46a37333f4f59b0facf8774ee743ebc4cc074e9b.

  • Regression: python -m pytest tests/test_coff.py::TestCoff::test_the_object_extends_over_everything_it_maps — fails on baseline (the object stops at rva 0x28d5 while its memory backs 0x39db bytes), passes on head
  • Focused: python -m pytest tests/test_coff.py — 6 passed
  • Full suite: python -m pytest tests/ — 240 passed, 9 skipped
  • Lint/type: pylint and pyright, merge-base relative, per changed file — 10.00 -> 10.00 and badness 0.0 -> 0.0 on both files
  • Hooks: pre-commit run --all-files — passed, tree unchanged
  • Workspace gate: cle only, in a detached worktree with its own virtualenv

Corpus reproducers, four relocatable COFF objects that fail on the baseline and load on the head (private dataset, cited by digest):

Architecture sha256 Baseline
x86-64 09f9934274cea1582c78e926651e74df05f610f17f7df6a5f1f397c31c5c3c1e Address 0x500000 is already backed!
x86 7237aa598570ab94832ba55f64e5427c9f313099495fecd563138d517b7b3fbe Address 0x500000 is already backed!
x86 b02cc40b120cd68d696b7de1303663eb7e8e62c2ffe2647d1d94f4354530e0bb Address 0x600000 is already backed!
x86 c160c90c2d42811b8adfe0d34da1232a52d9da7b9d6ac90e39e132999d5421b2 Address 0x800000 is already backed!

Each was loaded with cle.Loader(path, auto_load_libs=False, main_opts={"backend": "COFF"}). Loading every file under angr/binaries/tests (1039 objects, binaries at 12d67ec) the same way changes 5 loads, all of them COFF objects whose extent grew to the image they map, and produces no new error and no newly failing load.

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. pip check in that isolated environment reports the expected 9.3.3.dev0/9.3.4.dev0 skew between the branch and the pinned sibling wheels.

Re-keyed 2026-08-28. The figures above were measured at 85c9359aab22f7b132a710c0e120c010a6f28680 on baseline 6951e9221554f987e3971aa51f9c280a67d60324, which is the head the opening line named until now; the branch is at 76c6f0535f2157f4079a4616ff5e09a93db3a449 on 46a37333f4f59b0facf8774ee743ebc4cc074e9b. git range-diff 6951e9221554f987e3971aa51f9c280a67d60324..85c9359aab22f7b132a710c0e120c010a6f28680 46a37333f4f59b0facf8774ee743ebc4cc074e9b..76c6f0535f2157f4079a4616ff5e09a93db3a449 reports every commit unchanged and git diff 85c9359aab22f7b132a710c0e120c010a6f28680 76c6f0535f2157f4079a4616ff5e09a93db3a449 differs only by master's own advance (2 files changed, 4 insertions(+), 24 deletions(-)). Master touched none of the files this change touches between the two baselines, so every figure above still describes this head.

@angr-bot

Copy link
Copy Markdown
Member

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>
@zardus
zardus force-pushed the feature/coff-image-extent branch from 85c9359 to 76c6f05 Compare August 26, 2026 22:46
@zardus

zardus commented Aug 28, 2026

Copy link
Copy Markdown
Member Author

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 reproducer
import 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 46a37333f4f59b0facf8774ee743ebc4cc074e9b
object extent   0x400000-0x4028d4
memory it backs 0x400000-0x4039da
SHORT by 0x1106 bytes
address 0x403000 is inside the mapped image and reads as 00000000005d0100
find_object_containing(0x403000) -> None
describe_addr(0x403000) -> not part of a loaded object
next object ExternObject at 0x500000-0x500048

After — the extent covers the image, and the trailing tables are attributed to the object:

with this change, at 76c6f0535f2157f4079a4616ff5e09a93db3a449
object extent   0x400000-0x4039da
memory it backs 0x400000-0x4039da
covered
address 0x403000 is inside the mapped image and reads as 00000000005d0100
find_object_containing(0x403000) -> <Coff Object fauxware.obj, maps [0x400000:0x4039da]>
describe_addr(0x403000) -> .chks64+0x883 in fauxware.obj (0x3000)
next object ExternObject at 0x500000-0x500048

@zardus

zardus commented Aug 29, 2026

Copy link
Copy Markdown
Member Author

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

What this changes on a real corpus, measured. It changes the extent a loaded
object declares, not whether anything loads, so a load-depth probe reports zero
for it; the extent itself is measurable, and so is the absence of any knock-on
effect.

Sample. 12,000 objects drawn uniformly at random, from a seeded permutation,
out of a 624,920-object internal corpus of compiler- and vendor-produced
binaries; 11,989 were retrievable and probed, 468 of them COFF. 432 of those
load on master; the other 36 stop at NotImplementedError: Unsupported machine type, which is #724's class and is unchanged here.

Method. All 468 are loaded with the catalogue's declared recipe against
master (eac0e5540516b9199dd6a91933e80dc774ea3eac) and against this branch's
head (76c6f0535f2157f4079a4616ff5e09a93db3a449) in one environment, and the
loaded object is described from the outside. Separately all 468 run a complete
CFGFast(normalize=True, resolve_indirect_jumps=True) on both sides.

What changes. Every one of the 432 loaded objects. max_addr grows on
all of them, by 261 bytes at the smallest, 11,159 at the largest, a median of
830 and 454,916 in total — the relocation, symbol and string tables that sit
after the last section and that the object maps but did not previously claim. An
address in that tail answers "is this inside the object" correctly on this head
and incorrectly on master.

What does not change. Recovered functions and CFG nodes are identical on
every one of the 432
— 15,457 functions and 39,225 nodes on both sides, zero
objects differing — and no object that loaded before fails to load. So the
change is confined to the extent, which is what a one-line self._max_addr
assignment should do.

Overlap with the other open COFF changes. #724, #764 and #761 each touch
coff.py too, and none of the four merges cleanly on top of another: every pair
collides in cle/backends/coff.py or tests/test_coff.py. Measured separately,
#724 clears the 36 machine-type failures, #764 moves a section with no file
bytes off the image header on 398 of the 432, and #761 populates imports on
35.

The corpus is not redistributable, so its objects are described by architecture,
format and OS rather than named.

session: sharpen

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.

2 participants