Skip to content

PE: Build base relocations on every architecture the backend resolves - #755

Open
zardus wants to merge 1 commit into
masterfrom
feature/fix-cle-pe-reloc-arches
Open

zardus wants to merge 1 commit into
masterfrom
feature/fix-cle-pe-reloc-arches

Conversation

@zardus

@zardus zardus commented Aug 17, 2026 •

Copy link
Copy Markdown
Member

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Problem

A PE's whole .reloc directory is discarded on ARM64, ARMNT, MIPS and RISC-V. Loading tests/aarch64/windows/pe_reloc_arm64.exe, linked at 0x140000000, keeps only the padding entry, and rebasing the image then moves no pointer at all:

=== aarch64: tests/aarch64/windows/pe_reloc_arm64.exe
  reloc classes: [('IMAGE_REL_BASED_ABSOLUTE', 1)]
  total relocs: 1
    +0x2000  linked=0x1400011b8    rebased=0x1400011b8    delta=0x0

That is invisible at the preferred base and silently wrong anywhere else, including an image whose ImageBase is 0, where every absolute pointer in the file keeps a link-time value.

Root cause

PE._make_reloc looks the table up by self.arch.name, but ALL_RELOCATIONS was keyed on strings archinfo does not produce, and had no AArch64 entry at all:

ALL_RELOCATIONS = {
    "AMD64": relocation_table_generic,
    "arm": relocation_table_generic | relocation_table_arm,
    "X86": relocation_table_generic,
    "mips": relocation_table_generic | relocation_table_mips,
    "RISCV": relocation_table_generic | relocation_table_riscv,
}

archinfo names those architectures AARCH64, ARMEL, ARMHF, ARMCortexM, MIPS32 and RISCV64, so get_relocation returned None for every type and the loader logged Unknown reloc 10 on AARCH64. Re-keying alone is not enough: PEReloc.value fell through to self.resolvedby.rebased_addr, which no base relocation has, so a recognised type would have been dropped without a word.

Fix

The table is keyed on arch.name, IMAGE_REL_BASED_THUMB_MOV32 gains a value that reads and rewrites the imm4:i:imm3:imm8 halves of the MOVW.W/MOVT.W pair, and the base class returns None for a symbol-less relocation after warning once per type, so a recognised but unimplemented fixup is reported instead of being silently skipped. IMAGE_REL_BASED_ARM64_MOV32A and MOV32T stay unimplemented and now say so.

Rebasing by 0x10000 then moves every fixup:

  reloc classes: [('IMAGE_REL_BASED_ABSOLUTE', 1), ('IMAGE_REL_BASED_HIGHLOW', 3), ('IMAGE_REL_BASED_THUMB_MOV32', 2)]
    +0x2000  linked=0x40113f       rebased=0x41113f       delta=0x10000
    MOVW/MOVT +0x104c  linked=0x402000     rebased=0x412000     delta=0x10000

Testing

tests/test_pe_relocations.py loads both ARM images at their linked base and rebased: test_aarch64 asserts obj.memory.unpack_word(0x2000, size=8) == 0x1400011B8, test_armnt asserts thumb_mov32_immediate(obj.memory.load(0x104C, 8)) == 0x402000, and the two rebased cases assert rebased == linked + REBASE_DELTA for every fixup. test_x86_64_unchanged pins that tests/x86_64/windows/sioctl.sys keeps its 13 IMAGE_REL_BASED_DIR64 entries. All four ARM tests fail on the merge base, where the relocations do not exist.

Fixes #752. Needs angr/binaries#183 for the fixtures.

Validation: #755 (comment)

sync: angr/binaries#183

session: sharpen

@zardus

zardus commented Aug 17, 2026 •

Copy link
Copy Markdown
Member Author

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Validation record for head 04d58745ecc829d52624c44bc96f397a55abada2 against baseline 0e77ade3c39a3cee05f65051e57955675e1ac21b.

  • Regression: pytest tests/test_pe_relocations.py — 5 tests; against baseline cle all four ARM tests fail (empty relocation list, and the rebased pair reading unchanged bytes) and test_x86_64_unchanged passes; all 5 pass on head
  • Focused: pytest tests/ in cle — 230 passed, 9 skipped on head; 225 passed, 9 skipped on baseline
  • Lint/type: ruff check, ruff format --check, and run-ci-diff-checks.py --repository cle (the merge-base pylint and pyright comparison the hosted jobs apply) — all changed files improve or hold
  • Workspace gate: run-all-tests.sh --jobs 2 in an ANGR_FEATURE shell with cle, binaries and angr adopted — all selected suites passed: workspace checks, the test-inputs fixture check, every configured pre-commit hook, the per-feature instancing suite, cle 235 passed 9 skipped, angr 2,471 passed 46 skipped 2 xfailed 260 subtests, angr Rust 35 passed. archinfo, pypcode, pyvex, claripy and angr-management are skipped as unadopted and untouched. A first run at the default four workers lost tests/procedures/libc/test_strtol.py to SIGKILL; memory.events recorded exactly one new oom_kill, and the test passes alone in 140s, so the run was repeated at two workers

