Skip to content

Load minidumps whose writer left out a stream - #725

Open
zardus wants to merge 1 commit into
masterfrom
feature/fix-cle-minidump-streams
Open

Load minidumps whose writer left out a stream#725
zardus wants to merge 1 commit into
masterfrom
feature/fix-cle-minidump-streams

Conversation

@zardus

@zardus zardus commented Aug 10, 2026

Copy link
Copy Markdown
Member

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Problem

A minidump's directory lists only the streams its writer emitted, and the backend could not cope when one was absent. Striking ModuleListStream out of the directory of tests/x86_64/windows/breakpad/tiny-exe-with-cet-xsave.dmp — which is exactly what a reader sees when the writer never collected it:

    RAISED AttributeError: str(e)="'NoneType' object has no attribute 'modules'"
    isinstance(cle.CLEError) = False
      | if any(module.name.endswith("wow64.dll") for module in self._mdf.modules.modules):

The whole load goes with it: the memory the dump did capture, the register state of the threads it did record, and the section table built from its modules. And the one deliberate rejection was unusable — a missing SystemInfoStream raised MinidumpMissingStreamError with str(e)='' and isinstance(cle.CLEError) = False.

Root cause

Minidump.__init__ reaches into stream objects the minidump package leaves as None, at three sites — the wow64.dll scan above, for module in self._mdf.modules.modules: and for thread in self._mdf.threads.threads: — and a fourth treats a dump that captured no memory as invalid outright:

raise MinidumpMissingStreamError("MemoryList", "The memory segments were not defined")

A minidump's only mandatory parts are its header and its stream directory, so each of these fires on a real dump. The error class compounded it: class MinidumpMissingStreamError(Exception) calling super().__init__() with no arguments, so the explanation was stashed on an attribute str() never reached and the exception sat outside the CLEError hierarchy.

Fix

modules and threads are read once and default to [] when the stream is absent, so such a dump loads with no sections or no threads, and a dump with no memory list loads with segments = []. MinidumpMissingStreamError derives from CLEError and passes its explanation to the base class.

--- no ModuleListStream:      LOADED Minidump arch=AMD64 segments=65 threads=['0x5bc'] rip=0x7ff9111e39e4
--- no ThreadListStream:      LOADED Minidump arch=X86 segments=173 sections=30 threads=[]
--- no captured memory:       LOADED Minidump arch=AMD64 segments=0 threads=[]
--- no SystemInfoStream:      RAISED MinidumpMissingStreamError:
    str(e)='SystemInfoStream is missing. The architecture was not specified'
    isinstance(cle.CLEError) = True

A missing SystemInfoStream still raises, because the architecture is the one thing the backend cannot infer, and a module whose image the dump did not capture is still rejected a few lines below.

Testing

tests/test_minidump.py gains a test per absent stream: test_minidump_without_module_list asserts ld.main_object.threads == [0x5BC] on a dump whose module list is gone, test_minidump_without_thread_list pins the 30 sections that survive on tests/x86/windows/jusched_x86.dmp, test_minidump_without_captured_memory pins the empty segment list, and test_minidump_without_system_info pins the message and the CLEError base. Each takes a real dump and strikes a stream out of its directory in a temporary copy; both fixtures are on angr/binaries master. #715 reworks the same function, so whichever lands second needs a rebase.

Validation: #725 (comment)

session: sharpen

@zardus

zardus commented Aug 10, 2026

