Skip to content

ELF: Stop unloaded sections from claiming a relocatable object's addresses - #739

Open
zardus wants to merge 1 commit into
masterfrom
feature/fix-cle-rel-note-address
Open

ELF: Stop unloaded sections from claiming a relocatable object's addresses#739
zardus wants to merge 1 commit into
masterfrom
feature/fix-cle-rel-note-address

Conversation

@zardus

@zardus zardus commented Aug 13, 2026

Copy link
Copy Markdown
Member

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

A relocatable object carries no addresses of its own, so cle lays its allocated sections out itself. A section whose type is in _NON_ALLOCATED_SECTION_NAMES takes an address from that layout without reserving space, and no backer is ever added for it, so it sits on top of the section that follows: on a Linux kernel module the allocated .note.Linux covers the first 0x30 bytes of .text.

Regions documents that its members do not overlap and bisects on their end addresses, so one such section makes find_section_containing() return None for most of the object, or a stale section depending on what was looked up before. CFGFast discards a block whose section is not executable, and JumpTableResolver needs the table inside a mapped section when there are no segments, so an ARM module loses both the code under the note and its jump tables.

These sections now report that they occupy no memory, which is what the constant already claims about them. Nothing else moves: every address, loaded byte, relocation and symbol stays as it was.

The regression loads tests/x86_64/switch_default_abort.o, already on angr/binaries master, where .note.gnu.property covers .eh_frame; there is no companion binaries PR.

Validation: #739 (comment)

A relocatable object carries no addresses of its own, so __register_sections
lays its allocated sections out itself. A section whose type is in
_NON_ALLOCATED_SECTION_NAMES took an address from that layout without
reserving any space, and no backer is ever added for it, so it landed on top
of the section that follows. On a Linux kernel module the allocated
.note.Linux therefore covers the first 0x30 bytes of .text, and on MIPS
.reginfo shares an address with .MIPS.abiflags.

Regions documents that its members do not overlap and finds one by bisecting
on their end addresses, so a single overlapping section leaves that list
unsorted by the search key and the lookup misses regions that are really
there. find_section_containing() then returns None for most of the object's
mapped range, or a stale section, depending on what was looked up before it,
because Backend caches the previous hit. CFGFast reads it twice: a block
whose section is not executable is discarded, and JumpTableResolver requires
the table to be inside a mapped section when the object has no segments, so
an ARM module loses both the code the note covers and every jump table.

These sections now report that they occupy no memory, which is what the
constant already claims about them. Every section address, every loaded
byte, every relocation and every symbol stays as it was; the sections only
leave the address map, the way a section without SHF_ALLOC already does.
@zardus

zardus commented Aug 13, 2026

Copy link
Copy Markdown
Member Author

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Validation record for head eb63f1e26700e428693de40b33f706c6589602f7 against baseline 45c6509c753d07f740099035cd41f7f473dc6f31.

  • Regression: python -m pytest tests/test_regions.py::TestRelocatableSections -q — fails on baseline (.note.gnu.property overlaps .eh_frame at 0x400090, and .note.gnu.property.occupies_memory is True), passes on head
  • Focused: python -m pytest tests -q — 236 collected, 227 passed, 9 skipped, 0 failed
  • Lint/type: pre-commit run --all-files clean; ruff and pyright changed-file comparison against the baseline shows no regression on any of the three files
  • Workspace gate: complete local gate over cle plus the workspace's own checks — passed
  • Hosted CI: every check terminal and green, including ci / Typecheck, ci / Lint and all ten ci / Test shards
  • dec-snapshots comparison for this PR reports identical, no file changed

Loader comparison over 1710 corpus objects spanning 55 architectures and 10 container formats, and separately over 39 relocatable ELF objects that carry an allocated note or .reginfo section. Each object is loaded on both revisions and reduced to a digest of every object's base, entry, bounds, section and segment table, and of every byte the loader maps.

Measure 1710-object sample 39 affected objects
Loaded / errored 659 / 1051 on both 39 / 0 on both
Mapped bytes differing 0 0
Addresses, sizes, permissions, entry or bounds differing 0 0
Relocation or symbol counts differing 0 0
Objects whose occupies_memory flags change 14 39

CFGFast (normalize=True, resolve_indirect_jumps=True, data_references=False, force_complete_scan=False) over the same 1710 objects: output byte-identical on every object, no new error and no new timeout.

Over the 39 affected objects:

Measure Baseline Head
Errors / timeouts 0 0
Mapped addresses find_section_containing() cannot resolve 95434 of 160641 0 of 160641
Recovered blocks 17657 17663
Function entries 2603 2594
Recovered instruction bytes lost against baseline 0

Six objects change; every difference:

  • d08e00d2a2c98acdc04cce7e336383f33eb470dd2fdba1e26bc434dd593b6c8a (ARM, ET_REL): .text is 4 bytes and .note.Linux covered .init.text and part of .exit.text, so CFGFast dropped that code as non-executable. Recovery goes from 3 blocks and 6 instructions to 5 blocks and 17 instructions, and .init.text at 0x400004 and .exit.text at 0x40002c become function entries in place of the mid-function 0x400030.
  • 19475c90d964f3b3c46b1e7cf7261ee5a1cebb2fc14f92bd539f1a9778516a1e and 211ef307cbe4f8c2d6e018fdd9fb8b1e6d6aa4e448ddd6a0c4f561cb548dd9c7 (ARM, ET_REL): three jump tables now pass the mapped-section check, so 16 blocks that were orphan function entries become blocks of the function that switches to them. All 16 are still in the graph. One UnresolvableJumpTarget disappears where a table resolves, and one appears on the second object where newly reached code has its own unresolved jump.
  • 05c76d5f6d2b943cf9b13f2a3b67b9060fe3ec5b7ad3e27f2a6e7dfd9724ce69, 892e87957a373e133edd96e2985109dc1da43b48aea3e8fbfa8701c971604183, a3e588c7f2d21984946ce8df1f78ed3ea25d7fa0493d23160b69dbd2cdca3c1a (MIPS, ET_REL): one extern function each (module_put, kvfree_call_rcu, crypto_shash_final). _addrs_belong_to_same_section can now find the source section of a j to an external symbol, so the tail call is recognised as leaving the function. The block set and every block boundary are unchanged on all three.

Net across the 39: 18 function entries removed, 9 added, 0 bytes of recovered code lost, 44 gained.

The CFGFast comparison and the loader comparison were run in opposite revision orders and agree exactly on all 39 objects in both revisions, so none of these differences is nondeterminism.

Caveats: the corpus objects are named only by architecture, container and sha256 because the dataset is not public; tests/x86_64/switch_default_abort.o reproduces the loader half of the bug from angr/binaries master. This covers only the layout cle invents for a relocatable object. A linked ELF whose .tbss overlaps the section after it still produces overlapping mapped sections, which Regions handles by luck rather than by design; that is untouched here.

@angr-bot

Copy link
Copy Markdown
Member

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

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