Files
ILSpy/ICSharpCode.Decompiler/SingleFileBundle.cs
T
Christoph Wille a154a7bbbf Guard against OOB read when bundle signature is at file start
SingleFileBundle.IsBundle scans for the 32-byte bundle signature and reads the
8-byte header offset stored immediately before it. That offset only exists in a
genuine bundle, where the signature sits near the end of the file. The scan
started at the first byte, so a crafted file with the signature at offset 0..7
made it read before the start of the buffer. On the production path that buffer
is a page-aligned memory-mapped view, so the read faults on the preceding
unmapped page with an AccessViolationException -- a corrupted-state exception
that bypasses the loader's catch and terminates the process merely from opening
the file. The bundle probe runs on every opened file, so this needs no user
action beyond open.

Skip the backward read unless the match is at least sizeof(long) bytes into the
buffer. While in the same file, bound ReadManifest's FileCount against the bytes
that remain before it pre-sizes the entry array, so a crafted manifest can no
longer request a multi-gigabyte allocation.

Assisted-by: Claude:claude-opus-4-8:Claude Code
2026-06-27 22:40:54 +02:00

179 lines
5.9 KiB
C#

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Collections.Immutable;
using System.IO;
using System.IO.MemoryMappedFiles;
using System.Runtime.CompilerServices;
using System.Text;
namespace ICSharpCode.Decompiler
{
/// <summary>
/// Class for dealing with .NET 5 single-file bundles.
///
/// Based on code from Microsoft.NET.HostModel.
/// </summary>
public static class SingleFileBundle
{
/// <summary>
/// Check if the memory-mapped data is a single-file bundle
/// </summary>
public static unsafe bool IsBundle(MemoryMappedViewAccessor view, out long bundleHeaderOffset)
{
var buffer = view.SafeMemoryMappedViewHandle;
byte* ptr = null;
buffer.AcquirePointer(ref ptr);
try
{
return IsBundle(ptr, checked((long)buffer.ByteLength), out bundleHeaderOffset);
}
finally
{
buffer.ReleasePointer();
}
}
public static unsafe bool IsBundle(byte* data, long size, out long bundleHeaderOffset)
{
ReadOnlySpan<byte> bundleSignature = new byte[] {
// 32 bytes represent the bundle signature: SHA-256 for ".net core bundle"
0x8b, 0x12, 0x02, 0xb9, 0x6a, 0x61, 0x20, 0x38,
0x72, 0x7b, 0x93, 0x02, 0x14, 0xd7, 0xa0, 0x32,
0x13, 0xf5, 0xb9, 0xe6, 0xef, 0xae, 0x33, 0x18,
0xee, 0x3b, 0x2d, 0xce, 0x24, 0xb3, 0x6a, 0xae
};
byte* end = data + (size - bundleSignature.Length);
for (byte* ptr = data; ptr < end; ptr++)
{
if (*ptr == 0x8b && bundleSignature.SequenceEqual(new ReadOnlySpan<byte>(ptr, bundleSignature.Length)))
{
// A genuine bundle stores the 8-byte header offset immediately before the
// signature, so the signature never appears within the first sizeof(long)
// bytes of the file. Without this guard, a crafted file with the signature at
// offset 0..7 reads before the start of the (page-aligned, memory-mapped)
// buffer, faulting on the preceding unmapped page.
if (ptr - data >= sizeof(long))
{
bundleHeaderOffset = Unsafe.ReadUnaligned<long>(ptr - sizeof(long));
if (bundleHeaderOffset > 0 && bundleHeaderOffset < size)
{
return true;
}
}
}
}
bundleHeaderOffset = 0;
return false;
}
public struct Header
{
public uint MajorVersion;
public uint MinorVersion;
public int FileCount;
public string BundleID;
// Fields introduced with v2:
public long DepsJsonOffset;
public long DepsJsonSize;
public long RuntimeConfigJsonOffset;
public long RuntimeConfigJsonSize;
public ulong Flags;
public ImmutableArray<Entry> Entries;
}
/// <summary>
/// FileType: Identifies the type of file embedded into the bundle.
///
/// The bundler differentiates a few kinds of files via the manifest,
/// with respect to the way in which they'll be used by the runtime.
/// </summary>
public enum FileType : byte
{
Unknown, // Type not determined.
Assembly, // IL and R2R Assemblies
NativeBinary, // NativeBinaries
DepsJson, // .deps.json configuration file
RuntimeConfigJson, // .runtimeconfig.json configuration file
Symbols // PDB Files
};
public struct Entry
{
public long Offset;
public long Size;
public long CompressedSize; // 0 if not compressed, otherwise the compressed size in the bundle
public FileType Type;
public string RelativePath; // Path of an embedded file, relative to the Bundle source-directory.
}
/// <summary>
/// Reads the manifest header from the memory mapping.
/// </summary>
public static Header ReadManifest(MemoryMappedViewAccessor view, long bundleHeaderOffset)
{
using var stream = view.AsStream();
stream.Seek(bundleHeaderOffset, SeekOrigin.Begin);
return ReadManifest(stream);
}
/// <summary>
/// Reads the manifest header from the stream.
/// </summary>
public static Header ReadManifest(Stream stream)
{
var header = new Header();
using var reader = new BinaryReader(stream, Encoding.UTF8, leaveOpen: true);
header.MajorVersion = reader.ReadUInt32();
header.MinorVersion = reader.ReadUInt32();
// Major versions 3, 4 and 5 were skipped to align bundle versioning with .NET versioning scheme
if (header.MajorVersion < 1 || header.MajorVersion > 6)
{
throw new InvalidDataException($"Unsupported manifest version: {header.MajorVersion}.{header.MinorVersion}");
}
header.FileCount = reader.ReadInt32();
// FileCount is used below to pre-size the entry array. Each entry occupies at least
// one byte in the stream, so a count larger than the bytes that remain cannot be
// honest; reject it instead of attempting a huge allocation for a crafted manifest.
long remainingBytes = stream.Length - stream.Position;
if (header.FileCount < 0 || header.FileCount > remainingBytes)
{
throw new InvalidDataException($"Invalid bundle manifest: FileCount {header.FileCount} exceeds available data.");
}
header.BundleID = reader.ReadString();
if (header.MajorVersion >= 2)
{
header.DepsJsonOffset = reader.ReadInt64();
header.DepsJsonSize = reader.ReadInt64();
header.RuntimeConfigJsonOffset = reader.ReadInt64();
header.RuntimeConfigJsonSize = reader.ReadInt64();
header.Flags = reader.ReadUInt64();
}
var entries = ImmutableArray.CreateBuilder<Entry>(header.FileCount);
for (int i = 0; i < header.FileCount; i++)
{
entries.Add(ReadEntry(reader, header.MajorVersion));
}
header.Entries = entries.MoveToImmutable();
return header;
}
private static Entry ReadEntry(BinaryReader reader, uint bundleMajorVersion)
{
Entry entry;
entry.Offset = reader.ReadInt64();
entry.Size = reader.ReadInt64();
entry.CompressedSize = bundleMajorVersion >= 6 ? reader.ReadInt64() : 0;
entry.Type = (FileType)reader.ReadByte();
entry.RelativePath = reader.ReadString();
return entry;
}
}
}