Skip to content

Load partial-memory minidumps - #715

Open
zardus wants to merge 2 commits into
masterfrom
feature/minidump-partial
Open

Load partial-memory minidumps#715
zardus wants to merge 2 commits into
masterfrom
feature/minidump-partial

Conversation

@zardus

@zardus zardus commented Aug 9, 2026

Copy link
Copy Markdown
Member

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

A minidump only has to capture the memory ranges its writer chose, and cle
assumed otherwise. A module with no captured range at its base address raised
CLEInvalidBinaryError, reading a thread's segment base assumed its TEB was in
the file, and module sections were plain Section objects, whose permission
properties raise NotImplementedError, so CFGFast crashed on every minidump.

Modules now map whatever part of their image the dump holds, thread registers
survive a missing TEB, and a section carries its captured size separately from
the image size.

The regression test loads a Breakpad crash dump that captures only the faulting
instruction and the two thread stacks, a fixture added by angr/binaries#175, and
the Windows and macOS jobs check out binaries master, so the checks here stay
red until that merges.

Validation: #715 (comment)

sync: angr/binaries#175

A minidump only has to capture the memory ranges its writer selected, and cle
assumed the opposite in three places, so a dump smaller than a full-memory one
either failed to load or broke a consumer.

Modules had to have a captured range starting exactly at their base address,
or loading raised CLEInvalidBinaryError. A module now maps whatever part of
its image the dump holds: a writer emits one range per run of pages sharing a
protection and stores adjacent ranges adjacently, so a fully captured module
is still a single file range, while a module with nothing captured gets a
section with no file bytes so an analysis can still attribute an address to
it.

Reading a thread's fs or gs segment base means reading its TEB, which a small
dump usually leaves out. thread_registers() now omits that register instead of
raising KeyError, and the TLS thread manager skips a thread whose TEB is
missing with a warning instead of propagating the KeyError out of cle.Loader().

Module sections were also plain Section instances, whose permission properties
raise NotImplementedError, so anything that inspected them crashed on every
minidump; angr's CFGFast does. They are now MinidumpSection instances that
report read, write and execute, matching what the backend reports for its
memory segments, and that carry the captured size separately from the image
size.
@zardus

zardus commented Aug 9, 2026

