Repository navigation
Conversation
|
THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS Validation record for head
Corpus reproducers, six universal Mach-O libraries that fail on the baseline and load on the head (private dataset, cited by digest). Each is a two-slice fat file whose x86-64 and arm64 slices are both
Each was loaded with
Composition with the open loader changes was checked on the same objects: with #730 applied, the baseline still fails all six and the head still loads all six; with #765 applied, the head loads all six and the AVR object lands at 0x1000 instead of 0. Neither conflicts with this diff in 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_776 |
Every other placement in _map_object asks the address space first. The position-independent main binary does not: it takes 0x400000 unconditionally, which is right while it is the only object being placed and wrong as soon as it is not. A container backend loads its children through the loader, and a child can come out marked as the main binary. Universal2 marks every slice it loads, because a Mach-O executable refuses to load as anything else. The slices of a universal Mach-O therefore all asked for 0x400000, the first one got the backer, and the rest failed with "Address 0x400000 is already backed!". Six universal Mach-O libraries in a corpus sweep failed to load this way. Ask _is_range_free first and fall back to the ordinary rebase search, so the main binary keeps its conventional base whenever that base is available. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
6f08fcb to
3443309
Compare
|
THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS Two objects marked as the main binary, and the slices of a universal Mach-O, which marks every slice it loads. Both fixtures are 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("multi_arch", "fauxware_macho_multiarch")
ld = cle.Loader(path, auto_load_libs=False)
for o in ld.all_objects:
print(f"{type(o).__name__:<12} {o.min_addr:#x}-{o.max_addr:#x} is_main_bin={o.is_main_bin}")
print("--- two container children both marked as the main binary")
class Mock(cle.backends.Backend):
def __init__(self, size, **kw):
super().__init__("/dev/zero", None, **kw)
self.size = size
self.pic = True
self.has_memory = False
@property
def max_addr(self):
return self.mapped_base + self.size - 1
ld2 = cle.Loader(T("i386", "manysum"), auto_load_libs=False)
first = Mock(0x1000, arch=ld2.main_object.arch, is_main_bin=True)
second = Mock(0x1000, arch=ld2.main_object.arch, is_main_bin=True)
ld2.dynamic_load(first)
print(f" first {first.min_addr:#x}-{first.max_addr:#x}")
try:
ld2.dynamic_load(second)
print(f" second {second.min_addr:#x}-{second.max_addr:#x}"
f"{' ON TOP OF THE FIRST' if second.min_addr <= first.max_addr else ''}")
except Exception: # noqa: BLE001
print(" second: EXCEPTION: " + traceback.format_exc().strip().splitlines()[-1])Before — 0x400000 is taken without asking, so the second main-binary object lands on the first: cle master at
|
THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS
Problem
The main binary's conventional base is handed out without being asked for. Two
objects marked as the main binary, dynamically loaded into a loader for
binaries/tests/i386/manysum:That is not a contrived pair. A container backend loads its children through the
loader, and a Mach-O executable refuses to load as anything but the main binary,
so
Universal2marks every slice it loads. Onbinaries/tests/multi_arch/fauxware_macho_multiarchthe i386 slice lands on thecontainer object itself:
Root cause
_map_objectasks whether a range is free before it uses a custom base or alinked base, and does not ask before it uses
0x400000:The
elseis unconditional, so the main-binary case has no fallback at all: thesecond object to ask gets the same address as the first.
Fix
Ask before taking it, and fall back to the ordinary rebase search when it is
taken. The main binary keeps its conventional base whenever that base is free,
which is every single-main-object load.
Testing
tests/test_rebase.py::test_the_main_binary_base_is_not_handed_out_twiceplacestwo objects marked as the main binary and asserts the first takes
0x400000andthe second starts past the first's end; on the merge base both start at
0x400000.Five unavailable binaries recorded by the campaign reproduce the same failure.
On the exact patch parent
46a37333f4f59b0facf8774ee743ebc4cc074e9b,each raises
ValueErrorbecause address0x400000is already backed. On head3443309fefc6d821bcac352beaf23002d6026a50, all five load successfully. Theyare identified only by SHA-256:
19be29f38a574b5b1448a6256f2cec0f2ed850c750792dd46a57fa4a37b8e4ea1a74eab65c67676a4584c7316929054cc861bc6dbad65ebb9d8f530e9da9e3a824e042930c58dbc0c832f896b2569fa6e2160bd86cb877d2d476621fa91595777389f21dcf8406ea78817e5ed47985b2eeff59beb92e617879cbd90febb2f13e751e8656af24f4a5af5614167071c326df333c4046466573e9b34d61eb715908The matched regression set was the other 482 campaign-recorded Mach-O objects
in the retained 487-object selection. It was unchanged: 345 loaded on both
sides, 137 were refused identically on both sides, all 482 kept the same load
verdict and metadata, and zero got worse. Each side used one rooted environment;
the commands were:
#674 would hold the default universal load to a single slice, which hides this for
fat binaries without fixing the unchecked placement.
Validation: #776 (comment)
session: sharpen