using System.Buffers.Binary; using System.Collections.Immutable; using System.Reflection; using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; using System.Reflection.PortableExecutable; namespace XFESystem.ManagedRuntime; /// /// Loads an actual CLI PE image and interprets a deliberately bounded console IL profile. /// Unknown instructions, exception regions, local calls and native/instance calls are rejected. /// This does not load the program into the host CLR and does not execute native image bytes. /// public sealed class ManagedApplicationRunner { public ManagedExecutionResult Execute(ReadOnlyMemory image, IManagedConsole console, int instructionLimit = ManagedRuntimeCapabilities.MaxInstructions) { ArgumentNullException.ThrowIfNull(console); if (image.Length is < 128 or > ManagedRuntimeCapabilities.MaxImageBytes) throw new BadImageFormatException("Managed image size is outside the accepted bounds."); if (instructionLimit is < 1 or > ManagedRuntimeCapabilities.MaxInstructions) throw new ArgumentOutOfRangeException(nameof(instructionLimit)); using var pe = new PEReader(ImmutableArray.Create(image.ToArray())); if (!pe.HasMetadata || pe.PEHeaders.CorHeader is not { } header || (header.Flags & CorFlags.ILOnly) == 0 || (header.Flags & CorFlags.NativeEntryPoint) != 0) throw new BadImageFormatException("Expected an IL-only managed PE executable."); var metadata = pe.GetMetadataReader(); if (!metadata.IsAssembly) throw new BadImageFormatException("A netmodule cannot be launched."); var assembly = metadata.GetAssemblyDefinition(); if (!IsNet10(metadata, assembly)) throw new NotSupportedException("This profile accepts net10.0 programs only."); var entry = MetadataTokens.EntityHandle(header.EntryPointTokenOrRelativeVirtualAddress); if (entry.Kind != HandleKind.MethodDefinition) throw new BadImageFormatException("A managed entry point is required."); var method = metadata.GetMethodDefinition((MethodDefinitionHandle)entry); if ((method.Attributes & MethodAttributes.Static) == 0 || (method.Attributes & MethodAttributes.PinvokeImpl) != 0) throw new NotSupportedException("The entry point must be a static managed method."); var entrySignature = metadata.GetBlobReader(method.Signature); var entryHeader = entrySignature.ReadSignatureHeader(); if (entryHeader.IsInstance || entryHeader.IsGeneric) throw new BadImageFormatException("Invalid entry signature."); int entryArguments = entrySignature.ReadCompressedInteger(); var entryReturn = entrySignature.ReadSignatureTypeCode(); if (entryReturn is not (SignatureTypeCode.Void or SignatureTypeCode.Int32) || entryArguments is < 0 or > 1) throw new NotSupportedException("Expected Main() or Main(string[]) returning void or Int32."); if (entryArguments == 1 && (entrySignature.ReadSignatureTypeCode() != SignatureTypeCode.SZArray || entrySignature.ReadSignatureTypeCode() != SignatureTypeCode.String)) throw new NotSupportedException("Main arguments must be string[]."); if (entrySignature.RemainingBytes != 0) throw new BadImageFormatException("Trailing entry signature bytes."); var body = pe.GetMethodBody(method.RelativeVirtualAddress); if (body.ExceptionRegions.Length != 0) throw new NotSupportedException("Exception-handling IL is outside this initial compatibility profile."); var code = body.GetILBytes() ?? throw new BadImageFormatException("Missing method body."); ValidateInstructions(code); var stack = new object?[Math.Min(body.MaxStack, 256)]; int count = 0, pc = 0, executed = 0, calls = 0; if (body.MaxStack > 256) throw new NotSupportedException("Evaluation stack limit exceeded."); var locals = new object?[LocalCount(metadata, body.LocalSignature)]; int stringCharacters = 0; void ChargeString(int length) { if (length > ManagedRuntimeCapabilities.MaxStringCharacters || length > ManagedRuntimeCapabilities.MaxTotalStringCharacters - stringCharacters) throw new InvalidOperationException("Managed string allocation limit exceeded."); stringCharacters += length; } object? Pop() { if (count == 0) throw new BadImageFormatException("Evaluation stack underflow."); var value = stack[--count]; stack[count] = null; return value; } void Push(object? value) { if (count >= stack.Length) throw new BadImageFormatException("Evaluation stack overflow."); stack[count++] = value; } int Number(object? value) => value is int number ? number : throw new NotSupportedException("Expected an Int32 stack value."); while (pc < code.Length) { if (++executed > instructionLimit) throw new InvalidOperationException("Managed instruction limit exceeded."); byte op = code[pc++]; if (op == 0x00) continue; // nop if (op == 0x02) { if (entryArguments != 1) throw new BadImageFormatException("No argument exists."); Push(Array.Empty()); continue; } if (op is >= 0x06 and <= 0x09) { Push(Local(locals, op - 0x06)); continue; } if (op is >= 0x0A and <= 0x0D) { SetLocal(locals, op - 0x0A, Pop()); continue; } if (op == 0x11) { Push(Local(locals, code[pc++])); continue; } if (op == 0x13) { SetLocal(locals, code[pc++], Pop()); continue; } if (op == 0x14) { Push(null); continue; } if (op is >= 0x15 and <= 0x1E) { Push(op - 0x16); continue; } if (op == 0x1F) { Push((int)(sbyte)code[pc++]); continue; } if (op == 0x20) { Push(ReadInt(code, ref pc)); continue; } if (op == 0x25) { var value = Pop(); Push(value); Push(value); continue; } if (op == 0x26) { Pop(); continue; } if (op == 0x72) { int token = ReadInt(code, ref pc); if ((token & unchecked((int)0xFF000000)) != 0x70000000) throw new BadImageFormatException("Invalid user string token."); var value = metadata.GetUserString(MetadataTokens.UserStringHandle(token & 0xFFFFFF)); if (value.Length > 16_384) throw new NotSupportedException("Managed string literal limit exceeded."); ChargeString(value.Length); Push(value); continue; } if (op == 0x28) { int token = ReadInt(code, ref pc); Call(metadata, token, console, Pop, Push, ChargeString); calls++; continue; } if (op == 0x2A) { if (count != (entryReturn == SignatureTypeCode.Void ? 0 : 1)) throw new BadImageFormatException("Unexpected stack at ret."); return new(metadata.GetString(assembly.Name), executed, entryReturn == SignatureTypeCode.Void ? 0 : Number(Pop()), calls); } if (op is >= 0x58 and <= 0x5A) { int right = Number(Pop()), left = Number(Pop()); Push(op == 0x58 ? unchecked(left + right) : op == 0x59 ? unchecked(left - right) : unchecked(left * right)); continue; } if (op is >= 0x2B and <= 0x33 || op is >= 0x38 and <= 0x40) { bool shortForm = op < 0x38; int kind = shortForm ? op - 0x2B : op - 0x38; int offset = shortForm ? (sbyte)code[pc++] : ReadInt(code, ref pc); bool jump; if (kind == 0) jump = true; else if (kind is 1 or 2) { var value = Pop(); bool truth = value is not null && (value is not int number || number != 0); jump = kind == 2 ? truth : !truth; } else { int right = Number(Pop()), left = Number(Pop()); jump = kind switch { 3 => left == right, 4 => left >= right, 5 => left > right, 6 => left <= right, 7 => left < right, 8 => left != right, _ => false }; } if (jump) pc += offset; continue; } throw new NotSupportedException($"Unsupported IL opcode 0x{op:X2}."); } throw new BadImageFormatException("Method fell through without ret."); } private static void Call(MetadataReader reader, int token, IManagedConsole console, Func pop, Action push, Action chargeString) { var handle = MetadataTokens.EntityHandle(token); if (handle.Kind != HandleKind.MemberReference) throw new NotSupportedException("Only declared platform interop calls are available."); var member = reader.GetMemberReference((MemberReferenceHandle)handle); if (member.Parent.Kind != HandleKind.TypeReference) throw new NotSupportedException("Unsupported call owner."); var type = reader.GetTypeReference((TypeReferenceHandle)member.Parent); string owner = reader.GetString(type.Namespace) + "." + reader.GetString(type.Name), name = reader.GetString(member.Name); if (type.ResolutionScope.Kind != HandleKind.AssemblyReference) throw new NotSupportedException("Only external platform bindings are permitted."); string scope = reader.GetString(reader.GetAssemblyReference((AssemblyReferenceHandle)type.ResolutionScope).Name); var signature = reader.GetBlobReader(member.Signature); var header = signature.ReadSignatureHeader(); if (header.Kind != SignatureKind.Method || header.IsInstance || header.IsGeneric) throw new NotSupportedException("Instance/generic interop is unavailable."); int parameterCount = signature.ReadCompressedInteger(); if (parameterCount is < 0 or > 4) throw new NotSupportedException("Interop parameter limit exceeded."); var returnType = SignatureType(reader, ref signature); var parameterTypes = new string[parameterCount]; for (int i = 0; i < parameterCount; i++) parameterTypes[i] = SignatureType(reader, ref signature); if (signature.RemainingBytes != 0) throw new BadImageFormatException("Trailing interop signature bytes."); var arguments = new object?[parameterCount]; for (int i = parameterCount - 1; i >= 0; i--) arguments[i] = pop(); for (int i = 0; i < parameterCount; i++) if (parameterTypes[i] == "String" ? arguments[i] is not (null or string) : arguments[i] is not int) throw new BadImageFormatException("Evaluation stack argument type does not match the interop signature."); string Text(int i) => arguments[i] switch { null => "", string text => text, int value => value.ToString(), _ => throw new NotSupportedException("Unsupported console parameter type.") }; if (owner == "System.Console" && scope == "System.Console") { if (name == "WriteLine" && parameterCount == 0 && returnType == "Void") { console.WriteLine(""); return; } if (name is "WriteLine" or "Write" && parameterCount == 1 && returnType == "Void" && parameterTypes[0] is "String" or "Int32") { if (name == "WriteLine") console.WriteLine(Text(0)); else console.Write(Text(0)); return; } if (name == "ResetColor" && parameterCount == 0 && returnType == "Void") { console.ResetColor(); return; } if (name == "set_ForegroundColor" && parameterCount == 1 && returnType == "Void" && parameterTypes[0] == "System.ConsoleColor" && arguments[0] is int fg) { console.ForegroundColor = (ConsoleColor)fg; return; } if (name == "set_BackgroundColor" && parameterCount == 1 && returnType == "Void" && parameterTypes[0] == "System.ConsoleColor" && arguments[0] is int bg) { console.BackgroundColor = (ConsoleColor)bg; return; } if (name == "get_ForegroundColor" && parameterCount == 0 && returnType == "System.ConsoleColor") { push((int)console.ForegroundColor); return; } if (name == "get_BackgroundColor" && parameterCount == 0 && returnType == "System.ConsoleColor") { push((int)console.BackgroundColor); return; } } if (owner == "XFEExtension.NetCore.XFEConsole.XFEConsole" && scope == "XFEExtension.NetCore.XFEConsole" && name == "WriteLine" && parameterCount == 1 && parameterTypes[0] == "String" && returnType == "Void") { console.XfeWriteLine(Text(0)); return; } if (owner == "System.String" && scope == "System.Runtime" && name == "Concat" && parameterCount == 2 && returnType == "String" && parameterTypes[0] == "String" && parameterTypes[1] == "String") { string left = Text(0), right = Text(1); chargeString(checked(left.Length + right.Length)); push(left + right); return; } throw new NotSupportedException($"Platform binding unavailable: {owner}.{name}"); } private static string SignatureType(MetadataReader reader, ref BlobReader signature) { var code = signature.ReadSignatureTypeCode(); if (code == SignatureTypeCode.TypeHandle) { var handle = signature.ReadTypeHandle(); if (handle.Kind != HandleKind.TypeReference) throw new NotSupportedException("Only external primitive enum signatures are accepted."); var type = reader.GetTypeReference((TypeReferenceHandle)handle); if (type.ResolutionScope.Kind != HandleKind.AssemblyReference || reader.GetString(reader.GetAssemblyReference((AssemblyReferenceHandle)type.ResolutionScope).Name) != "System.Console") throw new NotSupportedException("Unsupported enum assembly."); return reader.GetString(type.Namespace) + "." + reader.GetString(type.Name); } return code switch { SignatureTypeCode.Void => "Void", SignatureTypeCode.String => "String", SignatureTypeCode.Int32 => "Int32", _ => throw new NotSupportedException("Unsupported platform signature type.") }; } private static bool IsNet10(MetadataReader reader, AssemblyDefinition assembly) { foreach (var handle in assembly.GetCustomAttributes()) { var attribute = reader.GetCustomAttribute(handle); if (attribute.Constructor.Kind != HandleKind.MemberReference) continue; var constructor = reader.GetMemberReference((MemberReferenceHandle)attribute.Constructor); if (constructor.Parent.Kind != HandleKind.TypeReference) continue; var type = reader.GetTypeReference((TypeReferenceHandle)constructor.Parent); if (reader.GetString(type.Namespace) != "System.Runtime.Versioning" || reader.GetString(type.Name) != "TargetFrameworkAttribute") continue; var value = reader.GetBlobReader(attribute.Value); return value.ReadUInt16() == 1 && value.ReadSerializedString() == ManagedRuntimeCapabilities.TargetFramework; } return false; } private static int LocalCount(MetadataReader reader, StandaloneSignatureHandle handle) { if (handle.IsNil) return 0; var signature = reader.GetBlobReader(reader.GetStandaloneSignature(handle).Signature); if (signature.ReadSignatureHeader().Kind != SignatureKind.LocalVariables) throw new BadImageFormatException("Invalid local signature."); int count = signature.ReadCompressedInteger(); if (count is < 0 or > 64) throw new NotSupportedException("Local variable limit exceeded."); for (int i = 0; i < count; i++) if (signature.ReadSignatureTypeCode() is not (SignatureTypeCode.Int32 or SignatureTypeCode.Boolean)) throw new NotSupportedException("This initial profile supports Int32/Boolean locals only."); if (signature.RemainingBytes != 0) throw new BadImageFormatException("Trailing local signature bytes."); return count; } private static object? Local(object?[] locals, int index) => index < locals.Length ? locals[index] ?? 0 : throw new BadImageFormatException("Invalid local index."); private static void SetLocal(object?[] locals, int index, object? value) { if (index >= locals.Length) throw new BadImageFormatException("Invalid local index."); locals[index] = value; } private static int ReadInt(byte[] code, ref int at) { int value = BinaryPrimitives.ReadInt32LittleEndian(code.AsSpan(at, 4)); at += 4; return value; } private static void ValidateInstructions(byte[] code) { if (code.Length > 65_536) throw new NotSupportedException("Method body size limit exceeded."); var boundaries = new HashSet(); var targets = new List(); int at = 0; while (at < code.Length) { boundaries.Add(at); byte op = code[at++]; int length = op switch { 0x00 or 0x02 or >= 0x06 and <= 0x0D or >= 0x14 and <= 0x1E or 0x25 or 0x26 or 0x2A or >= 0x58 and <= 0x5A => 0, 0x11 or 0x13 or 0x1F or >= 0x2B and <= 0x33 => 1, 0x20 or 0x28 or 0x72 or >= 0x38 and <= 0x40 => 4, _ => throw new NotSupportedException($"Unsupported IL opcode 0x{op:X2}.") }; if (code.Length - at < length) throw new BadImageFormatException("Truncated IL operand."); if (op is >= 0x2B and <= 0x33) targets.Add(at + 1 + (sbyte)code[at]); if (op is >= 0x38 and <= 0x40) targets.Add(at + 4 + BinaryPrimitives.ReadInt32LittleEndian(code.AsSpan(at, 4))); at += length; } foreach (int target in targets) if (!boundaries.Contains(target)) throw new BadImageFormatException("Branch target is outside an instruction boundary."); } }