diff --git a/ICSharpCode.Decompiler.Tests/Metadata/WebCilFileTests.cs b/ICSharpCode.Decompiler.Tests/Metadata/WebCilFileTests.cs new file mode 100644 index 000000000..60f345c64 --- /dev/null +++ b/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 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(); + 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(); + 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()); + 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 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)); + } + } +} diff --git a/ICSharpCode.Decompiler/Metadata/WebCilFile.cs b/ICSharpCode.Decompiler/Metadata/WebCilFile.cs index 47e7d4bd5..e13bdb97e 100644 --- a/ICSharpCode.Decompiler/Metadata/WebCilFile.cs +++ b/ICSharpCode.Decompiler/Metadata/WebCilFile.cs @@ -36,13 +36,21 @@ namespace ICSharpCode.Decompiler.Metadata { readonly MemoryMappedViewAccessor view; readonly long webcilOffset; + readonly long viewLength; + unsafe byte* basePointer; - private WebCilFile(string fileName, long webcilOffset, long metadataOffset, MemoryMappedViewAccessor view, ImmutableArray sectionHeaders, ImmutableArray wasmSections, MetadataReaderProvider provider, MetadataReaderOptions metadataOptions = MetadataReaderOptions.Default) + private unsafe WebCilFile(string fileName, long webcilOffset, long metadataOffset, MemoryMappedViewAccessor view, ImmutableArray sectionHeaders, ImmutableArray wasmSections, MetadataReaderProvider provider, MetadataReaderOptions metadataOptions = MetadataReaderOptions.Default) : base(MetadataFileKind.WebCIL, fileName, provider, metadataOptions, 0) { this.webcilOffset = webcilOffset; this.MetadataOffset = (int)metadataOffset; 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.WasmSections = wasmSections; } @@ -125,6 +133,14 @@ namespace ICSharpCode.Decompiler.Metadata 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 { view?.Dispose(); @@ -190,7 +206,7 @@ namespace ICSharpCode.Decompiler.Metadata int i = 0; 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; } @@ -203,14 +219,43 @@ namespace ICSharpCode.Decompiler.Metadata { 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"); } + // 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 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) { var reader = GetSectionData(rva).GetReader(); @@ -224,14 +269,9 @@ namespace ICSharpCode.Decompiler.Metadata 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) - { - byte* ptr = (byte*)0; - view.SafeMemoryMappedViewHandle.AcquirePointer(ref ptr); - return new SectionData(ptr + section.RawDataPtr + webcilOffset + (rva - section.VirtualAddress), (int)section.RawDataSize); - } + return new SectionData(basePointer + offset, length); } 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); } - protected override void Dispose(bool disposing) + protected override unsafe void Dispose(bool disposing) { if (disposing) + { + if (basePointer != null) + { + view.SafeMemoryMappedViewHandle.ReleasePointer(); + basePointer = null; + } view.Dispose(); + } base.Dispose(disposing); }