Corpus A/B, 1,557 PE objects, one process per object per side. The oracle is each
file's own base relocation directory parsed from its bytes; no expected value
comes from CLE. Counts exclude IMAGE_REL_BASED_ABSOLUTE padding entries.

machine objects base relocations in the files built on baseline built on head
I386 458 1,188,958 1,188,930 1,188,930
AMD64 469 213,518 213,518 213,518
ARM64 306 221,282 0 221,282
ARMNT 176 138,748 0 138,718
RISCV64 58 13,431 0 13,271
THUMB 43 88,722 0 88,722
AMD64^linux 37 138,411 0 0
LOONGARCH64 10 441 0 0

The last two rows are objects this change cannot help: 37 whose machine type the
backend does not resolve until #757, and 10 LOONGARCH64 objects that
archinfo has no architecture for.

Each object was then loaded twice, at its nominal base and 0x10000 higher, and
every fixup site compared with the file's value plus the shift the loader
applied:

fixup sites, over the 1,520 objects both sides load Baseline Head
HIGHLOW carrying the shift 1,188,911 1,293,526
HIGHLOW left unrelocated 193,365 28
DIR64 carrying the shift 186,496 363,769
DIR64 left unrelocated 234,713 160
THUMB_MOV32 carrying the shift 0 34,103
THUMB_MOV32 left unrelocated 34,133 30
carrying a value that is neither 0 0
  • Every difference: 388 objects gain base relocations. 242 of them load at their preferred base, where a fixup rewrites the same bytes, and their loaded memory is byte-identical to baseline. 146 have ImageBase 0 and are mapped at 0x400000, so their bytes change; 345,830 fixup sites on those were checked at two load addresses and every one carries exactly the shift.
  • Sites still unrelocated on head: 218, all in 29 objects whose .reloc blocks pefile refuses (page RVA outside the image, or a block it returns empty), so CLE never sees the entries. The I386 count of that class is 28 on both sides, which is the control.
  • x86 and x86-64: loaded memory, relocations, symbols, entry point and function hints byte-identical on 927 objects.
  • Load errors: 11 on both sides, unchanged — 10 ArchNotFound on LOONGARCH64, which pefile resolves and archinfo does not, and one IndexError in _meta_iat that Stop indexing PE and ELF header tables past their declared length #732 fixes. No new error, no timeout, no memory regression.
  • Thumb MOVW/MOVT decode checked independently on all 34,133 corpus instances: every site is a MOVW.W followed by a MOVT.W, every decoded immediate lands inside the image's linked address range, and re-encoding reproduces the original bytes.

CFG effect. Applying a base relocation only changes bytes when the image moves,
so the 242 objects that load at their preferred base cannot change a CFG. The
146 that CLE maps at 0x400000 because their ImageBase is 0 do. CFGFast with
defaults, one process per object per side:

Baseline Head
objects 132 132
objects whose block set changed — 58
blocks 431,052 435,819
functions 32,788 36,229
bytes covered 5,796,484 6,023,938
errors 0 0
timeouts 0 0

15 of those objects cover 252,544 more bytes and 42 cover 25,090 fewer. The
largest gains are the THUMB images whose sha256 begins 91af04b8a587edaf,
b3dcdbe21277b1df and 11ca64ae318f51e9, at 70,026, 61,080 and 37,574 more
bytes.

The reductions are the fix working: with the pointers still naming the old base,
CFGFast reached data and decoded it. The two largest are ac5738b8add1a987
and 7d77b3ba0e815b96, both THUMB. What the head stops recovering on the first
is a 0123456789ABCDEF table followed by a zero run; on the second, 4,562 of
the 4,592 bytes that disappear are the image's own string literals —
<null time>, Block number, Failed to cr....

CFGFast is deterministic on this population: two independent runs of the same
revision over all 247 objects agreed on every block address, function address,
block count and byte count, so every difference above is the change.

Caveats: the corpus is a private dataset, so objects are named by machine type and count rather than by path; the fixtures in angr/binaries#183 reproduce both architectures publicly. IMAGE_REL_BASED_ARM_MOV32 and the MIPS and RISC-V type tables are reachable now but still unimplemented, so they warn once and leave the fixup as linked; no object in the population uses them.

