Skip to content

ELF: do not relocate an image the linker has already finished with - #801

Open
zardus wants to merge 1 commit into
masterfrom
feature/linked-image-relocations
Open

ELF: do not relocate an image the linker has already finished with#801
zardus wants to merge 1 commit into
masterfrom
feature/linked-image-relocations

Conversation

@zardus

@zardus zardus commented Aug 29, 2026

Copy link
Copy Markdown
Member

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Problem

CLE applies relocations the linker has already applied, so every analysis
downstream reads bytes that are not in the binary. On
binaries/tests/i386/linked_with_emit_relocs cle registers 8 relocations where
2 are still outstanding, and 5 of the 128 bytes of .text come back from
cle.Loader different from the file:

relocations registered: 8
.text bytes differing: 5 of 128

Nothing raises.

The same fault on real firmware: on u-boot 2026.07's big-endian MIPS image cle
applies 29228 entries and 65495 of the 413956 bytes of .text differ from the
file; on the little-endian one, 10200 entries and 23807 of 214932 bytes.

Root cause

__register_sections applies every relocation section when the object carries
no dynamic relocation tags:

if isinstance(sec_readelf, RelocationSection | RelrRelocationSection) and not (
    "DT_REL" in self._dynamic
    or "DT_RELA" in self._dynamic
    or "DT_JMPREL" in self._dynamic
    or "DT_RELR" in self._dynamic
):

That is right for a relocatable object, where nothing has been applied yet and
no relocation section is allocated. A linked image built with --emit-relocs,
which is how u-boot keeps its .rel.text, has had every one of those entries
applied by the linker already, and a REL entry takes its addend from memory, so
applying it again computes S + (S + A).

Fix

Run a relocation section only when it is SHF_ALLOC, or when the object is
relocatable and therefore has no allocated relocation section at all. On the
fixture that leaves the 2 outstanding entries and .text byte-identical to the
file; on both u-boot MIPS images it takes the applied relocations to 0 and
.text to byte-identical.

Deliberately not done here: GenericRelativeReloc.value adds mapped_base
where a relative relocation wants the distance the object moved. That is a
separate defect, and #773 fixes it with the identical one-line change this pull
request used to carry as well. Dropping it here is what lets the two apply
together.

Testing

tests/test_linked_image_relocations.py loads the fixture above -- a
freestanding clang and GNU ld build, linked at 0x1000, with --emit-relocs --
and asserts that the loaded .text matches the file. It fails on master.

Validation: #801 (comment)

sync: angr/binaries#219

session: sharpen

@zardus

zardus commented Aug 29, 2026

Copy link
Copy Markdown
Member Author

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Loading binaries/tests/i386/linked_with_emit_relocs before and after this
change, with the script the regression is built from:

import cle
path = "binaries/tests/i386/linked_with_emit_relocs"
ld = cle.Loader(path, auto_load_libs=False)
obj = ld.main_object
text = obj.sections_map[".text"]
with open(path, "rb") as fp:
    fp.seek(text.offset)
    on_disk = fp.read(text.memsize)
mem = ld.memory.load(text.vaddr, text.memsize)
print("relocations registered:", len(obj.relocs))
print(".text bytes differing:", sum(a != b for a, b in zip(mem, on_disk)), "of", text.memsize)

Before — the loaded .text is not the file's:

cle master 3812052
Unknown reloc 9 on X86
relocations registered: 8
.text bytes differing: 5 of 128

After — only the two allocated entries in .rel.dyn run:

with this change
relocations registered: 2
.text bytes differing: 0 of 128

The pointer table in .data still reads 0x2020, 0x2040 for functions at
0x1020 and 0x1040 on both sides. That is a different defect in
GenericRelativeReloc.value, and #773 fixes it; this change no longer touches
that line.

@zardus

zardus commented Aug 29, 2026

Copy link
Copy Markdown
Member Author

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Validation record for head 88a2363abf7669a5c0e2cc9c733cac608996e007 against baseline 3812052df2ad284cd16684fb7b7eb66e8d14dc6d, with angr/binaries#219 at f0024dccbcdaeb1c6c3a126b78c112357562f200.

2026-09-04. The relative-relocation half of this change has been dropped. It replaced the same line of cle/backends/elf/relocation/generic.py with the same expression as #773 and differed only in the comment above it, so the two conflicted; git merge-tree now reports a clean merge in both orders. #773 owns that line, and the figures below are for what is left.

