Description
Background and motivation
As many other users who asking to run things in DllMain under NativeAOT, I'm using NativeAOT to inject code into a game, and I very much want to stay within the .NET ecosystem and avoid writing a separate C++ library shim just for the injection bit.
From what I've read, NativeAOT doesn't support runtime initialization under loader lock. However, I've also seen samples of runtime-less code, which is limited, but still usable for simple tasks with enough boilerplate (https://github.com/MichalStrehovsky/zerosharp).
A minimal hook injection involves calling a few LibraryImport
-ed Win32 APIs (GetModuleHandle, VirtualAlloc/Protect
), taking an address of an UnmanagedCallersOnly
function, and writing to some pointers. All of this is doable without a managed runtime, and it probably doesn't violate loader lock rules.
To facilitate this scenario while sticking to the rules imposed by NativeAOT and the OS, I'm proposing two attributes. The first:
- Would apply to an
UnmanagedCallersOnly
method, - Would restrict the method's code to no-runtime-compatible operations and calls to other "runtimeless" methods,
- Would compile the method directly from IL to bytecode (or minimize whatever thunks are usually inserted for reverse P/Invokes, to bring the code down to the bare metal as much as possible),
- Would allow unmanaged callers to invoke this function directly, without initializing the runtime if it hasn't yet been initialized,
The second:
- Would apply to a method marked with the first attribute,
- Would only apply to a single, parameterless method in the module,
- Would, allow insertion of a call to this method into what's currently a no-op DllMain.
API Proposal
namespace System.Runtime.InteropServices;
[AttributeUsage(AttributeTargets.Method)]
public sealed class UnmanagedMethod : Attribute;
[AttributeUsage(AttributeTargets.Method)]
public sealed class UnmanagedModuleInitializer : Attribute;
API Usage
public static unsafe class Example
{
[DllImport("kernel32.dll")]
private static extern uint GetModuleHandleW(char* moduleName);
[DllImport("kernel32.dll")]
private static extern uint VirtualProtect(uint address, uint size, uint newProtect, out uint oldProtect);
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvStdcall)])]
[UnmanagedMethod]
[UnmanagedModuleInitializer]
public static void OnStartup()
{
// Call to PeekMessageA in gta_sa.exe's main loop
var originalInstructionPtr = GetModuleHandleW(null) + (0x748A57 - 0x400000);
var newFunctionPtr = (uint)(delegate* unmanaged[Stdcall]<void*, nint, uint, uint, uint, int>)&NotPeekMessageA;
VirtualProtect(originalInstructionPtr, 6, 0x04, out var oldProtect);
*(byte*)originalInstructionPtr = 0xE8;
*(uint*)(originalInstructionPtr + 1) = newFunctionPtr - (originalInstructionPtr + 5);
VirtualProtect(originalInstructionPtr, 6, oldProtect, out _);
}
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvStdcall)])]
private static int NotPeekMessageA(void* lpMsg, nint hWnd, uint wMsgFilterMin, uint wMsgFilterMax, uint wRemoveMsg)
{
return 0; // Install/proc custom synchronization context, then return from real PeekMessageA
}
}
Alternative Designs
It might also be possible to reduce the number of attributes - for instance, add another parameter to UnmanagedCallersOnly
instead of designating a separate UnmanagedMethod
attribute.
Risks
Users will now be able to cause undefined behavior if their runtimeless code violates loader locks in other ways (e.g., LoadLibrary
).
Metadata
Metadata
Assignees
Type
Projects
Status