Copy link
Copy Markdown
Member Author

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Validation record for head 17c659ae00332e95b0fea01c15193ca16eb01b53 against
baseline b58ea02a446106647cdaae32bdf91b7062404cc1, with angr/binaries at
33e2712fec2f2f6a4642ee2c5011799a1de5ca42 (angr/binaries#175).

  • Focused: python -m pytest tests/test_minidump.py — 2 passed
  • Regression: with cle/backends/minidump/__init__.py and cle/backends/tls/minidump_tls.py reverted to the baseline, test_minidump fails with NotImplementedError from Section.is_readable at cle/backends/region.py:176 and test_partial_minidump fails with CLEInvalidBinaryError: Missing segment for loaded module: c:\test_app.exe at cle/backends/minidump/__init__.py:71. Reverting only cle/backends/tls/minidump_tls.py leaves test_partial_minidump failing with KeyError: 'fs' at cle/backends/tls/minidump_tls.py:33, so the fixture reaches the TEB handling too. Both tests pass on head
  • Pre-commit: every configured hook over all files — 22 passed, 2 skipped (no matching files)

The measurements below are from 3dc857289b39c5a721ca699bf066b02df5a2c0fd, which differs from head only in an assertion that now names pefile's IMAGE_DOS_SIGNATURE instead of spelling the literal:

  • Full suite: python -m pytest (cle, CPython 3.12.13, Linux) — 203 passed, 9 skipped
  • Lint: pylint against the merge base — cle/backends/minidump/__init__.py 9.81 -> 9.84, cle/backends/tls/minidump_tls.py 8.89 -> 9.12, tests/test_minidump.py 10.00 -> 10.00
  • Type: pyright badness (errors * 10 + warnings, over line count) — 0.160 -> 0.121, 0.256 -> 0.204, 0.000 -> 0.000

End to end with angr 9.3.3.dev0, angr.Project(dump, auto_load_libs=False).analyses.CFGFast():

Dump Baseline Head
tests/x86/windows/jusched_x86.dmp NotImplementedError from Section.is_readable loads, 1969 CFG nodes
tests/x86/windows/partial/minidump2.dmp CLEInvalidBinaryError at load loads, 0 CFG nodes (no module image is captured)

Section file ranges, checked against the raw dump bytes:

  • In jusched_x86.dmp only the first page of kernel32.dll is captured. On baseline kernel32.addr_to_offset(0x74f82000) returns file offset 0x1149e8a, which holds bytes belonging to an unrelated memory range; on head it returns None.
  • 29 of that dump's 30 modules are captured whole, each split into four to six ranges that are adjacent in memory and in the file. Their sections report filesize == memsize, unchanged from baseline; for example jusched.exe is 0x92000 at file offset 0x7e8a.
  • Every section's declared file range in both dumps matches the dump's own bytes at its first and last sixteen bytes, and none extends past end of file (30 sections in jusched_x86.dmp, 13 in minidump2.dmp).

Caveats:

  • cle's own test matrix (windows-2022, macos-15) checks out angr/binaries at master with actions/checkout, which the sync: directive does not affect, so those two jobs cannot see the new fixture until Add a partial-memory Breakpad minidump fixture binaries#175 merges. Both report 1 failed, 202 passed, 9 skipped, the failure being CLEFileNotFoundError on minidump2.dmp. The shared angr-ci job does resolve the sibling branch: its build fetched refs/pull/175/head for binaries, and Build, Lint, Typecheck, all ten Test workers and Decompiler Snapshot Testing pass.
  • The 9 skips are the TODO-marked cases in tests/test_macho_bindinghelper.py, unrelated to this change and skipped on master too.
  • Both fixtures are x86. The AMD64 and WoW64 paths change only in omitting a register when the TEB is absent; no minidump fixture exercises them.

Corpus measurement of the open queue, 2026-08-15 — this record understates the change by about twenty-fold

Correcting the record upward, which is as much of a correction as the other direction. The open pull-request queue was scored against 733 objects drawn from a sweep's own failing units (35 error classes, 49 architectures, 16 containers), with each repository's current master as the baseline rather than the revisions the sweep pinned. Each object is loaded with auto_load_libs=False, use_sim_procedures=False and then run through CFGFast(normalize=True, data_references=False, resolve_indirect_jumps=True, force_complete_scan=False) with a 120-second timeout.

This PR was filed against the failure it is named for — CLEInvalidBinaryError from the minidump backend, 96 corpus units, 94 of them minidumps. 14 of 16 measured objects clear; the two that do not are CART containers that stop on a different CLEError.

The larger effect is in a class this PR was never associated with. The NotImplementedError permission class holds 6,565 corpus units and is two unrelated halves: TE/UEFI images, which already load on master, and minidumps, which do not, because module sections are plain Section objects whose is_readable, is_writable and is_executable raise. MinidumpSection supplies exactly those three properties. 12 of the 12 measured minidumps in that class complete CFGFast with this branch — 6 x86 and 6 x86-64 — recovering 107 to 200 blocks each, while all 12 measured TE images are unchanged because they never failed.

Weighting each (class, architecture, container) cell by the corpus units it holds:

Class Cell Sample Corpus units cleared
NotImplementedError permissions x86 minidump 6 of 6 1,035
NotImplementedError permissions x86-64 minidump 6 of 6 1,071
NotImplementedError permissions x86 / x86-64 TE 0 of 12 0 (already load)
CLEInvalidBinaryError x86-64 minidump 7 of 7 82
CLEInvalidBinaryError x86 minidump 7 of 7 12
CLEInvalidBinaryError x86-64 CART 0 of 2 0
2,200

About 2,200 corpus units, from this PR alone — roughly twenty times what the class in its title accounts for, and the largest upward correction in the queue measurement. It is not the largest absolute figure in the queue: angr#6794 clears about 14,946 units on its own, cle#728 about 3,441 and angr#6805 about 3,804. The point is only that this record, as written, described about a twentieth of what the change does.

One detail worth recording for anyone re-running this: the permission class was drawn on is_readable, but on current master these objects reach the same defect at is_executable first. It is the same missing trio of properties, only a different first caller.

The corpus is private, so its objects are named by architecture, container and digest rather than by path.

@angr-bot

angr-bot commented Aug 9, 2026

Copy link
Copy Markdown
Member

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

The assertion that a fully captured module maps its image base compared the
loaded bytes against a b"MZ" literal. Use pefile's own IMAGE_DOS_SIGNATURE,
which says what the check means and keeps a header magic literal out of a test
file, where it otherwise reads as a hand-assembled container.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
zardus added a commit that referenced this pull request Aug 10, 2026
A minidump's stream directory lists only the streams its writer chose to emit;
the header and the directory are the only mandatory parts of the file. The
backend read four of them and handled a missing one three different ways: the
memory list was required, the module and thread lists were dereferenced
unguarded and produced an AttributeError from the middle of a loop, and only the
system information stream raised a considered error.

A dump with no memory list now loads with no memory, and one with no module or
thread list loads with no sections or with no threads. The module list is read on
the architecture path as well, where a 64-bit dump without one failed before any
of that.

MinidumpMissingStreamError, which the missing system information stream still
raises when no architecture was passed either, now derives from CLEError and
hands its explanation to the base class. It was invisible to except CLEError, and
str() on it was empty because the explanation only ever reached an attribute.

A module whose image the dump did not capture is still rejected twenty lines
further down; #715 covers that half.
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