Browse Source

Bound WebCilFile section access against mapped-view length

WebCilFile builds raw native pointers into the memory-mapped view directly
from section-header fields read out of attacker-controlled metadata. Unlike
PEFile.GetSectionData, which delegates to the bounds-checked PEReader, this
hand-rolled path validated nothing: a crafted section header could produce a
SectionData (and hence a BlobReader) pointing far outside the view, an
out-of-bounds read reachable on normal decompilation through method-body and
field-data RVA resolution. The (int)RawDataSize narrowing cast could also
yield a negative length.

Resolve and bounds-check the raw-data range against the view length before
constructing SectionData, widening the arithmetic to long so crafted uint
fields cannot wrap the range check or narrow into an apparently valid length.
Structural parsing in FromFile now reports a crafted or truncated module as
"not a WebCIL file" (null) rather than letting EndOfStreamException,
OverflowException or BadImageFormatException escape the loader.

Assisted-by: Claude:claude-opus-4-8:Claude Code
collection-expression-tests
Christoph Wille 1 week ago committed by Siegfried Pammer
parent
commit
3d6a529e58
  1. 204
      ICSharpCode.Decompiler.Tests/Metadata/WebCilFileTests.cs
  2. 71
      ICSharpCode.Decompiler/Metadata/WebCilFile.cs

204
ICSharpCode.Decompiler.Tests/Metadata/WebCilFileTests.cs

