// 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 { /// /// Class for dealing with .NET 5 single-file bundles. /// /// Based on code from Microsoft.NET.HostModel. /// public static class SingleFileBundle { /// /// Check if the memory-mapped data is a single-file bundle /// 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(); } } /// /// The 32-byte bundle signature: SHA-256 of ".net core bundle". /// internal static ReadOnlySpan BundleSignature => new byte[] { 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 }; public static unsafe bool IsBundle(byte* data, long size, out long bundleHeaderOffset) { ReadOnlySpan bundleSignature = BundleSignature; // 'end' is the last position at which a full signature still fits, so it is a valid // candidate itself: a memory-mapped view reports the exact file length on Unix (Windows // rounds it up to a page), and the signature may occupy the final bytes of the file. byte* end = data + (size - bundleSignature.Length); for (byte* ptr = data; ptr <= end; ptr++) { if (*ptr == 0x8b && bundleSignature.SequenceEqual(new ReadOnlySpan(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(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 Entries; } /// /// 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. /// 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. } /// /// Reads the manifest header from the memory mapping. /// public static Header ReadManifest(MemoryMappedViewAccessor view, long bundleHeaderOffset) { using var stream = view.AsStream(); stream.Seek(bundleHeaderOffset, SeekOrigin.Begin); return ReadManifest(stream); } /// /// Reads the manifest header from the stream. /// 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(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; } } }