MemoryModuleNativeAOT is an in-memory loader for .NET NativeAOT PE binaries on Windows based on NativeAOT-RunPE.
It targets NativeAOT executables and DLLs that cannot be loaded with Assembly.Load, and that typically crash when used with traditional PE memory loaders such as MemoryModule, Donut, sRDI, or pe_to_shellcode.
- Load .NET NativeAOT EXE and DLL binaries directly from memory
- Expose a MemoryModule-style C API
- Handle loader structures required by the NativeAOT runtime
- Register static TLS and unwind metadata
- Resolve exports from in-memory NativeAOT DLL images
- Support unloading with cleanup logic, including tracked TLS and FLS slot release
NativeAOT binaries include their own runtime and rely on Windows loader internals that ordinary manual mapping implementations do not reproduce.
In practice, the runtime expects:
- a valid loader entry in the PEB loader state
- membership in the loader's
BaseAddressIndextree - proper static TLS registration
- valid exception/unwind metadata registration
Without those pieces, NativeAOT startup typically ends in STATUS_FAIL_FAST_EXCEPTION (0xc0000602).
This project implements the missing loader-side behavior needed to make NativeAOT images initialize successfully from memory.
At a high level, the loader:
- Maps PE sections, applies relocations, and resolves imports
- Creates and links a synthetic
LDR_DATA_TABLE_ENTRY - Inserts the module into the loader's
BaseAddressIndexred-black tree - Registers TLS using
LdrpHandleTlsData - Hooks
TlsAlloc/TlsFreeandFlsAlloc/FlsFreeimports so runtime-owned slots can be tracked - Registers exception handling tables with
RtlAddFunctionTable - Applies final section protections
- Calls the EXE entry point or
DllMain - Resolves exports from the in-memory module
On unload, it releases tracked TLS and FLS slots, detaches TLS, unregisters unwind metadata, calls DllMain(DLL_PROCESS_DETACH) when needed, and removes the synthetic loader entry.
cmake -S . -B build
cmake --build build --config Releasetest.exe /path/to/test.dll
test.exe /path/to/test.dll --get-proc Add
test.exe /path/to/test.dll --call-int32 GetTlsValue
test.exe /path/to/test.dll --call-int32x2 Add 7 5
test.exe /path/to/test.dll --free
The public API is declared in src/memory_nativeaot.h:
MemoryNativeAOTLoadLibrary(const void* data, size_t size)MemoryNativeAOTLoadLibraryEx(const void* data, size_t size, const char* module_name_hint)MemoryNativeAOTLoadLibraryFromFileA(const char* path)MemoryNativeAOTGetProcAddress(MemoryModuleNativeAOT* module, const char* export_name)MemoryNativeAOTGetModuleBase(MemoryModuleNativeAOT* module)MemoryNativeAOTFreeLibrary(MemoryModuleNativeAOT* module)
If you unload a NativeAOT DLL after calling managed exports, ensure those exports ran on worker threads that have already exited before MemoryNativeAOTFreeLibrary. Otherwise the current thread may still hold runtime or TLS state during teardown.
Build the sample NativeAOT payloads with the .NET 8+ SDK:
cd src
dotnet publish -c Release -r win-x64The sample DLL exports:
Add(int, int)GetTlsValue()TouchRuntime()
- Windows 10 1903+ / Windows 11
- x64 only
- C++17 with a recent MSVC-compatible toolchain
- Target binaries: .NET 7+ NativeAOT (
PublishAot=true)