@ -0,0 +1,204 @@
// Copyright (c) 2026 Siegfried Pammer
//
// 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;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using ICSharpCode.Decompiler.Metadata;
using NUnit.Framework;
namespace ICSharpCode.Decompiler.Tests.Metadata
{
// Exercises WebCilFile against crafted Wasm/WebCIL input. The section-header fields driving
// the memory-mapped pointer arithmetic come straight from the file, so a crafted module must
// be rejected rather than reading outside the mapped view (CWE-125) or crashing the loader.
[TestFixture]
public class WebCilFileTests
{
const uint WASM_MAGIC = 0x6d736100u; // "\0asm"
const uint WEBCIL_MAGIC = 0x4c496257u; // "WbIL"
struct SectionHeaderRaw
{
public uint VirtualSize;
public uint VirtualAddress;
public uint RawDataSize;
public uint RawDataPtr;
}
static void WriteULEB128(List<byte> buffer, uint value)
{
do
{
byte b = (byte)(value & 0x7F);
value >>= 7;
if (value != 0)
b |= 0x80;
buffer.Add(b);
}
while (value != 0);
}
static byte[] BuildWebcilPayload(ushort coffSections, uint peCliHeaderRVA, SectionHeaderRaw[] sections)
{
using var ms = new MemoryStream();
using var w = new BinaryWriter(ms);
w.Write(WEBCIL_MAGIC);
w.Write((ushort)0); // VersionMajor
w.Write((ushort)0); // VersionMinor
w.Write(coffSections);
w.Write((ushort)0); // reserved0
w.Write(peCliHeaderRVA);
w.Write(0u); // PECliHeaderSize
w.Write(0u); // PEDebugRVA
w.Write(0u); // PEDebugSize
foreach (var s in sections)
{
w.Write(s.VirtualSize);
w.Write(s.VirtualAddress);
w.Write(s.RawDataSize);
w.Write(s.RawDataPtr);
}
w.Flush();
return ms.ToArray();
}
// Wraps a WebCIL payload in the minimal Wasm container WebCilFile.FromFile recognizes:
// a Data section whose two data segments are the (skipped) first segment and the WebCIL blob.
static byte[] BuildWasmContainer(byte[] webcilPayload)
{
var content = new List<byte>();
WriteULEB128(content, 2); // number of data segments
content.Add(1); // segment 1 kind
WriteULEB128(content, 0); // segment 1 length
content.Add(1); // segment 2 kind
WriteULEB128(content, (uint)webcilPayload.Length); // segment 2 length
content.AddRange(webcilPayload);
var file = new List<byte>();
file.AddRange(BitConverter.GetBytes(WASM_MAGIC));
file.AddRange(BitConverter.GetBytes(1u)); // Wasm version
file.Add(11); // WasmSectionId.Data
WriteULEB128(file, (uint)content.Count);
file.AddRange(content);
return file.ToArray();
}
static WebCilFile FromBytes(byte[] bytes)
{
string path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".wasm");
File.WriteAllBytes(path, bytes);
try
{
return WebCilFile.FromFile(path);
}
finally
{
File.Delete(path);
}
}
[Test]
public void TruncatedSectionHeaders_ReturnsNullWithoutReadingPastView()
{
// CoffSections claims far more section headers than the file (or mapped view) holds.
byte[] payload = BuildWebcilPayload(coffSections: 0xFFFF, peCliHeaderRVA: 0, sections: Array.Empty<SectionHeaderRaw>());
Assert.That(FromBytes(BuildWasmContainer(payload)), Is.Null);
}
[Test]
public void CliHeaderRvaInNoSection_ReturnsNull()
{
var sections = new[] {
new SectionHeaderRaw { VirtualAddress = 0, VirtualSize = 0x10, RawDataSize = 0x10, RawDataPtr = 0 }
};
// PECliHeaderRVA points outside the single section, so RVA translation cannot resolve it.
byte[] payload = BuildWebcilPayload(coffSections: 1, peCliHeaderRVA: 0x1000, sections: sections);
Assert.That(FromBytes(BuildWasmContainer(payload)), Is.Null);
}
static ImmutableArray<SectionHeader> Section(uint virtualAddress, uint virtualSize, uint rawDataPtr, uint rawDataSize)
{
return ImmutableArray.Create(new SectionHeader {
VirtualAddress = virtualAddress,
VirtualSize = virtualSize,
RawDataPtr = rawDataPtr,
RawDataSize = rawDataSize
});
}
[Test]
public void OobSectionHeader_IsRejected()
{
// The exploit shape from the finding: a section spanning the whole RVA space whose raw-data
// pointer/size point far outside a small mapped view.
var headers = Section(virtualAddress: 0, virtualSize: 0xFFFFFFFF, rawDataPtr: 0xFFFFFFFF, rawDataSize: 0x7FFFFFFF);
bool ok = WebCilFile.TryGetSectionDataRange(headers, webcilOffset: 0, viewLength: 0x1000, rva: 0x10, out _, out _);
Assert.That(ok, Is.False);
}
[Test]
public void RawDataSizeBeyondInt32_IsRejected()
{
var headers = Section(virtualAddress: 0, virtualSize: 0x100, rawDataPtr: 0, rawDataSize: 0x80000000);
bool ok = WebCilFile.TryGetSectionDataRange(headers, webcilOffset: 0, viewLength: 0x100000000L, rva: 0, out _, out _);
Assert.That(ok, Is.False);
}
[Test]
public void RawDataRangePastView_IsRejected()
{
var headers = Section(virtualAddress: 0, virtualSize: 0x100, rawDataPtr: 0xF0, rawDataSize: 0x20);
bool ok = WebCilFile.TryGetSectionDataRange(headers, webcilOffset: 0, viewLength: 0x100, rva: 0, out _, out _);
Assert.That(ok, Is.False);
}
[Test]
public void RvaInNoSection_IsRejected()
{
var headers = Section(virtualAddress: 0x10, virtualSize: 0x10, rawDataPtr: 0, rawDataSize: 0x10);
bool ok = WebCilFile.TryGetSectionDataRange(headers, webcilOffset: 0, viewLength: 0x1000, rva: 0x100, out _, out _);
Assert.That(ok, Is.False);
}
[Test]
public void RvaPastRawData_IsRejected()
{
// The RVA sits inside the section's virtual size but past its (smaller) raw data, so there
// are no bytes to read from it.
var headers = Section(virtualAddress: 0, virtualSize: 0x100, rawDataPtr: 0, rawDataSize: 0x10);
bool ok = WebCilFile.TryGetSectionDataRange(headers, webcilOffset: 0, viewLength: 0x1000, rva: 0x20, out _, out _);
Assert.That(ok, Is.False);
}
[Test]
public void InBoundsSection_IsResolved()
{
var headers = Section(virtualAddress: 0x20, virtualSize: 0x40, rawDataPtr: 0x80, rawDataSize: 0x40);
bool ok = WebCilFile.TryGetSectionDataRange(headers, webcilOffset: 0x10, viewLength: 0x1000, rva: 0x30, out long offset, out int length);
Assert.That(ok, Is.True);
// offset = RawDataPtr + webcilOffset + (rva - VirtualAddress) = 0x80 + 0x10 + 0x10
Assert.That(offset, Is.EqualTo(0xA0));
// length = remaining raw data from the RVA = RawDataSize - (rva - VirtualAddress) = 0x40 - 0x10
Assert.That(length, Is.EqualTo(0x30));
}
}
}

