Skip to content

PE: Record the import hint/name entries, not the span between them - #834

Open
zardus wants to merge 1 commit into
masterfrom
fix/pe-import-hint-name-spans
Open

zardus wants to merge 1 commit into
masterfrom
fix/pe-import-hint-name-spans

Conversation

@zardus

@zardus zardus commented Sep 8, 2026 •

Copy link
Copy Markdown
Member

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Problem

Two PE files angr/binaries already tracks have their entry point covered by a
cle metadata region, so CFGFast treats the entry as data and decodes nothing
there. The entry sits in an executable section in both cases:

tests/x86_64/windows/Project1.vmp.exe
    entry 0x706467   section .vmp1    exec=True
    covering region: 0x63b512 +0x1f65dc (2,057,692 bytes) import-hint-name-table
tests/x86_64/windows/7107ab06446ce4a51226196453066e7d361972364ad1543fe8a3a03a957e1bd5
    entry 0x142a401b4 section .tempest exec=True
    covering region: 0x141c1e670 +0xe4cfcc (14,995,404 bytes) import-hint-name-table

Every address inside such a region is refused the same way, so on an image where
the region covers the code the analysis can come back with nothing at all.

Root cause

PE._meta_imports in cle/backends/pe/pe.py keeps only the lowest and highest
hint/name RVA it sees and records the whole range as one region:

if hn_min is not None and hn_max is not None:
    sub_regions.append(
        StringBlob(vaddr=base + hn_min, size=hn_max - hn_min, ...)
    )

Nothing in the PE format requires the IMAGE_IMPORT_BY_NAME entries to be
contiguous or in order — each ILT and IAT slot points at one independently. On
Project1.vmp.exe the entries themselves
total 3,360 bytes inside that 2,057,692-byte span, so 99.8% of what cle reports
as an import string table is whatever else happens to lie between the first entry
and the last, including the entry point.

angr's _flatten_regions emits the sub-region, CFGFast._process_metadata_regions
occupies it in _seg_list, and the lift distance at any covered address is then
0, so _lift raises SimTranslationError and the address is dropped.

Fix

Record each hint/name entry's own extent and coalesce the ones that touch, so the
region describes the bytes cle actually parsed. cle owns this: it is the producer,
and a consumer cannot tell a real string table from a span that merely contains
one.

Project1.vmp.exe   hint/name regions 1 -> 179, bytes 2,057,692 -> 3,360
7107ab06...bd5     hint/name regions 1 ->  21, bytes 14,995,404 ->   382

Coalescing runs rather than emitting one blob per import is deliberate: a normal
contiguous table still comes out as a single region, so 37 of the 99 PE files in
angr/binaries that have an import hint/name table keep exactly the region they
had. It follows the same shape as the region merging in cle/backends/ihex.py
and cle/backends/srec.py, which merge only exactly abutting regions where this
also merges overlapping ones.

Two other PE producers can also report a region larger than the data it
describes — the load-config sub-tables, built from header fields nothing
validates, and the resource directory's declared Size — but neither is this
table and each wants its own change with its own reproducer.

Testing

tests/test_pe_meta_regions.py::TestPEScatteredHintNameTable::test_scattered_hint_name_entries
loads tests/x86_64/windows/Project1.vmp.exe and asserts 179 hint/name regions
totalling 3,360 bytes with none covering obj.entry. On master it fails with
assert 1 == 179 and prints the single 2,057,692-byte blob. The rest of cle's
suite is unaffected.

The checkable effect on angr: running CFGFast restricted to the entry's own
neighbourhood, _seg_list at the entry goes from string to code on both
files and the entry is recovered as a function, where in that same window before
the change neither file produced a function or a node. These are packed
binaries, and an unbounded CFGFast over either of them was not run to
completion, so this makes no claim about whole-binary function counts.

Validation: #834 (comment)

session: sharpen

_meta_imports took the minimum and maximum hint/name RVA over every
import and recorded the whole range as one StringBlob. Nothing in the
format requires those entries to be contiguous or ordered, so on an
image whose imports point at scattered entries the blob covers
megabytes of unrelated address space, including code.

That reaches angr: CFGFast._process_metadata_regions marks every
region cle reports as data, so where the blob covers an address the
lift distance is 0, _lift raises SimTranslationError and the address
is refused. On tests/x86_64/windows/Project1.vmp.exe the blob is
2,057,692 bytes and covers the entry at 0x706467; on
tests/x86_64/windows/7107ab06...bd5 it is 14,995,404 bytes and covers
the entry at 0x142a401b4.

