Harden BAML reader against crafted-resource crashes#3841
Open
christophwille wants to merge 1 commit into
Open
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
Hardens the BAML decompiler’s reader/defer-pass against crafted .baml resources that previously could crash ILSpy (uncatchable StackOverflowException, out-of-range indexing, oversized allocations) by adding bounded record navigation, safer offset resolution, and targeted regression tests.
Changes:
- Introduces
BamlDeferReaderto centralize and harden defer-block key scanning with bounded indexing and a maximum nesting depth. - Uses
TryGetValuewhen resolving defer-record offsets so malformed offsets surface asInvalidDataExceptioninstead ofKeyNotFoundException. - Adds Windows/WPF-bound tests that craft malformed BAML streams to assert
InvalidDataExceptionbehavior (deep nesting, unterminated blocks, oversized signature length).
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| ILSpy.BamlDecompiler.Tests/ILSpy.BamlDecompiler.Tests.csproj | Includes new security regression test file in the test project build. |
| ILSpy.BamlDecompiler.Tests/BamlSecurityTests.cs | Adds crafted-stream regression tests for previously crashable/memory-intensive BAML inputs. |
| ICSharpCode.BamlDecompiler/Baml/BamlRecords.cs | Adds BamlDeferReader helper and replaces duplicated defer key-walk logic with bounded/depth-capped traversal. |
| ICSharpCode.BamlDecompiler/Baml/BamlReader.cs | Adds signature-length validation and makes defer offset resolution robust via TryGetValue + InvalidDataException. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Browsing an embedded .baml resource runs it through BamlReader.ReadDocument, whose post-parse defer pass walks the record list with NavigateTree. That walk recursed on every nested StaticResourceStart/KeyElementStart record with no depth cap and indexed the record list with no bound. A crafted resource could therefore drive recursion into a StackOverflowException -- which is uncatchable and kills the process despite the resource node's try/catch -- or walk the index off the end of the list. Both are reachable from ordinary resource browsing, no project export required. Consolidate the duplicated defer walk into one bounded, depth-capped helper that fails with a catchable InvalidDataException, so the existing UI catch turns a malformed resource into a "BAML decompilation failed" message instead of a crash. Reject an oversized signature length before it drives a multi-gigabyte allocation (it is read before the MSBAML check), and resolve defer offsets with TryGetValue so a bogus offset reports malformed data rather than escaping as a bare KeyNotFoundException. Assisted-by: Claude:claude-opus-4-8:Claude Code
84291ab to
c93de10
Compare
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.
Browsing an assembly's embedded
.bamlresource runs it throughBamlReader.ReadDocument, whose post-parse "defer" pass walks the record list withNavigateTree. That walk had two defects, both reachable from ordinary resource browsing (no "save as project" / project export required): it recursed on every nestedStaticResourceStart/KeyElementStartrecord with no depth cap, and it indexed the record list (doc[index], aList<BamlRecord>) with no bound.A crafted resource with tens of thousands of nested start records (~4 bytes each) drives the recursion into a
StackOverflowException. That exception is uncatchable in .NET and terminates the ILSpy process even thoughBamlResourceEntryNode.Decompilewraps the call intry/catch— the catch cannot see a stack overflow. A start record with no matching end instead walks the index past the end of the list. The project-export path (BamlResourceFileHandler.WriteResourceToFile) has no catch at all, so there even the index overrun is unhandled.This consolidates the (previously duplicated) defer walk into a single helper that bounds every record-list access and depth-caps the nesting walk, failing with a catchable
InvalidDataException. The existing UI catch then turns a malformed resource into a "BAML decompilation failed" message instead of a crash. The cap (1000) sits far below the CLR stack limit, so it never rejects legitimate BAML, which nests only a handful of levels.Two smaller hardening fixes in the same reader, same root cause of trusting attacker-controlled offsets/lengths:
ReadSignaturenow rejects a signature length that cannot fit in the remaining stream before it allocates (the length is read before theMSBAMLcheck, so a crafted value otherwise drives a multi-gigabyte allocation), and the defer pass resolves record offsets withTryGetValueso a bogus offset reports malformed data rather than escaping as a bareKeyNotFoundException.Tests in
ILSpy.BamlDecompiler.Testscraft minimal malformed.bamlstreams and assert that the publicXamlDecompiler.Decompileentry point throwsInvalidDataExceptionfor the deeply-nested, unterminated, and oversized-signature cases rather than crashing or allocating. They live in the existing WPF-bound (Windows-only) BAML test project alongside the other cases.