Skip to content

Rebase the PowerPC64 ELFv1 initial TOC value - #814

Open
zardus wants to merge 1 commit into
masterfrom
ppc64-rebase-abiv1-rtoc
Open

Rebase the PowerPC64 ELFv1 initial TOC value#814
zardus wants to merge 1 commit into
masterfrom
ppc64-rebase-abiv1-rtoc

Conversation

@zardus

@zardus zardus commented Sep 4, 2026

Copy link
Copy Markdown
Member

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Problem

MetaELF.ppc64_initial_rtoc returns a link-time address for PowerPC64 ELFv1 objects, so
an object the loader moves reports a TOC pointer that is not where its TOC is.
tests/ppc64/libc.so.6 from angr/binaries loads at 0x400000 under
angr.Project(path, auto_load_libs=False) and reports this:

main_object.entry               0x447b60
main_object.ppc64_initial_rtoc  0x1cc6f0
entry_state().regs.r2           0x1cc6f0

The entry point comes out of the same function descriptor as the TOC and is rebased; the
TOC is not. angr's set_entry_register_values stores the attribute straight into r2, so
a plain Project on that file starts with r2 one base too low, and every TOC-relative
load from it addresses memory outside the image. Any ELFv1 object the loader moves is
wrong by the distance it moved. ELFv2 is unaffected.

Root cause

_ppc64_abiv1_entry_fix unpacks both words out of the ELFv1 entry descriptor and stores
them as they appear in the file:

self._entry = self.memory.unpack_word(AT.from_lva(ep_offset, self).to_rva())
self._ppc64_abiv1_initial_rtoc = self.memory.unpack_word(AT.from_lva(ep_offset + 8, self).to_rva())

Backend.entry translates the first on the way out, with
return AT.from_lva(self._entry, self).to_mva(). ppc64_initial_rtoc returned the second
unchanged. The ELFv2 branch beside it computes from sections_map[".got"].vaddr, which
Region._rebase has already moved, so one property returned a mapped address under one
ABI and a linked address under the other. The two branches have meant different things
since the ELFv2 one arrived in #218.

Fix

Translate the ELFv1 value in the property, the way Backend.entry translates the entry
point, and leave None alone for a relocatable object that has no descriptor to read. It
belongs in the property rather than in _ppc64_abiv1_entry_fix, because that runs from
ELF.__init__ before the loader has picked a base. ppc64_initial_rtoc is now a mapped
address for every object:

main_object.ppc64_initial_rtoc  0x5cc6f0
entry_state().regs.r2           0x5cc6f0

0x5cc6f0 is also that object's mapped .got plus 0x8000, and the word cle itself
leaves at the descriptor once it has resolved the R_PPC64_RELATIVE covering it.

Testing

tests/test_ppc64_initial_rtoc.py::test_ppc64_abiv1_rebased loads tests/ppc64/libc.so.6
at 0x10100000 and asserts the TOC comes back as 0x102cc6f0, cross-checked against the
mapped .got; on master it comes back as 0x1cc6f0 and the test fails. The four existing
tests in that file are untouched and pass on both revisions. Loading all seventeen
tracked files under tests/ppc64/ on both revisions, each alone and again with its
libraries, moves nothing but this attribute — across 61 object loads per revision the
memory backers and the symbol tables hash identically, and the 31 rows that differ are
libc.so.6 and ld64.so.1 at the four bases they get mapped to.

Validation: #814 (comment)

session: sharpen

ppc64_initial_rtoc returns a mapped address under ELFv2, where it comes from
the .got section's vaddr, and a link-time address under ELFv1, where it is read
straight out of the entry descriptor. The entry point word beside it in that
same descriptor goes through AT.from_lva(...).to_mva() in Backend.entry, so an
ELFv1 shared object reports a rebased entry point and an unrebased TOC.

tests/ppc64/libc.so.6 mapped at 0x10100000 reports 0x1cc6f0; its mapped .got
plus 0x8000, and the descriptor word after cle resolves the R_PPC64_RELATIVE
over it, are both 0x102cc6f0. angr's SimLinux puts the attribute straight into
r2, so a plain Project on that object starts with r2 one base too low.

_ppc64_abiv1_entry_fix runs from ELF.__init__, before the loader picks a base,
so the translation belongs in the property rather than in the read.
@zardus

zardus commented Sep 4, 2026

Copy link
Copy Markdown
Member Author

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Loader and entry-state values for tests/ppc64/libc.so.6 from angr/binaries, before and
after this change. The script is angr.Project(path, auto_load_libs=False) followed by
reads of the entry descriptor, the property, and entry_state().regs.r2; the descriptor
address comes from the ELF header's e_entry translated with AT.from_lva(...).to_mva().