Collect each entry's own extent instead and coalesce the ones that
touch, in the same shape as the region merging in ihex and srec. A
normal contiguous hint/name table still comes out as one region.

Over the 106 PE files angr/binaries tracks, 62 change and 44 do not,
no region of another sort moves, and no byte is covered afterwards
that was not covered before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@zardus

zardus commented Sep 8, 2026 •

Copy link
Copy Markdown
Member Author

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Validation record for head 86c82bfcbf8ddc0232896db5cbc4875890cec0c5 against baseline 0e77ade3c39a3cee05f65051e57955675e1ac21b.

Measured against angr a34418ad568b678e66fe47ce50a9f431ab0ff801 and angr/binaries fc07821c89535534979b02760e7e1bfc35faf690. The workspace gate below ran later, against angr 23b470d9f845e005286c498f70d1e108502fc1c5 and pyvex 2324a19e8.

  • Regression: pytest --import-mode=append -q tests/test_pe_meta_regions.py — on the baseline 1 failed, 19 passed with assert 1 == 179 and [<StringBlob IMPORT_HINT_NAME_TABLE @ 0x63b512, 2057692 bytes>]; on the head 20 passed
  • Focused: pytest --import-mode=append -q tests/test_pe_meta_regions.py tests/test_pe.py tests/test_pe_delay_imports.py tests/test_pe_coff_symbols.py — 39 passed
  • Full suite: pytest --import-mode=append -q tests/ — 262 passed, 9 skipped on the head; on the baseline 1 failed, 261 passed, 9 skipped, the one failure being the new test
  • Lint: pre-commit run --files cle/backends/pe/pe.py tests/test_pe_meta_regions.py — every hook passes and no file is modified (ruff, black, pyupgrade, check-ast, python-use-type-annotations and the rest)
  • Merge-base lint and type check against the base revision — pylint 10.00 to 10.00 on both changed files, pyright 52 to 52 errors on cle/backends/pe/pe.py and 0 to 0 on tests/test_pe_meta_regions.py. The pyright counts were taken in an environment without the CI image's sortedcontainers-stubs, so they are local numbers; both sides were measured the same way and neither moved.
  • Workspace gate: ./feature.sh test pe-meta-regions, with both cle and angr built from the branch's collection. Ten suites ran — workspace, test inputs, test packages, pre-commit, feature build, mono, pysoot, cle, angr and angr's Rust tests. cle: 262 passed, 9 skipped. angr: 2,937 passed, 21 failed, 47 skipped, 2 xfailed, 286 subtests. Not covered, because the feature does not adopt them: archinfo, pypcode, pyvex, angr-management.
  • The 21 angr failures are not caused by this change. They are tests/engines/vex/test_avx512.py (11 tests), test_vex.py::test_cmplesd and ::test_cmpltsd, test_callable.py::test_manyfloatsum_x86_64 and its symbolic twin, test_ops.py::{test_irop_catevenlanes,test_irop_mulhi,test_irop_perm,test_saturating_packing}, and test_sqrt.py::{test_sqrt_concrete,test_sqrt_symbolic}. Re-running those five files with cle at the baseline 0e77ade3, everything else held identical, gives the same 21 failures, 58 passed — the set of failures unique to the branch is empty. angr's own master CI fails the identical 21 on 23b470d9f (run 34216086807), a tree with none of this change in it; master went red at that commit's parent b485d2c59, the AVX-512 lifting merge, and was green at 14ed7c5b8 before it (run 34208506804). The same 21 are why this pull request's checks are red; there is a separate comment with that detail.

The before/after at the entry point of the two reproducers is in the output comment on this pull request, not here.

Regression control over all 106 PE files angr/binaries tracks, dumping every meta region on both revisions and diffing them:

Measure Baseline Head
Files whose region set changed — 62
Files unchanged — 44
Files where a region of another sort changed — 0
Files with any byte covered on the head but not the baseline — 0
Files where the hint/name regions vanished entirely — 0
Files whose entry sits inside a meta region 2 0
Largest single region across all 106 14,995,404 10,759,184
Total bytes marked as metadata across all 106 33,165,866 14,803,251
  • The "0 bytes newly covered" row is exact rather than sampled: the head's covered byte set is subtracted from the baseline's for every file. Coalescing can only produce a subset of the old span, and the measurement confirms it, so nothing is newly treated as data.
  • 99 of the 106 files have an import hint/name table at all. 37 of those keep exactly the single region they had, which is the contiguous-table case; the other 7 unchanged files have no hint/name region to change.
  • Largest shrinks: Project1.vmp.exe 2,057,692 to 3,360 bytes, 7107ab06...bd5 14,995,404 to 382, and tests/x86_64/windows/444a401b900eb825f216e95111dcb6ef94b01a81fc7b88a48599867db8c50365.sys 1,315,908 to 306.
  • The largest region left on the head is a 10,759,184-byte resource directory in tests/x86_64/windows/1179ea5ceedaa1ae4014666f42a20e976701d61fe52f1e126fc78066fddab4b7.exe, which lies inside its image and is genuine resource data.

