Skip to content

Harden BAML reader against crafted-resource crashes#3841

Open
christophwille wants to merge 1 commit into
masterfrom
christophwille/97
Open

Harden BAML reader against crafted-resource crashes#3841
christophwille wants to merge 1 commit into
masterfrom
christophwille/97

Conversation

@christophwille

Copy link
Copy Markdown
Member

Browsing an assembly's embedded .baml resource runs it through BamlReader.ReadDocument, whose post-parse "defer" pass walks the record list with NavigateTree. That walk had two defects, both reachable from ordinary resource browsing (no "save as project" / project export required): it recursed on every nested StaticResourceStart / KeyElementStart record with no depth cap, and it indexed the record list (doc[index], a List<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 though BamlResourceEntryNode.Decompile wraps the call in try/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: ReadSignature now rejects a signature length that cannot fit in the remaining stream before it allocates (the length is read before the MSBAML check, so a crafted value otherwise drives a multi-gigabyte allocation), and the defer pass resolves record offsets with TryGetValue so a bogus offset reports malformed data rather than escaping as a bare KeyNotFoundException.

Tests in ILSpy.BamlDecompiler.Tests craft minimal malformed .baml streams and assert that the public XamlDecompiler.Decompile entry point throws InvalidDataException for 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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 BamlDeferReader to centralize and harden defer-block key scanning with bounded indexing and a maximum nesting depth.
  • Uses TryGetValue when resolving defer-record offsets so malformed offsets surface as InvalidDataException instead of KeyNotFoundException.
  • Adds Windows/WPF-bound tests that craft malformed BAML streams to assert InvalidDataException behavior (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.

Comment thread ICSharpCode.BamlDecompiler/Baml/BamlReader.cs Outdated
Comment thread ICSharpCode.BamlDecompiler/Baml/BamlRecords.cs
Comment thread ILSpy.BamlDecompiler.Tests/BamlSecurityTests.cs Outdated
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
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