Focused, python -m pytest, fixture from angr/binaries#219:

tests/test_linked_image_relocations.py, at this head    1 passed
tests, at this head                                     3 failed, 258 passed, 9 skipped
tests, master 3812052d alone                            3 failed, 257 passed, 9 skipped

The three are master's, not this change's: test_gopclntab.py::TestGoPclntab::test_macho_binary, the same class's test_macho_binary_supplies_the_function_symbols, and test_macho.py::test_relocatable_object, each a CLEFileNotFoundError. This head's one extra pass over master's is the regression added here.

The regression fails on master 3812052d: cle registers 8 relocations for the fixture where 2 are still outstanding, and the loaded .text differs from the file in 5 of its 128 bytes.

CI's Lint and Typecheck rules, run at this head against master 3812052d, using angr/ci-settings@b23d782 (pyright error counts, not the retired per-line score):

changed file pylint pyright errors
cle/backends/elf/elf.py 10.00 -> 10.00 46 -> 46
tests/test_linked_image_relocations.py new file, 10.00/10.00 0

u-boot 2026.07. .text diff counts bytes of .text in the loaded image that differ from the file:

tree malta, MIPS BE maltael, MIPS LE
master 3812052d 29228 relocs, 65495 of 413956 10200 relocs, 23807 of 214932
master + this 0 relocs, 0 of 413956 0 relocs, 0 of 214932

The MIPS repair is this change on its own and needs nothing else; the loaded image is byte-identical to the one the earlier two-part version of this pull request produced.

The x86 image reaches this code only once #802 restores its section table, and there this change and #773 are both needed:

tree qemu-x86
master 3812052d section table discarded: 0 relocs, 0 of 558238
master + #802 46101 relocs, 60045 of 558238
master + #802 + this 15659 relocs, 20632 of 558238
master + #802 + this + #773 15659 relocs, 0 of 558238

The last row's loaded image is byte-identical (sha256 d1cc71a2f83250c7ae79cebb6abb3b52e4ffca20d99409e6aab7ed6282017103) to master + #802 + the earlier two-part version of this pull request. So dropping the duplicated line costs nothing once #773 is in, and #802 now needs both rather than this one alone.

Corpus: loading every ELF file under tests/ in angr/binaries at #219 and comparing section count, symbol count, relocation count, address range and a sha256 of the loaded image between this head and master 3812052d gives 1 differing file, tests/i386/linked_with_emit_relocs, which is the fixture this change is about (8 relocations and one image hash on master, 2 and another here). The same 9 files fail to load on both sides.

The branch has been rebased from eac0e554 onto master 3812052d. git range-diff reports the commit unchanged and the diff against the new merge base is identical to the old one apart from an index line and a hunk offset, so every figure above describes the same patch. The rebase was forced by ci / Build: cle master a96c36c1 moved the archinfo and pyvex pins from 9.3.4.dev0 to 9.3.5.dev0, and at the old base uv pip install --no-sources ./src/cle could not resolve archinfo==9.3.4.dev0 against an archinfo master that now builds 9.3.5.dev0.

Not run on this head, and not claimed: angr's Python and Rust suites, the GUI and fixture suites, and pre-commit across every repository. cle's own suite above ran against an existing build of the compiled libraries rather than a fresh one. The CI disposition this record carried for head 43e8f064 is superseded.

@angr-bot

Copy link
Copy Markdown
Member

Corpus decompilation diffs can be found at angr/dec-snapshots@master...angr/cle_801

@zardus
zardus force-pushed the feature/linked-image-relocations branch 2 times, most recently from 43e8f06 to 8326d9f Compare September 4, 2026 01:44
__register_sections applies every relocation section when the object carries no
dynamic relocation tags. That is right for a relocatable object, where nothing
has been applied yet and no relocation section is allocated. A linked image
built with --emit-relocs -- which is how u-boot keeps its .rel.text -- has had
every one of those entries applied by the linker already, and a REL entry reads
its addend out of memory, so applying it again computes S + (S + A) and rewrites
the code.

Run a relocation section only when it is SHF_ALLOC, or when the object is
relocatable and therefore has none allocated. On the i386 fixture that drops the
registered relocations from 8 to the 2 that are still outstanding, and takes the
loaded .text from 5 of its 128 bytes differing from the file to none.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@zardus
zardus force-pushed the feature/linked-image-relocations branch from 8326d9f to 88a2363 Compare September 4, 2026 02:08
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