Repository navigation
Conversation
|
THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS Validation record for head
Caveats: one PE in Re-keyed 2026-08-29. Re-keyed from What did move is the baseline, and it moved through this change's own file. Master gained four commits between the two, one of them Hosted CI at The corpus measurement posted separately on this pull request (#790 (comment)) — the five objects that raise Re-keyed 2026-09-08. Re-keyed from The change did not move. The two commit messages are left as they were written, so that the rebase stays a replay anybody can check with Everything below is re-measured at
Carried forward from the measurements above, which are properties of an unchanged payload rather than of the baseline: the paired 493-object load census, the zero-backing cost figures, the Hosted CI at |
|
Corpus decompilation diffs can be found at angr/dec-snapshots@master...angr/cle_790 |
|
THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS Coupling note, which the description above does not state: this change has a consumer in angr. Zero-filling a PE section's unmapped tail removes the Verified on So the two PRs decide the recovered string set between them and the result depends on merge order. Flagging it here rather than changing either. |
|
THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS Full loader output for Before — the backend builds 0xa600 bytes for an object that declares 0xb000, so the last address of the image and the whole uninitialised cle master (d2ecea0)After — the image spans the range the object declares, and the tail reads as the zeroes a PE section's virtual size means: with this change (6ae4329) |
6ae4329 to
d34beb5
Compare
|
THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS Rebased onto cle master The conflict was The change itself did not move: comparing the branch's own diff before and after the rebase with context discarded gives a byte-identical set of added and removed lines. Validation at the new head: |
d34beb5 to
9eab3c5
Compare
|
THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS A separate corpus measurement, from a sweep triaging its own unowned failure This change does not only widen the mapping. It clears a hard load failure: Mechanism. The clamp in Method. Each arm is a On cle master Six pull requests the index offered as candidates on the two rows, plus the Rate and denominator. 5 of 385 in that collection is 1.30% (Wilson 95% The corpus is not redistributable, so the objects are described by format, session: sharpen |
Backend.max_addr is the last address an object covers, not one past it, so the mapped image spans max_addr - min_addr + 1 bytes. The backend clamped it to the difference alone and dropped the final byte of the last section. The clamp is not a rare path: _get_memory_mapped_image() builds from raw section data and pads between sections, so it usually runs past the last section's virtual end and the branch is taken. Every PE in angr/binaries ends up with part of its declared range unbacked, 88 of the 92 by exactly one byte. Clemory.load returns up to the length it is asked for, so a base relocation whose field ends on the missing byte reads short and IMAGE_REL_BASED_HIGHLOW raises struct.error out of Backend.relocate, through Loader, and out of Project: the binary does not load at all. Five PE objects in a corpus sweep failed this way, each one a .NET assembly whose single fixup patches the operand of the jmp [_CorExeMain] stub in a sixteen-byte final section. Loader already sizes an object as max_addr - min_addr + 1 and hands the next one the space above max_addr, so the byte was already this object's own. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A section is mapped over its whole virtual size, and the bytes past the raw data the file holds for it are zero. That padding is emitted ahead of the next section, so the last section of all has nothing to back its own, and the image ends at its raw data while max_addr covers its virtual size. A section the file cuts short is excluded: those bytes are unknown rather than zero, which is what PE.Preserve incomplete sections already decided.
9eab3c5 to
0015420
Compare
|
THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS Second coupling note. cle#835 changes
0 load errors on every arm. The variant formula —
session: sharpen |
THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS
Problem
cle backs less of a PE than the object says it covers. On
binaries/tests/i386/windows/rain32.upx, whose.rsrcdeclaresmemsize=0x1000overfilesize=0x600:Every PE loses the last byte of its image this way, and a section whose virtual size exceeds its raw data loses that whole tail: 106 of the 106 PEs
angr/binariestracks atfc07821cback less than they declare. A base relocation whose field lands in the gap then gets a short buffer andIMAGE_REL_BASED_HIGHLOWraisesstruct.errorout ofLoader, losing the object entirely.Root cause
Two independent off-by-a-region errors in
cle/backends/pe/pe.py.Backend.max_addris the last address an object covers, not one past it, but the clamp readif self.max_addr - self.min_addr < len(mapped_image)and truncated tomapped_image[: self.max_addr - self.min_addr], one byte short of the declared range._get_memory_mapped_image()separately stops at the last section's raw data. A section is mapped over its wholeMisc_VirtualSize, but the zero padding that backs that is emitted ahead of the next section, so the last section of all gets none..rsrcis last inrain32.upx, which is why the 0xa00 bytes it declares beyond its raw data are in no object at all.Fix
The clamp spans
max_addr - min_addr + 1, and the image is zero-padded up to the highestvirtual address + Misc_VirtualSizeof any section whose raw data the file holds in full. Same binary, same commands:Deliberately not done: a section whose raw data lies outside the file keeps its unbacked tail, because those bytes are unknown rather than zero.
test_loading_incomplete_pe_filepins that behaviour, and widening the fix to cover it fails that test.Testing
tests/test_pe.py::test_mapped_image_covers_max_addrrequiresld.memory.load(min_addr, max_addr - min_addr + 1)onbinaries/tests/i386/simple_windows.exeto return the full declared length;test_mapped_image_covers_uninitialized_tailrequiresrain32.upx's.rsrctail to read as zeroes. Both fail on the commit before them andtests/test_pe.pyis 17 passed here. Across those same 106 PEs, 1 still backs less than it declares, and that is the out-of-file-section case above;CFGFast(normalize=True)over the public PE fixtures the change touches returns identical function and node address sets on both revisions.One consumer, flagged rather than changed: zero-filling a section's unmapped tail removes the
Nonereturn fromCFGFast._load_a_byte_as_intfor those addresses, which the open angr pull request 6943 depends on in itsis_sz = Falsebranch, so the two decide the recovered string set between them and merge order matters. Detail in the coupling note on this PR. cle#835 edits the same PE code and its description says this change'simage_endbecomes too small once it lands; measured, the two are independent and neither loses a byte, which the second coupling note records.Validation: #790 (comment)
session: sharpen