using System.Reflection; using System.Security.Cryptography; using System.Text.Json; using Mono.Cecil; using Mono.Cecil.Cil; using Xunit; namespace XFESystem.Desktop.Platform.Tests; public sealed class NativeInputPatchTests { private sealed record Fixture(string Root, string Runtime, string Kernel); private static Fixture Create(bool wrongCaptureSignature = false, bool misplacedHook = false) { var root = Path.Combine(AppContext.BaseDirectory, "PatchFixtures", Guid.NewGuid().ToString("N")); Directory.CreateDirectory(Path.Combine(root, "obj", "Native")); Directory.CreateDirectory(Path.Combine(root, "bin")); File.WriteAllText(Path.Combine(root, "XFESystem.Kernel.csproj"), ""); string runtimePath = Path.Combine(root, "obj", "Native", "Cosmos.Kernel.System.dll"); string kernelPath = Path.Combine(root, "bin", "XFESystem.Kernel.dll"); using var kernel = AssemblyDefinition.CreateAssembly(new("XFESystem.Kernel", new Version(1, 0)), "XFESystem.Kernel", ModuleKind.Dll); var captureType = new TypeDefinition("XFESystem.Kernel", "NativeKeyboardCapture", Mono.Cecil.TypeAttributes.Public | Mono.Cecil.TypeAttributes.Abstract | Mono.Cecil.TypeAttributes.Sealed, kernel.MainModule.TypeSystem.Object); kernel.MainModule.Types.Add(captureType); var capture = new MethodDefinition("Observe", Mono.Cecil.MethodAttributes.Public | Mono.Cecil.MethodAttributes.Static, kernel.MainModule.TypeSystem.Void); capture.Parameters.Add(new("scanCode", Mono.Cecil.ParameterAttributes.None, wrongCaptureSignature ? kernel.MainModule.TypeSystem.Int32 : kernel.MainModule.TypeSystem.Byte)); capture.Parameters.Add(new("released", Mono.Cecil.ParameterAttributes.None, kernel.MainModule.TypeSystem.Boolean)); capture.Body.Instructions.Add(Instruction.Create(OpCodes.Ret)); captureType.Methods.Add(capture); AddPointerCapture(kernel); kernel.Write(kernelPath); using var runtime = AssemblyDefinition.CreateAssembly(new("Cosmos.Kernel.System", new Version(3, 0, 94, 0)), "Cosmos.Kernel.System", ModuleKind.Dll); var keyboard = new TypeDefinition("Cosmos.Kernel.System.Input", "KeyboardManager", Mono.Cecil.TypeAttributes.Public | Mono.Cecil.TypeAttributes.Abstract | Mono.Cecil.TypeAttributes.Sealed, runtime.MainModule.TypeSystem.Object); runtime.MainModule.Types.Add(keyboard); var decoder = new MethodDefinition("HandleScanCode", Mono.Cecil.MethodAttributes.Assembly | Mono.Cecil.MethodAttributes.Static, runtime.MainModule.TypeSystem.Void); decoder.Parameters.Add(new("scanCode", Mono.Cecil.ParameterAttributes.None, runtime.MainModule.TypeSystem.Byte)); decoder.Parameters.Add(new("released", Mono.Cecil.ParameterAttributes.None, runtime.MainModule.TypeSystem.Boolean)); keyboard.Methods.Add(decoder); decoder.Body.Instructions.Add(Instruction.Create(OpCodes.Ldc_I4_7)); decoder.Body.Instructions.Add(Instruction.Create(OpCodes.Pop)); if (misplacedHook) { decoder.Body.Instructions.Add(Instruction.Create(OpCodes.Ldarg_0)); decoder.Body.Instructions.Add(Instruction.Create(OpCodes.Ldarg_1)); decoder.Body.Instructions.Add(Instruction.Create(OpCodes.Call, runtime.MainModule.ImportReference(capture))); } decoder.Body.Instructions.Add(Instruction.Create(OpCodes.Ret)); AddPointerDecoder(runtime); runtime.Write(runtimePath); return new(root, runtimePath, kernelPath); } private static void AddPointerCapture(AssemblyDefinition kernel) { var type = new TypeDefinition("XFESystem.Kernel", "NativePointerCapture", Mono.Cecil.TypeAttributes.Public | Mono.Cecil.TypeAttributes.Abstract | Mono.Cecil.TypeAttributes.Sealed, kernel.MainModule.TypeSystem.Object); kernel.MainModule.Types.Add(type); var capture = new MethodDefinition("Observe", Mono.Cecil.MethodAttributes.Public | Mono.Cecil.MethodAttributes.Static, kernel.MainModule.TypeSystem.Void); foreach (var parameter in new[] { kernel.MainModule.TypeSystem.Int32, kernel.MainModule.TypeSystem.Int32, kernel.MainModule.TypeSystem.Boolean, kernel.MainModule.TypeSystem.Boolean, kernel.MainModule.TypeSystem.Boolean, kernel.MainModule.TypeSystem.Int32, kernel.MainModule.TypeSystem.Int64 }) capture.Parameters.Add(new(parameter)); capture.Body.Instructions.Add(Instruction.Create(OpCodes.Ret)); type.Methods.Add(capture); var clock = new MethodDefinition("ReadTimestamp", Mono.Cecil.MethodAttributes.Public | Mono.Cecil.MethodAttributes.Static, kernel.MainModule.TypeSystem.Int64); clock.Body.Instructions.Add(Instruction.Create(OpCodes.Ldc_I8, 123L)); clock.Body.Instructions.Add(Instruction.Create(OpCodes.Ret)); type.Methods.Add(clock); } private static void AddPointerDecoder(AssemblyDefinition runtime) { var buttons = new TypeDefinition("Cosmos.Kernel.HAL.Devices.Input", "PointerButtons", Mono.Cecil.TypeAttributes.Public | Mono.Cecil.TypeAttributes.Sealed, runtime.MainModule.ImportReference(typeof(Enum))); buttons.Fields.Add(new("value__", Mono.Cecil.FieldAttributes.Public | Mono.Cecil.FieldAttributes.SpecialName | Mono.Cecil.FieldAttributes.RTSpecialName, runtime.MainModule.TypeSystem.Int32)); runtime.MainModule.Types.Add(buttons); var mouse = new TypeDefinition("Cosmos.Kernel.System.Input", "MouseManager", Mono.Cecil.TypeAttributes.Public | Mono.Cecil.TypeAttributes.Abstract | Mono.Cecil.TypeAttributes.Sealed, runtime.MainModule.TypeSystem.Object); runtime.MainModule.Types.Add(mouse); foreach (string property in new[] { "X", "Y", "LeftButton", "RightButton", "MiddleButton" }) { var getter = new MethodDefinition("get_" + property, Mono.Cecil.MethodAttributes.Public | Mono.Cecil.MethodAttributes.Static, property is "X" or "Y" ? runtime.MainModule.TypeSystem.Int32 : runtime.MainModule.TypeSystem.Boolean); getter.Body.Instructions.Add(Instruction.Create(OpCodes.Ldc_I4_0)); getter.Body.Instructions.Add(Instruction.Create(OpCodes.Ret)); mouse.Methods.Add(getter); } foreach (string name in new[] { "HandleRelative", "HandleAbsolute" }) { var decoder = new MethodDefinition(name, Mono.Cecil.MethodAttributes.Assembly | Mono.Cecil.MethodAttributes.Static, runtime.MainModule.TypeSystem.Void); decoder.Parameters.Add(new(runtime.MainModule.TypeSystem.Int32)); decoder.Parameters.Add(new(runtime.MainModule.TypeSystem.Int32)); decoder.Parameters.Add(new(buttons)); if (name == "HandleRelative") decoder.Parameters.Add(new(runtime.MainModule.TypeSystem.Int32)); var firstReturn = Instruction.Create(OpCodes.Ret); var secondReturn = Instruction.Create(OpCodes.Ret); decoder.Body.Instructions.Add(Instruction.Create(OpCodes.Ldarg_0)); decoder.Body.Instructions.Add(Instruction.Create(OpCodes.Brfalse_S, firstReturn)); decoder.Body.Instructions.Add(Instruction.Create(OpCodes.Ldc_I4_7)); decoder.Body.Instructions.Add(Instruction.Create(OpCodes.Pop)); decoder.Body.Instructions.Add(Instruction.Create(OpCodes.Br_S, secondReturn)); decoder.Body.Instructions.Add(firstReturn); decoder.Body.Instructions.Add(Instruction.Create(OpCodes.Ldc_I4_8)); decoder.Body.Instructions.Add(Instruction.Create(OpCodes.Pop)); decoder.Body.Instructions.Add(secondReturn); mouse.Methods.Add(decoder); } } private static void RunPatch(string runtime, string kernel) { var main = Assembly.Load("NativeInputPatch").EntryPoint!; try { main.Invoke(null, [new[] { runtime, kernel }]); } catch (TargetInvocationException error) when (error.InnerException is not null) { System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(error.InnerException).Throw(); } } private static string Hash(string path) => Convert.ToHexStringLower(SHA256.HashData(File.ReadAllBytes(path))); [Fact] public void PrivateCopyGetsOnePrefixAndOriginalDecoderInstructionsRemain() { var fixture = Create(); string kernelHash = Hash(fixture.Kernel), original = Hash(fixture.Runtime); RunPatch(fixture.Runtime, fixture.Kernel); using var patched = AssemblyDefinition.ReadAssembly(fixture.Runtime); var decoder = patched.MainModule.Types.Single(t => t.Name == "KeyboardManager").Methods.Single(); Assert.Equal([OpCodes.Ldarg_0, OpCodes.Ldarg_1, OpCodes.Call, OpCodes.Ldc_I4_7, OpCodes.Pop, OpCodes.Ret], decoder.Body.Instructions.Select(i => i.OpCode).ToArray()); var call = Assert.IsAssignableFrom(decoder.Body.Instructions[2].Operand); Assert.Equal("XFESystem.Kernel.NativeKeyboardCapture", call.DeclaringType.FullName); Assert.Equal("Observe", call.Name); Assert.Equal(kernelHash, Hash(fixture.Kernel)); using var receipt = JsonDocument.Parse(File.ReadAllText(fixture.Runtime + ".XfeInput.json")); Assert.Equal(original, receipt.RootElement.GetProperty("BeforeSha256").GetString()); Assert.True(receipt.RootElement.GetProperty("OriginalDecoderPreserved").GetBoolean()); Assert.False(receipt.RootElement.GetProperty("GlobalPackageCacheModified").GetBoolean()); } [Fact] public void RepeatedPatchingIsByteForByteIdempotent() { var fixture = Create(); RunPatch(fixture.Runtime, fixture.Kernel); string first = Hash(fixture.Runtime); RunPatch(fixture.Runtime, fixture.Kernel); Assert.Equal(first, Hash(fixture.Runtime)); } [Fact] public void RuntimeOutsidePrivateObjIsRejectedWithoutMutation() { var fixture = Create(); string outside = Path.Combine(fixture.Root, "Cosmos.Kernel.System.dll"); File.Copy(fixture.Runtime, outside); string original = Hash(outside); Assert.Throws(() => RunPatch(outside, fixture.Kernel)); Assert.Equal(original, Hash(outside)); Assert.False(File.Exists(outside + ".XfeInput.json")); } [Fact] public void IncorrectObserverSignatureIsRejectedWithoutMutation() { var fixture = Create(wrongCaptureSignature: true); string original = Hash(fixture.Runtime); Assert.Throws(() => RunPatch(fixture.Runtime, fixture.Kernel)); Assert.Equal(original, Hash(fixture.Runtime)); } [Fact] public void UnexpectedExistingHookIsRejectedWithoutMutation() { var fixture = Create(misplacedHook: true); string original = Hash(fixture.Runtime); Assert.Throws(() => RunPatch(fixture.Runtime, fixture.Kernel)); Assert.Equal(original, Hash(fixture.Runtime)); } [Fact] public void EveryPointerNormalReturnObservesFinalPropertiesPacketWheelAndMonotonicClock() { var fixture = Create(); RunPatch(fixture.Runtime, fixture.Kernel); using var patched = AssemblyDefinition.ReadAssembly(fixture.Runtime); var mouse = patched.MainModule.Types.Single(t => t.Name == "MouseManager"); foreach (var decoder in mouse.Methods.Where(m => m.Name.StartsWith("Handle", StringComparison.Ordinal))) { var returns = decoder.Body.Instructions.Where(i => i.OpCode == OpCodes.Ret).ToArray(); Assert.Equal(2, returns.Length); foreach (var ret in returns) { int index = decoder.Body.Instructions.IndexOf(ret); Assert.Equal(OpCodes.Nop, decoder.Body.Instructions[index - 9].OpCode); Assert.Equal(new[] { "get_X", "get_Y", "get_LeftButton", "get_RightButton", "get_MiddleButton" }, decoder.Body.Instructions.Skip(index - 8).Take(5).Select(i => ((MethodReference)i.Operand).Name)); Assert.Equal(decoder.Name == "HandleRelative" ? OpCodes.Ldarg_3 : OpCodes.Ldc_I4_0, decoder.Body.Instructions[index - 3].OpCode); Assert.Equal("ReadTimestamp", ((MethodReference)decoder.Body.Instructions[index - 2].Operand).Name); Assert.Equal("XFESystem.Kernel.NativePointerCapture", ((MethodReference)decoder.Body.Instructions[index - 1].Operand).DeclaringType.FullName); } var branches = decoder.Body.Instructions.Where(i => i.OpCode == OpCodes.Brfalse || i.OpCode == OpCodes.Br).ToArray(); Assert.Equal(2, branches.Length); Assert.All(branches, branch => Assert.Equal(OpCodes.Nop, ((Instruction)branch.Operand).OpCode)); Assert.Equal(2, decoder.Body.Instructions.Count(i => i.OpCode == OpCodes.Pop)); Assert.Contains(decoder.Body.Instructions, i => i.OpCode == OpCodes.Ldc_I4_7); Assert.Contains(decoder.Body.Instructions, i => i.OpCode == OpCodes.Ldc_I4_8); } using var receipt = JsonDocument.Parse(File.ReadAllText(fixture.Runtime + ".XfeInput.json")); Assert.True(receipt.RootElement.GetProperty("PointerDecoderPreserved").GetBoolean()); Assert.Equal(2, receipt.RootElement.GetProperty("PointerHooks").GetArrayLength()); } [Fact] public void MalformedPointerCaptureRejectsBeforeChangingAnyDecoderBytes() { var fixture = Create(); using (var kernel = AssemblyDefinition.ReadAssembly(fixture.Kernel, new ReaderParameters { InMemory = true })) { kernel.MainModule.Types.Single(t => t.Name == "NativePointerCapture").Methods.Single(m => m.Name == "Observe").Parameters.RemoveAt(6); kernel.Write(fixture.Kernel); } string original = Hash(fixture.Runtime); Assert.Throws(() => RunPatch(fixture.Runtime, fixture.Kernel)); Assert.Equal(original, Hash(fixture.Runtime)); Assert.False(File.Exists(fixture.Runtime + ".XfeInput.json")); } [Fact] public void ActualCosmos394PrivateCopyRetainsDecodedInstructionsAndIsIdempotent() { var fixture = Create(); string packages = Environment.GetEnvironmentVariable("NUGET_PACKAGES") ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "packages"); string upstream = Path.Combine(packages, "cosmos.kernel.system", "3.0.94", "runtimes", "linux-x64", "lib", "net10.0", "Cosmos.Kernel.System.dll"); string upstreamHash = Hash(upstream); File.Copy(upstream, fixture.Runtime, true); File.Copy(typeof(XFESystem.Kernel.NativePointerCapture).Assembly.Location, fixture.Kernel, true); using var original = AssemblyDefinition.ReadAssembly(fixture.Runtime, new ReaderParameters { InMemory = true }); var expected = original.MainModule.Types.Single(t => t.Name == "MouseManager").Methods.Where(m => m.Name is "HandleRelative" or "HandleAbsolute") .ToDictionary(m => m.Name, m => NormalizeDecoder(m)); var originalKeyboard = NormalizeDecoder(original.MainModule.Types.Single(t => t.Name == "KeyboardManager").Methods.Single(m => m.Name == "HandleScanCode")); RunPatch(fixture.Runtime, fixture.Kernel); string firstHash = Hash(fixture.Runtime); using (var patched = AssemblyDefinition.ReadAssembly(fixture.Runtime)) { foreach (var decoder in patched.MainModule.Types.Single(t => t.Name == "MouseManager").Methods.Where(m => expected.ContainsKey(m.Name))) Assert.Equal(expected[decoder.Name], NormalizeDecoder(decoder)); Assert.Equal(originalKeyboard, NormalizeDecoder(patched.MainModule.Types.Single(t => t.Name == "KeyboardManager").Methods.Single(m => m.Name == "HandleScanCode"))); } RunPatch(fixture.Runtime, fixture.Kernel); Assert.Equal(firstHash, Hash(fixture.Runtime)); Assert.Equal(upstreamHash, Hash(upstream)); } private static string[] NormalizeDecoder(MethodDefinition method) { var original = new List<(Instruction Instruction, OpCode Code)>(); var body = method.Body.Instructions; for (int index = 0; index < body.Count; index++) { if (index == 0 && body.Count >= 3 && body[2].Operand is MethodReference keyboard && keyboard.DeclaringType.FullName == "XFESystem.Kernel.NativeKeyboardCapture") { index = 2; continue; } if (body[index].OpCode == OpCodes.Nop && index + 9 < body.Count && body[index + 8].Operand is MethodReference pointer && pointer.DeclaringType.FullName == "XFESystem.Kernel.NativePointerCapture" && pointer.Name == "Observe" && body[index + 9].OpCode == OpCodes.Ret) { original.Add((body[index], OpCodes.Ret)); index += 9; continue; } original.Add((body[index], body[index].OpCode)); } var positions = original.Select((item, index) => (item.Instruction, index)).ToDictionary(item => item.Instruction, item => item.index); return original.Select(item => { string opcode = item.Code.Name; if (item.Code.OperandType == OperandType.ShortInlineBrTarget) opcode = opcode[..^2]; string operand = item.Code == OpCodes.Ret ? "" : item.Instruction.Operand switch { Instruction branch => "target:" + positions[branch], Instruction[] branches => "targets:" + string.Join(',', branches.Select(branch => positions[branch])), MethodReference called => called.FullName, FieldReference field => field.FullName, TypeReference type => type.FullName, ParameterDefinition parameter => "arg:" + parameter.Index, VariableDefinition variable => "local:" + variable.Index, null => "", var value => value.ToString() ?? "" }; return opcode + "|" + operand; }).ToArray(); } }