71
ICSharpCode.Decompiler/Metadata/WebCilFile.cs

@ -36,13 +36,21 @@ namespace ICSharpCode.Decompiler.Metadata
{ {
readonly MemoryMappedViewAccessor view; readonly MemoryMappedViewAccessor view;
readonly long webcilOffset; readonly long webcilOffset;
readonly long viewLength;
unsafe byte* basePointer;
private WebCilFile(string fileName, long webcilOffset, long metadataOffset, MemoryMappedViewAccessor view, ImmutableArray<SectionHeader> sectionHeaders, ImmutableArray<WasmSection> wasmSections, MetadataReaderProvider provider, MetadataReaderOptions metadataOptions = MetadataReaderOptions.Default) private unsafe WebCilFile(string fileName, long webcilOffset, long metadataOffset, MemoryMappedViewAccessor view, ImmutableArray<SectionHeader> sectionHeaders, ImmutableArray<WasmSection> wasmSections, MetadataReaderProvider provider, MetadataReaderOptions metadataOptions = MetadataReaderOptions.Default)
: base(MetadataFileKind.WebCIL, fileName, provider, metadataOptions, 0) : base(MetadataFileKind.WebCIL, fileName, provider, metadataOptions, 0)
{ {
this.webcilOffset = webcilOffset; this.webcilOffset = webcilOffset;
this.MetadataOffset = (int)metadataOffset; this.MetadataOffset = (int)metadataOffset;
this.view = view; this.view = view;
this.viewLength = checked((long)view.SafeMemoryMappedViewHandle.ByteLength);
// Pin the mapped view once for the lifetime of this instance; SectionData hands out raw
// pointers into it, so the pointer must stay valid until Dispose releases it.
byte* ptr = null;
view.SafeMemoryMappedViewHandle.AcquirePointer(ref ptr);
this.basePointer = ptr;
this.SectionHeaders = sectionHeaders; this.SectionHeaders = sectionHeaders;
this.WasmSections = wasmSections; this.WasmSections = wasmSections;
} }
@ -125,6 +133,14 @@ namespace ICSharpCode.Decompiler.Metadata
return null; return null;
} }
catch (Exception ex) when (ex is EndOfStreamException or OverflowException or BadImageFormatException)
{
// A crafted or truncated module can drive the structural reads (Wasm sections, data
// segments, the WebCIL header and RVA translation) past the mapped view or produce an
// invalid metadata stream. Treat any such file as "not a WebCIL file" instead of
// letting the exception escape the loader.
return null;
}
finally finally
{ {
view?.Dispose(); view?.Dispose();
@ -190,7 +206,7 @@ namespace ICSharpCode.Decompiler.Metadata
int i = 0; int i = 0;
foreach (var section in sections) foreach (var section in sections)
{ {
if (rva >= section.VirtualAddress && rva < section.VirtualAddress + section.VirtualSize) if (rva >= section.VirtualAddress && rva < (long)section.VirtualAddress + section.VirtualSize)
{ {
return i; return i;
} }
@ -203,14 +219,43 @@ namespace ICSharpCode.Decompiler.Metadata
{ {
foreach (var section in sections) foreach (var section in sections)
{ {
if (rva >= section.VirtualAddress && rva < section.VirtualAddress + section.VirtualSize) if (rva >= section.VirtualAddress && rva < (long)section.VirtualAddress + section.VirtualSize)
{ {
return section.RawDataPtr + (rva - section.VirtualAddress) + webcilOffset; return (long)section.RawDataPtr + (rva - section.VirtualAddress) + webcilOffset;
} }
} }
throw new BadImageFormatException("RVA not found in any section"); throw new BadImageFormatException("RVA not found in any section");
} }
// Resolves an RVA to the (file offset, length) of its section's raw data inside the mapped
// view, or returns false when no section contains the RVA or the raw-data range falls outside
// the view. All arithmetic is widened to long so crafted uint header fields cannot wrap the
// range check or narrow into a length that looks valid.
internal static bool TryGetSectionDataRange(ImmutableArray<SectionHeader> sectionHeaders, long webcilOffset, long viewLength, int rva, out long offset, out int length)
{
offset = 0;
length = 0;
foreach (var section in sectionHeaders)
{
if (rva >= section.VirtualAddress && rva < (long)section.VirtualAddress + section.VirtualSize)
{
long delta = (long)rva - section.VirtualAddress;
long o = (long)section.RawDataPtr + webcilOffset + delta;
// Length is the raw data remaining from the RVA to the end of the section, matching
// PEReader.GetSectionData (callers such as GetInitialValue treat SectionData.Length
// as the bytes available starting at the RVA). A section whose virtual size exceeds
// its raw data can place the RVA past the raw bytes, leaving nothing to read.
long remaining = (long)section.RawDataSize - delta;
if (o < 0 || remaining < 0 || remaining > int.MaxValue || o > viewLength || remaining > viewLength - o)
return false;
offset = o;
length = (int)remaining;
return true;
}
}
return false;
}
public override MethodBodyBlock GetMethodBody(int rva) public override MethodBodyBlock GetMethodBody(int rva)
{ {
var reader = GetSectionData(rva).GetReader(); var reader = GetSectionData(rva).GetReader();
@ -224,14 +269,9 @@ namespace ICSharpCode.Decompiler.Metadata
public override unsafe SectionData GetSectionData(int rva) public override unsafe SectionData GetSectionData(int rva)
{ {
foreach (var section in SectionHeaders) if (TryGetSectionDataRange(SectionHeaders, webcilOffset, viewLength, rva, out long offset, out int length))
{ {
if (rva >= section.VirtualAddress && rva < section.VirtualAddress + section.VirtualSize) return new SectionData(basePointer + offset, length);
{
byte* ptr = (byte*)0;
view.SafeMemoryMappedViewHandle.AcquirePointer(ref ptr);
return new SectionData(ptr + section.RawDataPtr + webcilOffset + (rva - section.VirtualAddress), (int)section.RawDataSize);
}
} }
throw new BadImageFormatException("RVA not found in any section"); throw new BadImageFormatException("RVA not found in any section");
} }
@ -245,10 +285,17 @@ namespace ICSharpCode.Decompiler.Metadata
return new MetadataModule(context.Compilation, this, TypeSystemOptions.Default); return new MetadataModule(context.Compilation, this, TypeSystemOptions.Default);
} }
protected override void Dispose(bool disposing) protected override unsafe void Dispose(bool disposing)
{ {
if (disposing) if (disposing)
{
if (basePointer != null)
{
view.SafeMemoryMappedViewHandle.ReleasePointer();
basePointer = null;
}
view.Dispose(); view.Dispose();
}
base.Dispose(disposing); base.Dispose(disposing);
} }

Loading…
Cancel
Save