Copy link
Copy Markdown
Member Author

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Validation record for head 08852723cfedbb25167fa3fdde957d0bb8e60a60 against baseline 46a37333f4f59b0facf8774ee743ebc4cc074e9b. Python 3.12.13, minidump 0.0.24, commands run from the cle checkout with angr/binaries#176 checked out.

  • Regression: python -m pytest tests/test_minidump.py with only cle/backends/minidump/__init__.py reverted to the baseline — 4 failed, 1 passed; with it restored, 5 passed. test_minidump_without_module_list and test_minidump_without_captured_memory fail with AttributeError: 'NoneType' object has no attribute 'modules' at cle/backends/minidump/__init__.py:41, the wow64 lookup on the architecture path; test_minidump_without_thread_list fails with AttributeError: 'NoneType' object has no attribute 'threads' at line 78; test_minidump_without_system_info fails because the MinidumpMissingStreamError raised at line 38 is not a CLEError and pytest.raises(cle.CLEError) does not match it.
  • Focused: python -m pytest tests/test_minidump.py — 5 passed, 0 skipped.
  • Full suite: python -m pytest tests/ — 206 passed, 9 skipped.
  • Lint/type: every configured pre-commit hook passes and rewrites nothing; pylint per changed file 9.81 -> 9.90 for cle/backends/minidump/__init__.py and 10.00 -> 10.00 for tests/test_minidump.py; pyright badness 0.1596 -> 0.1508 and 0.0 -> 0.0, both against the baseline.

The tests get a dump with a missing stream by reading a real one and marking that stream's directory entry UnusedStream, which is what a writer that reserved an entry for a stream it did not collect leaves behind. The 32-bit case uses the existing jusched_x86.dmp; the 64-bit case, needed because the module list is what decides between AMD64 and WoW64 x86, uses the Breakpad minidump tests/x86_64/windows/breakpad/tiny-exe-with-cet-xsave.dmp added by angr/binaries#176. That pull request has since merged, so the fixture is on angr/binaries master and the checks here no longer wait on it.

Origin of the change: a corpus sweep loaded 5,250 minidumps through cle.Loader. Of the 250 labelled real-world, 18 failed and 16 of those failed inside Minidump.__init__ — 8 with no memory list (smallest sha256 e198aef56fbc85f9ddfb91fe360e1aea109e80d18f58558afbf0162ebff47d38, 912 bytes, x86, carrying SystemInfo, ThreadList, ModuleList and Exception streams), and among the rest 6 with mdf.modules is None and 3 with mdf.threads is None.

Caveats:

  • Most of those 8 dumps read as authored rather than captured: one module timestamp across every module, sequential thread ids, a header TimeDateStamp of 2. The case rests on the file format and on the backend already handling absent data elsewhere, not on the count.
  • Those 8 still fail on this branch, at the module loop Load partial-memory minidumps #715 replaces, so no single file among them can stand as the regression test.
  • The sweep itself was run on cle 638301bbf02865e53394e6643e1eb6e66083ee94, not on the head above; the failure modes were re-confirmed against the baseline with the tests in this PR.
  • The 9 skips are pre-existing TODO skips in tests/test_macho_bindinghelper.py. No minidump test is skipped.

Re-keyed 2026-08-28. The figures above were measured at 9f414421bcfb9c3a7d738514635748857e672371 on baseline b58ea02a446106647cdaae32bdf91b7062404cc1, which is the head the opening line named until now; the branch is at 08852723cfedbb25167fa3fdde957d0bb8e60a60 on 46a37333f4f59b0facf8774ee743ebc4cc074e9b. git range-diff b58ea02a446106647cdaae32bdf91b7062404cc1..9f414421bcfb9c3a7d738514635748857e672371 46a37333f4f59b0facf8774ee743ebc4cc074e9b..08852723cfedbb25167fa3fdde957d0bb8e60a60 reports every commit unchanged and git diff 9f414421bcfb9c3a7d738514635748857e672371 08852723cfedbb25167fa3fdde957d0bb8e60a60 differs only by master's own advance (23 files changed, 1358 insertions(+), 92 deletions(-)). Master touched none of the files this change touches between the two baselines, so every figure above still describes this head.

@angr-bot

Copy link
Copy Markdown
Member

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

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.
@zardus
zardus force-pushed the feature/fix-cle-minidump-streams branch from c9d2bf5 to 0885272 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 load report for four minidumps before and after this change. Each case takes a real fixture — tests/x86_64/windows/breakpad/tiny-exe-with-cet-xsave.dmp or tests/x86/windows/jusched_x86.dmp — and rewrites one stream directory entry to UnusedStream in a temporary in-memory copy, which is how a dump whose writer never collected that stream looks to a reader. Nothing else in the file is touched.

Before — three of the four dumps die mid-load on a None stream object, and the fourth raises an exception that is neither a CLEError nor carries a message:

cle at the merge base, 46a3733
cle: <cle at the merge base>/cle/__init__.py
fixtures: tests/x86_64/windows/breakpad/tiny-exe-with-cet-xsave.dmp, tests/x86/windows/jusched_x86.dmp
--- no ModuleListStream
    input: tiny-exe-with-cet-xsave.dmp with ModuleListStream struck from the directory
    RAISED AttributeError: str(e)="'NoneType' object has no attribute 'modules'"
    isinstance(cle.CLEError) = False
      | if any(module.name.endswith("wow64.dll") for module in self._mdf.modules.modules):
      | ^^^^^^^^^^^^^^^^^^^^^^^^^
      | AttributeError: 'NoneType' object has no attribute 'modules'
--- no ThreadListStream
    input: jusched_x86.dmp with ThreadListStream struck from the directory
    RAISED AttributeError: str(e)="'NoneType' object has no attribute 'threads'"
    isinstance(cle.CLEError) = False
      | for thread in self._mdf.threads.threads:
      | ^^^^^^^^^^^^^^^^^^^^^^^^^
      | AttributeError: 'NoneType' object has no attribute 'threads'
--- no captured memory (MemoryList+ModuleList+ThreadList)
    input: tiny-exe-with-cet-xsave.dmp with MemoryList, ModuleList, ThreadListStream struck from the directory
    RAISED AttributeError: str(e)="'NoneType' object has no attribute 'modules'"
    isinstance(cle.CLEError) = False
      | if any(module.name.endswith("wow64.dll") for module in self._mdf.modules.modules):
      | ^^^^^^^^^^^^^^^^^^^^^^^^^
      | AttributeError: 'NoneType' object has no attribute 'modules'
--- no SystemInfoStream
    input: tiny-exe-with-cet-xsave.dmp with SystemInfoStream struck from the directory
    RAISED MinidumpMissingStreamError: str(e)=''
    isinstance(cle.CLEError) = False
      | File "<cle at the merge base>/cle/backends/minidump/__init__.py", line 38, in __init__
      | raise MinidumpMissingStreamError("SystemInfo", "The architecture was not specified")
      | cle.backends.minidump.MinidumpMissingStreamError

After — the three load with whatever the dump did record, and the missing SystemInfoStream raises a CLEError that says so:

with this change, 0885272
cle: <cle with this change>/cle/__init__.py
fixtures: tests/x86_64/windows/breakpad/tiny-exe-with-cet-xsave.dmp, tests/x86/windows/jusched_x86.dmp
--- no ModuleListStream
    input: tiny-exe-with-cet-xsave.dmp with ModuleListStream struck from the directory
    LOADED  Minidump arch=AMD64 wow64=False
    segments=65 sections=0 sections_map=0 entries []
    threads=['0x5bc'] tls.threads=1
    thread 0x5bc: rip=0x7ff9111e39e4 rsp=0xcbc82ff448
--- no ThreadListStream
    input: jusched_x86.dmp with ThreadListStream struck from the directory
    LOADED  Minidump arch=X86 wow64=False
    segments=173 sections=30 sections_map=30 entries ['CRYPTBASE.dll', 'KERNELBASE.dll', 'advapi32.dll']...
    threads=[] tls.threads=0
--- no captured memory (MemoryList+ModuleList+ThreadList)
    input: tiny-exe-with-cet-xsave.dmp with MemoryList, ModuleList, ThreadListStream struck from the directory
    LOADED  Minidump arch=AMD64 wow64=False
    segments=0 sections=0 sections_map=0 entries []
    threads=[] tls.threads=0
--- no SystemInfoStream
    input: tiny-exe-with-cet-xsave.dmp with SystemInfoStream struck from the directory
    RAISED MinidumpMissingStreamError: str(e)='SystemInfoStream is missing. The architecture was not specified'
    isinstance(cle.CLEError) = True
      | File "<cle with this change>/cle/backends/minidump/__init__.py", line 48, in __init__
      | raise MinidumpMissingStreamError("SystemInfo", "The architecture was not specified")
      | cle.backends.minidump.MinidumpMissingStreamError: SystemInfoStream is missing. The architecture was not specified

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