Bound .rsrc resource-tree parsing against crafted input - #3840
Merged
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
Hardens Win32 .rsrc resource-tree parsing in the decompiler against crafted or truncated PE inputs by adding explicit bounds checks, recursion limits, and cycle detection, and by introducing a resolver seam to bound resource data reads. This reduces the risk of uncatchable crashes (stack overflow / native OOB reads) when opening hostile assemblies (e.g., via “Save as project”).
Changes:
- Add a bounded resource-data resolver and carry section length through the directory walk to validate offsets/sizes.
- Clamp directory entry iteration, cap recursion depth, and break cycles during tree traversal.
- Add targeted unit tests that exercise the new guards using pinned crafted
.rsrcbuffers.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| ICSharpCode.Decompiler/Util/Win32Resources.cs | Adds bounded parsing primitives (length tracking, in-bounds checks, depth/cycle guards) and refactors data access through a resolver delegate. |
| ICSharpCode.Decompiler.Tests/Util/Win32ResourcesTests.cs | Adds regression tests covering cycles, deep nesting, oversized counts/sizes, and valid-tree parsing. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
ReadWin32Resources walked the PE resource directory tree with raw native pointer arithmetic over attacker-controlled offsets, counts and sizes, with no bounds checks, no recursion-depth limit and no cycle detection. The root section pointer came from GetSectionData, whose length was read and then discarded, leaving every dereference unbounded. A crafted assembly could therefore turn merely opening it (the Save as project feature reads these resources unconditionally) into an uncatchable process kill or an out-of-bounds native read: a subdirectory entry pointing back at itself recursed until the stack overflowed; an inflated entry count walked off the section end; and a data entry whose Size was up to 4 GB made Buffer.MemoryCopy read far past the section, faulting on an unmapped page or copying adjacent process memory into the byte[] later written to app.ico/app.manifest on disk. None of this is containable, since a StackOverflowException cannot be caught and the repo has no corrupted-state exception handling. This is the sibling of the bundle signature fix in a154a7b. Carry the section length alongside the root pointer and bounds-check every offset, entry count, name-string length and data Size against it, cap recursion depth and track visited directory offsets to break cycles. A hostile or truncated file now yields a bounded, partial tree instead of a crash; well-formed resources parse exactly as before. The parser no longer needs the whole PEReader, only a delegate that resolves a data RVA to a bounded pointer, which is the seam the new tests drive over a pinned buffer. Assisted-by: Claude:claude-opus-4-8:Claude Code
christophwille
force-pushed
the
christophwille/96
branch
from
June 28, 2026 05:37
4a37bad to
8324129
Compare
siegfriedpammer
approved these changes
Jun 28, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
ReadWin32Resourceswalks the PE resource directory tree with raw native pointer arithmetic over attacker-controlled offsets, counts and sizes, with no bounds checks, no recursion-depth limit and no cycle detection. The root section pointer comes fromGetSectionData, whose length is read and then discarded, so every dereference is unbounded. Because "Save as project" reads these resources unconditionally, a crafted assembly can turn merely opening it into an uncatchable process kill or an out-of-bounds native read.Three concrete vectors: a subdirectory entry pointing back at itself (or a long acyclic chain) recurses until the stack overflows; an inflated
NumberOf*Entrieswalks the entry loop off the section end; and a data entry whoseSizeis up to 4 GB makesBuffer.MemoryCopyread far past the section base, faulting on an unmapped page or copying adjacent process memory into thebyte[]that is then written toapp.ico/app.manifeston disk. None of this is containable from the caller, since aStackOverflowExceptioncannot be caught and there is no corrupted-state-exception handling in the codebase. It is the same class of bug as the single-file bundle signature fix in a154a7b.The fix carries the section length (already returned by
GetSectionData) alongside the root pointer and bounds-checks every offset, entry count, name-string length and dataSizeagainst it; it caps recursion depth and tracks visited directory offsets to break cycles. A hostile or truncated file now yields a bounded, partial tree instead of a crash, while well-formed resources parse exactly as before — the existing project-decompiler tests that read real icons and manifests continue to pass. As part of this, the parser no longer depends on the wholePEReader: it takes a small delegate that resolves a data RVA to a bounded pointer, which is also the seam the new tests exercise.New tests build crafted
.rsrcbyte buffers over a pinned region and drive that seam directly: a self-referential subdirectory, a chain deeper than the depth cap, an over-range entry count, an over-range dataSize, an out-of-range name-string offset, and a well-formed Type/Name/Language tree whose data round-trips byte-for-byte as a regression guard.🤖 Generated with Claude Code