Caveats: an unbounded CFGFast on the two packed reproducers was not run to completion, so no whole-binary function count is claimed for either file. Exactly one region still runs past its mapped image on both revisions, the resource directory of tests/i386/windows/9f2ef84bde1e4ef445708cc5a605a09226363d502b1f5b5bf4a1cfc6dd5fc41e; it does not cover that file's entry and belongs to a different producer.

@zardus

zardus commented Sep 8, 2026

Copy link
Copy Markdown
Member Author

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

What CFGFast does at the entry point of tests/x86_64/windows/Project1.vmp.exe
(entry 0x706467) and
tests/x86_64/windows/7107ab06446ce4a51226196453066e7d361972364ad1543fe8a3a03a957e1bd5
(entry 0x142a401b4), before and after this change.

The run is CFGFast(regions=[(entry, entry + 0x400)], normalize=False), restricted to the
entry's own neighbourhood so these packed binaries finish in seconds.
_process_metadata_regions runs unconditionally in _pre_analysis, so the marking under
test is exercised on both sides. Nothing here is a whole-binary function count: an
unbounded CFGFast on either file was not run to completion.

Before — the import hint/name blob covers the entry, _seg_list calls it a string, and
neither file yields a function or a node in that window:

cle master 0e77ade
Project1.vmp.exe
  entry                  0x706467
  flattened meta regions 16
  largest region         2057692 bytes
  region covering entry  0x63b512 +2057692 string
  _seg_list at entry     string
  entry is a function    False
  functions / nodes      0 / 0

7107ab06446ce4a51226196453066e7d361972364ad1543fe8a3a03a957e1bd5
  entry                  0x142a401b4
  flattened meta regions 38
  largest region         14995404 bytes
  region covering entry  0x141c1e670 +14995404 string
  _seg_list at entry     string
  entry is a function    False
  functions / nodes      0 / 0

After — no metadata region covers the entry, _seg_list calls it code, and the entry is
recovered as a function on both files:

with this change
Project1.vmp.exe
  entry                  0x706467
  flattened meta regions 194
  largest region         744 bytes
  region covering entry  none
  _seg_list at entry     code
  entry is a function    True
  functions / nodes      22 / 38

7107ab06446ce4a51226196453066e7d361972364ad1543fe8a3a03a957e1bd5
  entry                  0x142a401b4
  flattened meta regions 58
  largest region         469 bytes
  region covering entry  none
  _seg_list at entry     code
  entry is a function    True
  functions / nodes      22 / 71

"Flattened meta regions" is what angr.analyses.cfg.meta_structs.get_data_regions_from_meta_regions
returns for the whole loader, and "largest region" is the largest of those.

@angr-bot

angr-bot commented Sep 8, 2026

Copy link
Copy Markdown
Member

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

@zardus

zardus commented Sep 8, 2026

Copy link
Copy Markdown
Member Author

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Why ci / Test (6) and ci / Test (8) are red on this pull request, measured 2026-09-08.

They are not caused by this change. Both shards fail angr's tests, which cle's CI also runs, and the same 21 tests fail on angr master with none of this change present: run 34216086807 on 23b470d9f. Master went red at that commit's parent b485d2c59, the AVX-512 lifting merge (angr/angr#7120, alongside angr/pyvex#584), and was green at 14ed7c5b8 before it (run 34208506804).

The cause is that those two merged while their archinfo counterpart, angr/archinfo#384, had not. As of this writing #384 is open. libVEX now emits AVX-512 guest state that archinfo's amd64 register file does not describe, so the AVX-512 tests raise KeyError: 'zmm1', and the amd64 VEX guest offsets shift underneath the older table, which is what the sqrt, packing and float tests are reacting to.

This pull request's run is simply the first cle CI run to start after that merge. The merge landed at 10:30Z, this run started at 12:56Z, and no cle run started in between; the previous cle run, at 10:05Z, predates the merge and was green.

Re-running these two checks once angr/archinfo#384 has landed should clear them. The change in this pull request is confined to cle/backends/pe/pe.py and one test file, and touches nothing these tests exercise.

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