CI on this PR: all 20 checks are green at head 988c00868f584e716c44aa66fed8408f069e5c3e,
read 2026-09-03, with the fixture pull request angr/binaries#183 still open.
Test macos-15 and Test windows-2022 are among them, green in run
33021189687.

Correction, 2026-09-03. An earlier version of this comment said that
Test macos-15 fails and Test windows-2022 is cancelled with it because
cle's own matrix job checks out angr/binaries with no ref: and so gets
master. That described run
32006690767 of 2026-08-17 and no
longer holds: cle master gained the missing resolution in 5125f1ba ("Check
out a referenced angr/binaries pull request on macOS and Windows", 2026-08-18).

Re-keyed 2026-08-28. The figures above were measured at 2161f5b74330b5333a63c5ffb0211f4683cfd1db on baseline 45c6509c753d07f740099035cd41f7f473dc6f31, which is the head the opening line named until now; the branch is at 988c00868f584e716c44aa66fed8408f069e5c3e on 46a37333f4f59b0facf8774ee743ebc4cc074e9b. git range-diff 45c6509c753d07f740099035cd41f7f473dc6f31..2161f5b74330b5333a63c5ffb0211f4683cfd1db 46a37333f4f59b0facf8774ee743ebc4cc074e9b..988c00868f584e716c44aa66fed8408f069e5c3e reports every commit unchanged and git diff 2161f5b74330b5333a63c5ffb0211f4683cfd1db 988c00868f584e716c44aa66fed8408f069e5c3e differs only by master's own advance (12 files changed, 353 insertions(+), 44 deletions(-)). Master touched none of the files this change touches between the two baselines, so every figure above still describes this head.

Re-keyed 2026-09-04, after a rebase onto cle master 0e77ade3c39a3cee05f65051e57955675e1ac21b. The branch moved from 988c00868f584e716c44aa66fed8408f069e5c3e to 04d58745ecc829d52624c44bc96f397a55abada2 because angr master bumped its sibling pins from 9.3.4.dev0 to 9.3.5.dev0 on 2026-09-02, and ci / Build installs the branch's own pyproject.toml with uv pip install --no-sources, so a fresh run on the old head died on a .dev version no index publishes. check-stale-pins.py refuses the old head and exits 0 on the new one. This was a plain replay: no conflict, no hand resolution, no fixup. git range-diff 46a37333..988c0086 0e77ade3..04d58745 reports every commit =, and the branch's own added and removed lines are byte-identical across the move, 158 lines on each side. Master touched none of the files this change touches in the range rebased over. So every figure above describes the same patch on a new base.

Hosted CI at 04d58745ecc829d52624c44bc96f397a55abada2, read 2026-09-04T18:00Z: 20 checks, all green -- 18 check runs plus the docs/readthedocs.org:cle and pre-commit.ci - pr status contexts, in run 33891279925, attempt 2.

Correction, 2026-09-04. An earlier version of the paragraph above read this same head at 16:24Z as 15 success, 4 failure, 1 cancelled, with the failures on cle.errors.CLEFileNotFoundError for tests/aarch64/langdetect_go.macho and tests/aarch64/relocatable_object.macho -- fixtures cle master began needing on 2026-09-03 in b6ff025b (#808) and 3812052d (#787) -- and said the branch was waiting on a rebase of angr/binaries#183. angr/binaries#183 now sits on binaries master 003e82a2bfa641530924055695b36cec8af483ab and carries both fixtures, and the whole run was re-run at this unchanged cle head. Nothing on this branch moved.

@angr-bot

Copy link
Copy Markdown
Member

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

@zardus
zardus force-pushed the feature/fix-cle-pe-reloc-arches branch 2 times, most recently from 0775fe8 to 58d16f7 Compare August 22, 2026 15:23
@zardus
zardus force-pushed the feature/fix-cle-pe-reloc-arches branch from 58d16f7 to 988c008 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

Full base-relocation report for the ARM64 and ARMNT PE fixtures of angr/binaries#183, loaded at their linked base and rebased by 0x10000, with tests/x86_64/windows/sioctl.sys as an unaffected control. linked is the pointer at the fixup site when the image is loaded at its ImageBase; rebased is the same site after cle.Loader(..., main_opts={"base_addr": ...}).

Before — every base relocation is dropped, so rebasing moves nothing (delta=0x0) on both ARM images:

cle at the merge base, 46a3733
Unknown reloc 10 on AARCH64
Unknown reloc 7 on ARMEL
Unknown reloc 3 on ARMEL
cle: <cle at the merge base>/cle/__init__.py
ALL_RELOCATIONS keys: ['AMD64', 'RISCV', 'X86', 'arm', 'mips']
=== aarch64: tests/aarch64/windows/pe_reloc_arm64.exe
  arch.name: AARCH64  linked_base=0x140000000
  reloc classes: [('IMAGE_REL_BASED_ABSOLUTE', 1)]
  total relocs: 1
    +0x2000  linked=0x1400011b8    rebased=0x1400011b8    delta=0x0
    +0x2008  linked=0x1400011c0    rebased=0x1400011c0    delta=0x0
    +0x2010  linked=0x1400011cc    rebased=0x1400011cc    delta=0x0
=== armnt: tests/armel/windows/pe_reloc_armnt.exe
  arch.name: ARMEL  linked_base=0x400000
  reloc classes: [('IMAGE_REL_BASED_ABSOLUTE', 1)]
  total relocs: 1
    +0x2000  linked=0x40113f       rebased=0x40113f       delta=0x0
    +0x2004  linked=0x40114b       rebased=0x40114b       delta=0x0
    +0x2008  linked=0x40115b       rebased=0x40115b       delta=0x0
    MOVW/MOVT +0x104c  linked=0x402000     rebased=0x402000     delta=0x0
    MOVW/MOVT +0x1130  linked=0x403000     rebased=0x403000     delta=0x0
=== amd64 control: tests/x86_64/windows/sioctl.sys
  arch.name: AMD64 reloc classes: [('DllImport', 15), ('IMAGE_REL_BASED_ABSOLUTE', 1), ('IMAGE_REL_BASED_DIR64', 13)]

After — each type is recognised and applied, and every fixup moves by the rebase delta:

with this change, 988c008
cle: <cle with this change>/cle/__init__.py
ALL_RELOCATIONS keys: ['AARCH64', 'AMD64', 'ARMCortexM', 'ARMEL', 'ARMHF', 'MIPS32', 'PPC32', 'RISCV64', 'X86']
=== aarch64: tests/aarch64/windows/pe_reloc_arm64.exe
  arch.name: AARCH64  linked_base=0x140000000
  reloc classes: [('IMAGE_REL_BASED_ABSOLUTE', 1), ('IMAGE_REL_BASED_DIR64', 3)]
  total relocs: 4
    +0x2000  linked=0x1400011b8    rebased=0x1400111b8    delta=0x10000
    +0x2008  linked=0x1400011c0    rebased=0x1400111c0    delta=0x10000
    +0x2010  linked=0x1400011cc    rebased=0x1400111cc    delta=0x10000
=== armnt: tests/armel/windows/pe_reloc_armnt.exe
  arch.name: ARMEL  linked_base=0x400000
  reloc classes: [('IMAGE_REL_BASED_ABSOLUTE', 1), ('IMAGE_REL_BASED_HIGHLOW', 3), ('IMAGE_REL_BASED_THUMB_MOV32', 2)]
  total relocs: 6
    +0x2000  linked=0x40113f       rebased=0x41113f       delta=0x10000
    +0x2004  linked=0x40114b       rebased=0x41114b       delta=0x10000
    +0x2008  linked=0x40115b       rebased=0x41115b       delta=0x10000
    MOVW/MOVT +0x104c  linked=0x402000     rebased=0x412000     delta=0x10000
    MOVW/MOVT +0x1130  linked=0x403000     rebased=0x413000     delta=0x10000
=== amd64 control: tests/x86_64/windows/sioctl.sys
  arch.name: AMD64 reloc classes: [('DllImport', 15), ('IMAGE_REL_BASED_ABSOLUTE', 1), ('IMAGE_REL_BASED_DIR64', 13)]

ALL_RELOCATIONS was keyed on "arm", "mips" and "RISCV", which archinfo does not
produce, and had no AARCH64 entry, so get_relocation returned None for every type
on those architectures and the whole .reloc directory was discarded. Key the table
on arch.name instead.

That alone is not safe. PEReloc.value falls through to self.resolvedby.rebased_addr,
which is None for a base relocation, so making the ARM table reachable turns every
IMAGE_REL_BASED_THUMB_MOV32 into an AttributeError during loading. Implement that
relocation, and let the base class skip a recognised but unimplemented type with a
warning rather than raising.
@zardus
zardus force-pushed the feature/fix-cle-pe-reloc-arches branch from 988c008 to 04d5874 Compare September 4, 2026 15:45
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.

PE base relocations are dropped on every architecture but x86 and x86-64: the relocation tables are keyed on names archinfo does not produce

2 participants