Skip to content

PE: bound resource metadata to its mapped section - #869

Open
zardus wants to merge 1 commit into
masterfrom
feature/fix-c-723
Open

zardus wants to merge 1 commit into
masterfrom
feature/fix-c-723

Conversation

@zardus

@zardus zardus commented Oct 8, 2026 •

Copy link
Copy Markdown
Member

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Problem

A PE resource data directory can declare a size that extends far beyond the section containing it. CLE currently publishes that full declared range as metadata, so CFGFast treats later mapped code as data. Two unavailable corpus samples consequently produced only one function and did not recover their entry points once their non-DEP sections were made available for analysis.

Related work in #862 clips metadata to the image bounds, but that still leaves both affected resource ranges covering their entry points. The useful bound here is the mapped section that contains the resource directory start.

Root cause

PE._meta_resources copied IMAGE_DIRECTORY_ENTRY_RESOURCE.Size directly into a DataDirectory. The size is input-controlled and was not limited by the containing section's mapped extent.

Fix

Compute each PE section's mapped size with the backend's existing _mapped_size rule. When the resource directory starts in a mapped section, clip its size at that section's end. A regression test uses the existing public PE fixture whose resource directory declares 0x7400 bytes inside a 0x1000 mapped resource section.

Testing

  • The focused CLE regression passes, and the full CLE suite reports 334 passed and 9 skipped.
  • All 14 workspace gate suites pass, including 4,703 angr tests and 737 angr-management tests.
  • Merge-base pylint remains 10.00 for both changed files; pyright error counts do not increase.
  • With the same no-DEP section behavior applied to both arms, the two unavailable samples improve from 1 function/1 block with no entry-point function to 1,021/3,439 and 1,098/3,665 with both entry points recovered.
  • A deterministic 12-object x86 PE resource-directory regression cohort has identical function counts, block counts, and entry-point recovery before and after; worse: 0.

The CFG recovery impact is coordinated with #867; the metadata boundary fixed here is independent of merge order.

Validation: #869 (comment)

sync: #867

session: sharpen

@zardus

zardus commented Oct 8, 2026

Copy link
Copy Markdown
Member Author

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Complete resource-directory metadata for tests/i386/windows/9f2ef84bde1e4ef445708cc5a605a09226363d502b1f5b5bf4a1cfc6dd5fc41e, before and after this change.

Before — the declared resource range crosses the mapped section boundary:

cle master 752bce5
resource start:         0x409000
resource size:          0x7400
resource end:           0x410400
containing section end: 0x40a000

After — the resource range ends at the containing section boundary:

cle 9d20c44
resource start:         0x409000
resource size:          0x1000
resource end:           0x40a000
containing section end: 0x40a000

@zardus

zardus commented Oct 8, 2026 •

Copy link
Copy Markdown
Member Author

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Validation record for head 9d20c44bea8ddccc0ddd685b08539cfbeb01dd7a against baseline 752bce544a058944b6bb2c49998fc4c22799ce75.

The affected binaries are unavailable for redistribution. They are identified by SHA-256:

  • 2be97017e1d91ad886ee7566f5f92098a370d7074cdf2b9c2d36d9026521470f
  • f65fd9d106db0fbe286542783c6a8bc19fadf3378c42e96fb88c059736605fe5

The base and candidate used clean, separately rooted Nix environments with identical non-CLE component heads. Set CLEAN_BASE_TREE and CLEAN_CANDIDATE_TREE to the frozen clean source trees, and set PRIVATE_ROOTS to a private writable roots directory. The exact public-safe environment command forms recorded in the evidence manifest are:

./nix/env.sh --feature fix-c-723 --source "cle=$CLEAN_BASE_TREE" --head cle=752bce544a058944b6bb2c49998fc4c22799ce75 --roots-dir "$PRIVATE_ROOTS/base" --clean env
./nix/env.sh --feature fix-c-723 --source "cle=$CLEAN_CANDIDATE_TREE" --head cle=9d20c44bea8ddccc0ddd685b08539cfbeb01dd7a --roots-dir "$PRIVATE_ROOTS/candidate" --clean env

For the measurement, set BASE_ENV and CANDIDATE_ENV to those environment roots, REPOS_ROOT to the feature collection's repository root, PRIVATE_SCRATCH to a private writable directory, and OBJECT and EXPECTED_SHA256 for one unavailable input. The following exact evidence-manifest command block verifies the bytes, environment, and imports, applies the sweep's controlled no-DEP behavior to both arms, measures resource geometry and CFGFast, releases its gate slot, and contains no unavailable object path:

./manager fix gate-slot acquire fix-c-723 --pid $$ || exit $?
unset PYTHONPATH
RTDB_BASE=$(mktemp -d "$PRIVATE_SCRATCH/c723-rtdb.XXXXXX")
cleanup_measurement() { test -n "$RTDB_BASE" && rm -r -- "$RTDB_BASE"; ./manager fix gate-slot release fix-c-723; }
trap cleanup_measurement EXIT
for ENV in "$BASE_ENV" "$CANDIDATE_ENV"; do
  env -u PYTHONPATH \
    ANGR_REPOS_ROOT="$REPOS_ROOT" \
    ANGR_GATE_ENV="$ENV" \
    RTDB_BASE="$RTDB_BASE" \
    PYTHONHASHSEED=0 \
    PYTHONSAFEPATH=1 \
    OBJECT="$OBJECT" \
    EXPECTED_SHA256="$EXPECTED_SHA256" \
    "$ENV/bin/python" -P - <<'PY'