Before — the TOC is reported at its link-time address, 0x400000 below where the
loader put it, and that address reaches r2:

cle master 3812052
mapped_base                    0x400000
entry descriptor               0x5b38c0
mem[descriptor]     (entry)    0x447b60
mem[descriptor + 8] (TOC)      0x5cc6f0
.got vaddr + 0x8000            0x5cc6f0

main_object.entry              0x447b60
main_object.ppc64_initial_rtoc 0x1cc6f0
entry_state().regs.r2          0x1cc6f0

After — the property agrees with the relocated descriptor word and with the mapped
.got, and r2 follows:

with this change
mapped_base                    0x400000
entry descriptor               0x5b38c0
mem[descriptor]     (entry)    0x447b60
mem[descriptor + 8] (TOC)      0x5cc6f0
.got vaddr + 0x8000            0x5cc6f0

main_object.entry              0x447b60
main_object.ppc64_initial_rtoc 0x5cc6f0
entry_state().regs.r2          0x5cc6f0

@zardus

zardus commented Sep 4, 2026

Copy link
Copy Markdown
Member Author

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Validation record for head d1e1c0440ec74820c886a3149c8297c6ecd4bc96 against baseline 3812052df2ad284cd16684fb7b7eb66e8d14dc6d.

  • Regression: pytest tests/test_ppc64_initial_rtoc.py::test_ppc64_abiv1_rebased — fails on baseline at line 54 with assert 1885936 == 271369968, that is 0x1cc6f0 against 0x102cc6f0, and passes on head
  • Focused: pytest tests/test_ppc64_initial_rtoc.py — baseline 4 passed, head 5 passed
  • Full suite: pytest tests -n 4 — baseline 3 failed / 257 passed / 9 skipped, head 3 failed / 258 passed / 9 skipped, same three node ids on both
  • Consumer suite: the eight angr test files that name PowerPC64, at angr 854269112a5ce5b1b356cf6d67bff9abaf986b55 — 89 passed on both revisions, identical. This is a no-regression check rather than evidence for the change. Some of those runs do load a library whose value moves, but angr's only reader of the attribute is set_entry_register_values, and it reads loader.main_object
  • Lint: pre-commit run --all-files on head — 22 passed, 2 skipped, nothing rewritten, so black 26.5.1 as the repository pins it is clean; ruff check and ruff format --check on the two changed files exit 0 on both revisions. At repository root ruff format --check reports 6 files it would reformat, identically on both revisions and none of them touched here; ruff is not this repository's formatter
  • Type: pyright 1.1.411 under ci-image/scripts/typecheck.py from angr/ci-settings origin/master b23d782e1052da0e512b1a1ef1902d06e0545730, which compares per-file error counts and covers test files — metaelf.py 21 on both, test_ppc64_initial_rtoc.py 9 on both, exit 0. The gate was calibrated by checking that an earlier draft of the test, which lacked the isinstance narrowing, made it exit 1 with that file at 12 errors
  • Differential load: all seventeen tracked files under tests/ppc64/, each loaded alone and again with its libraries, 34 configurations that all load on both revisions, 61 object loads from a real file per revision. Per object it compares the mapped base, the sha256 of every memory backer, the sha256 of the sorted symbol table, the count of TOC-relative relocations, and ppc64_initial_rtoc. 31 rows differ and every one differs in ppc64_initial_rtoc alone: libc.so.6 0x1cc6f0 to 0x102cc6f0 and to 0x5cc6f0, ld64.so.1 0x40248 to 0x10340248, to 0x440248 and to 0x640248. Memory and symbol hashes are identical on all 61. The harness is calibrated: it is the run that printed those 31 differences

Caveats: the three full-suite failures are test_macho.py::test_relocatable_object and two in test_gopclntab.py, all CLEFileNotFoundError, and they are an artefact of the local angr/binaries checkout rather than of this change. It sits at 3de2c41a297606c45be7985aa09b3ae615424d61, which predates 003e82a2bfa641530924055695b36cec8af483ab, where tests/aarch64/relocatable_object.macho and tests/aarch64/langdetect_go.macho landed. They fail identically on both revisions. Hosted CI checks out current master and does not see them: all twenty checks on this head pass. cle's four TOC-relative relocation classes also read this property, and one tracked fixture reaches them: simple_object.o carries three, and its ppc64_initial_rtoc is None on both revisions because a relocatable object has no entry descriptor, so those three take the same .TOC. value not found path either way and the object's memory hashes identically. The workspace's full ecosystem gate was not run: corpus sweeps hold the working trees and the pinned toolchain, and entering the workspace shell would reinstall the editables underneath them.

@angr-bot

angr-bot commented Sep 4, 2026

Copy link
Copy Markdown
Member

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

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