mirror of https://github.com/icsharpcode/ILSpy.git
Browse Source
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 Codepull/3829/head
4 changed files with 226 additions and 127 deletions
@ -0,0 +1,140 @@
@@ -0,0 +1,140 @@
|
||||
// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System.IO; |
||||
using System.Text; |
||||
|
||||
using ICSharpCode.BamlDecompiler; |
||||
using ICSharpCode.Decompiler.Metadata; |
||||
|
||||
using NUnit.Framework; |
||||
|
||||
namespace ILSpy.BamlDecompiler.Tests |
||||
{ |
||||
/// <summary>
|
||||
/// Hardening tests for the BAML reader: a crafted or corrupt resource must surface a
|
||||
/// single catchable exception rather than crashing the process (uncontrolled recursion
|
||||
/// terminating with an uncatchable StackOverflowException) or allocating gigabytes.
|
||||
/// These drive the public <see cref="XamlDecompiler.Decompile(Stream)"/> entry point,
|
||||
/// the same one resource browsing uses.
|
||||
/// </summary>
|
||||
[TestFixture] |
||||
public class BamlSecurityTests |
||||
{ |
||||
// BAML record type ids (see BamlRecordType in the decompiler).
|
||||
const byte DefAttributeKeyString = 0x26; |
||||
const byte StaticResourceStart = 0x30; |
||||
|
||||
static readonly string AssemblyPath = typeof(BamlSecurityTests).Assembly.Location; |
||||
|
||||
static XamlDecompiler CreateDecompiler(PEFile module) |
||||
{ |
||||
var resolver = new UniversalAssemblyResolver(AssemblyPath, false, module.Metadata.DetectTargetFrameworkId()); |
||||
var typeSystem = new BamlDecompilerTypeSystem(module, resolver); |
||||
return new XamlDecompiler(typeSystem, new BamlDecompilerSettings()); |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Writes the smallest header the reader accepts: the "MSBAML" signature block and
|
||||
/// the three (reader/updater/writer) 0.0x60 version structs.
|
||||
/// </summary>
|
||||
static void WriteValidHeader(BinaryWriter w) |
||||
{ |
||||
byte[] sig = Encoding.Unicode.GetBytes("MSBAML"); |
||||
w.Write((uint)sig.Length); // byte count; the reader takes len >> 1 characters
|
||||
w.Write(sig); // 12 bytes, already 4-byte aligned so no padding follows
|
||||
for (int i = 0; i < 3; i++) |
||||
{ |
||||
w.Write((ushort)0x0000); // Major
|
||||
w.Write((ushort)0x0060); // Minor
|
||||
} |
||||
} |
||||
|
||||
static void WriteDefAttributeKeyString(BinaryWriter w) |
||||
{ |
||||
w.Write(DefAttributeKeyString); |
||||
w.Write((byte)9); // SizedBamlRecord size prefix (1 byte size + 8 bytes data)
|
||||
w.Write((ushort)0); // ValueId
|
||||
w.Write((uint)0); // pos
|
||||
w.Write(false); // Shared
|
||||
w.Write(false); // SharedSet
|
||||
} |
||||
|
||||
static void WriteStaticResourceStart(BinaryWriter w) |
||||
{ |
||||
w.Write(StaticResourceStart); |
||||
w.Write((ushort)0); // TypeId
|
||||
w.Write((byte)0); // Flags
|
||||
} |
||||
|
||||
[Test] |
||||
public void DeeplyNestedDeferBlock_DoesNotStackOverflow() |
||||
{ |
||||
// A DefAttributeKeyString defer block followed by tens of thousands of nested
|
||||
// StaticResourceStart records makes the unfixed NavigateTree recurse without a
|
||||
// depth cap, terminating the process with an uncatchable StackOverflowException.
|
||||
var ms = new MemoryStream(); |
||||
using (var w = new BinaryWriter(ms, Encoding.Unicode, leaveOpen: true)) |
||||
{ |
||||
WriteValidHeader(w); |
||||
WriteDefAttributeKeyString(w); |
||||
for (int i = 0; i < 20000; i++) |
||||
WriteStaticResourceStart(w); |
||||
} |
||||
ms.Position = 0; |
||||
|
||||
using var module = new PEFile(AssemblyPath); |
||||
Assert.Throws<InvalidDataException>(() => CreateDecompiler(module).Decompile(ms)); |
||||
} |
||||
|
||||
[Test] |
||||
public void UnterminatedDeferBlock_ThrowsInvalidData() |
||||
{ |
||||
// A single StaticResourceStart with no matching StaticResourceEnd walks the
|
||||
// record index off the end of the list (ArgumentOutOfRangeException before the
|
||||
// fix); the reader must report it as malformed data instead.
|
||||
var ms = new MemoryStream(); |
||||
using (var w = new BinaryWriter(ms, Encoding.Unicode, leaveOpen: true)) |
||||
{ |
||||
WriteValidHeader(w); |
||||
WriteDefAttributeKeyString(w); |
||||
WriteStaticResourceStart(w); |
||||
} |
||||
ms.Position = 0; |
||||
|
||||
using var module = new PEFile(AssemblyPath); |
||||
Assert.Throws<InvalidDataException>(() => CreateDecompiler(module).Decompile(ms)); |
||||
} |
||||
|
||||
[Test] |
||||
public void OversizedSignatureLength_ThrowsInvalidData() |
||||
{ |
||||
// The signature length is attacker-controlled and read before the MSBAML check;
|
||||
// an enormous value must be rejected before it drives a multi-gigabyte allocation.
|
||||
var ms = new MemoryStream(); |
||||
using (var w = new BinaryWriter(ms, Encoding.Unicode, leaveOpen: true)) |
||||
{ |
||||
w.Write((uint)0x7FFFFFFF); |
||||
} |
||||
ms.Position = 0; |
||||
|
||||
using var module = new PEFile(AssemblyPath); |
||||
Assert.Throws<InvalidDataException>(() => CreateDecompiler(module).Decompile(ms)); |
||||
} |
||||
} |
||||
} |
||||
Loading…
Reference in new issue