import hashlib
import json
import os
import sys
from pathlib import Path

import angr
import cle
from cle.backends.pe.regions import PESection
from cle.structs import MemRegionSort

env_root = Path(os.environ["ANGR_GATE_ENV"]).resolve()
repos_root = Path(os.environ["ANGR_REPOS_ROOT"]).resolve()
rtdb_root = Path(os.environ["RTDB_BASE"]).resolve()
assert Path(sys.prefix).resolve() == env_root
assert Path(angr.__file__).is_relative_to(env_root)
assert Path(cle.__file__).is_relative_to(env_root)
assert repos_root.is_dir() and rtdb_root.is_dir()
assert "PYTHONPATH" not in os.environ
with open(os.environ["OBJECT"], "rb") as stream:
    assert hashlib.file_digest(stream, "sha256").hexdigest() == os.environ["EXPECTED_SHA256"]

original_executable = PESection.is_executable.fget
PESection.is_executable = property(
    lambda section: original_executable(section)
    or getattr(section, "executable_without_dep", False)
)
original_register = cle.backends.pe.PE._register_sections

def register_sections(pe):
    original_register(pe)
    for section in pe.sections:
        section.executable_without_dep = False
    if pe.supports_nx:
        return
    entry_section = pe.find_section_containing(pe._entry)
    if entry_section is None or entry_section.is_executable:
        return
    for section in pe.sections:
        if not section.only_contains_uninitialized_data:
            section.executable_without_dep = True

cle.backends.pe.PE._register_sections = register_sections
project = angr.Project(os.environ["OBJECT"], auto_load_libs=False)
obj = project.loader.main_object
resource = next(
    region for region in obj.meta_regions
    if region.sort == MemRegionSort.RESOURCE_DIRECTORY
)
section = obj.find_section_containing(resource.vaddr)
cfg = project.analyses.CFGFast(
    normalize=True,
    data_references=False,
    resolve_indirect_jumps=True,
    show_progressbar=False,
)
print(json.dumps({
    "cle": cle.__file__,
    "resource": [resource.vaddr, resource.vaddr + resource.size],
    "resource_section": [section.vaddr, section.vaddr + section.memsize],
    "resource_covers_entry": resource.vaddr <= obj.entry < resource.vaddr + resource.size,
    "functions": len(cfg.functions),
    "blocks": len(list(cfg.model.nodes())),
    "entry_is_function": obj.entry in cfg.functions,
}, sort_keys=True))
PY
done

The embedded register_sections override is deliberate: it matches the coordinated change in #867. With that controlled behavior, the measurements were:

  • 2be970…470f: resource metadata ended at 0x48f000 before and at its containing section's 0x44a000 end after. CFGFast changed from 1 function/1 block with no entry-point function to 1,021 functions/3,439 blocks with the entry point recovered.
  • f65fd9…fe5: resource metadata ended at 0x489000 before and at its containing section's 0x447000 end after. CFGFast changed from 1 function/1 block with no entry-point function to 1,098 functions/3,665 blocks with the entry point recovered.

Without the no-DEP behavior, the exact base and candidate each recover 0 functions and 0 blocks because section permissions prevent CFGFast from scanning code. That is an independent barrier. This PR directly changes the resource metadata geometry in either case: on the base the metadata covers each entry point, while on the candidate it ends at the containing resource section and no longer covers either entry point.

The same command measured a deterministic regression set of 12 other x86 PE objects with campaign records and nonzero resource directories already contained by their mapped sections. For each affected sample's shard, the set used the first six eligible objects in lexical SHA-256 order. All 12 loaded and completed CFGFast. Every function count, block count, and entry-point recovery state was identical before and after; worse: 0.

The public regression fixture was tested with:

cd features/fix-c-723/repos/cle && ../../../../nix/run.sh --feature fix-c-723 -- pytest --import-mode=append -q tests/test_pe_meta_regions.py

Result: 20 passed. The regression verifies that the existing public fixture's resource metadata is clipped from its declared 0x7400 bytes to the containing section's 0x1000 mapped extent.

The finalized collection was built and gated with:

./feature.sh build fix-c-723
./feature.sh test fix-c-723

Every one of the 14 gate suites ran and passed. Notable results were CLE 334 passed/9 skipped, angr 4,703 passed/47 skipped/42 xfailed with 1,206 subtests passed, and angr-management 737 passed. Worktree cleanliness passed.

The merge-base lint/type delta was checked with:

./nix/run.sh --feature fix-c-723 -- python .agents/skills/angr-validate-workspace/scripts/run-ci-diff-checks.py --repository features/fix-c-723/repos/cle

Both changed files remain at pylint 10.00. Pyright errors remain 55 for cle/backends/pe/pe.py and 0 for tests/test_pe_meta_regions.py; no lint or type regression was found.

@angr-bot

angr-bot commented Oct 8, 2026

Copy link
Copy Markdown
Member

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

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