Files

1036 lines
46 KiB
C

// Userspace PE module registry and dependency loader. Kernel process objects know only VADs.
#include <ntlite/atomic.h>
#include <intrin.h>
#include <ntlite/module_loader.h>
#include <ntlite/pe_loader.h>
#include <ntlite/process_loader.h>
#include <ntlite/syscall.h>
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
#include <string.h>
#include "module_loader_internal.h"
#define NTL_MODULE_CONTEXT_MAGIC 0x4D4F444Cu
#define NTL_MODULE_PAGE_SIZE 4096u
#define NTL_LOADER_DATA_MAGIC 0x30444C4Eu
#define NTL_LOADER_DATA_VERSION 4u
typedef struct NTL_LOADER_IMAGE_RECORD {
uintptr_t Base;
size_t Size;
uintptr_t EntryPoint;
char Name[NTL_MODULE_NAME_MAX];
} NTL_LOADER_IMAGE_RECORD;
typedef struct NTL_LOADER_STRING_RECORD {
uint32_t Offset;
uint32_t Length;
} NTL_LOADER_STRING_RECORD;
typedef struct NTL_LOADER_DATA {
uint32_t Magic;
uint32_t Version;
uint32_t Size;
uint32_t ImageCount;
uint32_t CurrentDirectoryOffset;
uint32_t CurrentDirectoryLength;
uint32_t StartupDataOffset;
uint32_t StartupDataLength;
uint32_t ArgumentVectorOffset;
uint32_t ArgumentCount;
uint32_t EnvironmentVectorOffset;
uint32_t EnvironmentCount;
NTL_LOADER_IMAGE_RECORD Images[];
} NTL_LOADER_DATA;
typedef struct NTL_MODULE_ENTRY {
struct NTL_MODULE_ENTRY* Next;
uintptr_t Base;
size_t Size;
uintptr_t EntryPoint;
uint32_t PublicRefCount;
uint32_t ImportPinned;
uint32_t Initial;
char Name[NTL_MODULE_NAME_MAX];
} NTL_MODULE_ENTRY;
struct NTL_MODULE_CONTEXT {
uint32_t Magic;
SYS_HANDLE ProcessHandle;
SYS_HANDLE PrimaryModuleRootHandle;
SYS_STRING_VIEW PrimaryModuleDirectory;
SYS_HANDLE SecondaryModuleRootHandle;
SYS_STRING_VIEW SecondaryModuleDirectory;
SYS_HANDLE FallbackModuleRootHandle;
SYS_STRING_VIEW FallbackModuleDirectory;
NTL_MODULE_NAME_RESOLVER Resolver;
void* ResolverContext;
NTL_MODULE_ENTRY* Head;
uint32_t Count;
uint32_t OwnsStorage;
};
static NTL_MODULE_CONTEXT gCurrentContext;
static volatile int32_t gCurrentContextState;
static volatile int32_t gCurrentContextBusy;
// Converts one ASCII character to lowercase for module-name comparisons.
static char NtlpLower(char c) {
return (c >= 'A' && c <= 'Z') ? (char)(c - 'A' + 'a') : c;
}
// Compares one string view with a module name without case sensitivity.
static bool NtlpViewEqualsNameI(SYS_STRING_VIEW view, const char* name) {
if (!name) return false;
size_t n = strlen(name);
if (view.Length != n) return false;
for (size_t i = 0; i < n; ++i) {
if (NtlpLower(view.Data[i]) != NtlpLower(name[i])) return false;
}
return true;
}
// Allocates page-backed userspace metadata and clears the complete allocation.
static KSTATUS NtlpAllocZero(size_t bytes, void** out) {
if (!bytes || !out) return NTLSTATUS_INVALID_PARAMETER;
*out = 0;
if (bytes > SIZE_MAX - (NTL_MODULE_PAGE_SIZE - 1u)) return NTLSTATUS_NO_MEMORY;
size_t pages = (bytes + NTL_MODULE_PAGE_SIZE - 1u) / NTL_MODULE_PAGE_SIZE;
SYS_ALLOC_VM_REQUEST request;
memset(&request, 0, sizeof(request));
request.Size = sizeof(request);
request.Protect = SYS_VM_READWRITE;
request.Flags = SYS_VM_FLAG_NONE;
request.PageCount = pages;
request.BaseOut = out;
KSTATUS status = SysAllocVm(&request);
if (KSUCCESS(status)) memset(*out, 0, pages * NTL_MODULE_PAGE_SIZE);
return status;
}
// Releases page-backed userspace metadata.
static void NtlpFree(void* p) {
if (p) (void)SysFreeVm(p);
}
// Copies and validates one basename-only module identity.
static KSTATUS NtlpCopyName(SYS_STRING_VIEW name, char out[NTL_MODULE_NAME_MAX]) {
if (!SysSvIsValid(name) || name.Length == 0 || name.Length >= NTL_MODULE_NAME_MAX ||
SysSvContainsByte(name, '\0'))
return NTLSTATUS_INVALID_PARAMETER;
for (size_t i = 0; i < name.Length; ++i) {
if (name.Data[i] == '/' || name.Data[i] == '\\') return NTLSTATUS_INVALID_PARAMETER;
}
memcpy(out, name.Data, name.Length);
out[name.Length] = '\0';
return NTLSTATUS_SUCCESS;
}
// Returns the final native path component without modifying the source view.
static SYS_STRING_VIEW NtlpBasename(SYS_STRING_VIEW path) {
if (!SysSvIsValid(path)) return SysSvFromParts((const char*)0, 0);
size_t start = 0;
for (size_t i = 0; i < path.Length; ++i) {
if (path.Data[i] == '/') start = i + 1u;
}
return SysSvFromParts(path.Data + start, path.Length - start);
}
// Finds one process-local module by canonical case-insensitive name.
static NTL_MODULE_ENTRY* NtlpFindName(NTL_MODULE_CONTEXT* context, SYS_STRING_VIEW name) {
for (NTL_MODULE_ENTRY* entry = context ? context->Head : 0; entry; entry = entry->Next) {
if (NtlpViewEqualsNameI(name, entry->Name)) return entry;
}
return 0;
}
// Resolves one opaque userspace module handle against the live registry.
static NTL_MODULE_ENTRY* NtlpFindHandle(NTL_MODULE_CONTEXT* context, NTL_MODULE_HANDLE handle) {
if (!context || handle == NTL_MODULE_HANDLE_INVALID) return 0;
NTL_MODULE_ENTRY* wanted = (NTL_MODULE_ENTRY*)(uintptr_t)handle;
for (NTL_MODULE_ENTRY* entry = context->Head; entry; entry = entry->Next) {
if (entry == wanted) return entry;
}
return 0;
}
// Copies one private module entry into the public userspace information shape.
static void NtlpFillInfo(const NTL_MODULE_ENTRY* entry, NTL_MODULE_INFO* info) {
if (!entry || !info) return;
memset(info, 0, sizeof(*info));
info->Base = entry->Base;
info->Size = entry->Size;
info->EntryPoint = entry->EntryPoint;
info->PublicRefCount = entry->PublicRefCount;
info->ImportPinned = entry->ImportPinned;
strncpy(info->Name, entry->Name, sizeof(info->Name) - 1u);
}
// Preserves an import name when the caller already uses canonical module names.
KSTATUS NtlResolveModuleNameIdentity(void* context,
SYS_STRING_VIEW requestedName,
SYS_STRING_VIEW* canonicalNameOut) {
(void)context;
if (!canonicalNameOut || !SysSvIsValid(requestedName) || requestedName.Length == 0)
return NTLSTATUS_INVALID_PARAMETER;
*canonicalNameOut = requestedName;
return NTLSTATUS_SUCCESS;
}
// Resolves installed personality compatibility aliases for generic launchers.
KSTATUS NtlResolveSystemModuleName(void* context,
SYS_STRING_VIEW requestedName,
SYS_STRING_VIEW* canonicalNameOut) {
(void)context;
if (!canonicalNameOut || !SysSvIsValid(requestedName) || requestedName.Length == 0)
return NTLSTATUS_INVALID_PARAMETER;
#define NTL_SYSTEM_ALIAS(name, target) do { \
if (NtlpViewEqualsNameI(requestedName, name)) { \
*canonicalNameOut = SYS_SV_LITERAL(target); \
return NTLSTATUS_SUCCESS; \
} \
} while (0)
NTL_SYSTEM_ALIAS("kernel32.dll", "win32.dll");
NTL_SYSTEM_ALIAS("user32.dll", "win32.dll");
NTL_SYSTEM_ALIAS("gdi32.dll", "win32.dll");
NTL_SYSTEM_ALIAS("ole32.dll", "win32.dll");
NTL_SYSTEM_ALIAS("winmm.dll", "win32.dll");
NTL_SYSTEM_ALIAS("msvcrt.dll", "posix.dll");
#undef NTL_SYSTEM_ALIAS
*canonicalNameOut = requestedName;
return NTLSTATUS_SUCCESS;
}
// Applies caller-owned import policy and validates the resulting module basename.
static KSTATUS NtlpCanonicalName(NTL_MODULE_CONTEXT* context,
SYS_STRING_VIEW requested,
SYS_STRING_VIEW* canonicalOut) {
if (!context || !canonicalOut) return NTLSTATUS_INVALID_PARAMETER;
KSTATUS status = context->Resolver
? context->Resolver(context->ResolverContext, requested, canonicalOut)
: NtlResolveModuleNameIdentity(0, requested, canonicalOut);
if (!KSUCCESS(status)) return status;
char scratch[NTL_MODULE_NAME_MAX];
return NtlpCopyName(*canonicalOut, scratch);
}
// Replaces the final component of an explicit module path with its canonical policy name.
static KSTATUS NtlpBuildCanonicalPath(SYS_STRING_VIEW path,
SYS_STRING_VIEW canonical,
char out[SYS_PATH_MAX],
SYS_STRING_VIEW* pathOut) {
if (!pathOut || !out || !SysSvIsValid(path) || path.Length == 0)
return NTLSTATUS_INVALID_PARAMETER;
char name[NTL_MODULE_NAME_MAX];
KSTATUS status = NtlpCopyName(canonical, name);
if (!KSUCCESS(status)) return status;
SYS_STRING_VIEW base = NtlpBasename(path);
if (!base.Data || base.Length == 0) return NTLSTATUS_INVALID_PARAMETER;
size_t prefix = (size_t)(base.Data - path.Data);
if (prefix > path.Length || canonical.Length > SYS_PATH_MAX - 1u - prefix)
return NTLSTATUS_BUFFER_TOO_SMALL;
memcpy(out, path.Data, prefix);
memcpy(out + prefix, canonical.Data, canonical.Length);
size_t length = prefix + canonical.Length;
out[length] = '\0';
*pathOut = SysSvFromParts(out, length);
return NTLSTATUS_SUCCESS;
}
// Opens one canonical dependency from a single capability-relative search location.
static KSTATUS NtlpOpenDependencyAt(SYS_HANDLE rootHandle,
SYS_STRING_VIEW directory,
SYS_STRING_VIEW canonical,
SYS_HANDLE* fileOut) {
if (rootHandle == SYS_HANDLE_INVALID || !fileOut || !SysSvIsValid(directory) ||
SysSvContainsByte(directory, '\0'))
return NTLSTATUS_INVALID_PARAMETER;
*fileOut = SYS_HANDLE_INVALID;
char path[SYS_PATH_MAX];
char name[NTL_MODULE_NAME_MAX];
KSTATUS status = NtlpCopyName(canonical, name);
if (!KSUCCESS(status)) return status;
size_t dirLength = directory.Length;
size_t total = dirLength + (dirLength ? 1u : 0u) + canonical.Length;
if (total + 1u > sizeof(path)) return NTLSTATUS_BUFFER_TOO_SMALL;
if (dirLength) memcpy(path, directory.Data, dirLength);
size_t pos = dirLength;
if (dirLength) path[pos++] = '/';
memcpy(path + pos, canonical.Data, canonical.Length);
path[total] = '\0';
SYS_OPEN_FILE_AT_REQUEST open = SysMakeOpenFileAtRequest(rootHandle, SysSvFromCstr(path),
SYS_FILE_ACCESS_READ | SYS_FILE_ACCESS_QUERY,
SYS_FILE_OPEN_EXISTING,
SYS_FILE_FLAG_NONE, fileOut);
return SysOpenFileAt(&open);
}
// Searches the available current-directory root, then image-private and configured fallback roots.
static KSTATUS NtlpOpenDependency(NTL_MODULE_CONTEXT* context,
SYS_STRING_VIEW canonical,
SYS_HANDLE* fileOut) {
if (!context || !fileOut) return NTLSTATUS_INVALID_PARAMETER;
KSTATUS status = NtlpOpenDependencyAt(context->PrimaryModuleRootHandle,
context->PrimaryModuleDirectory, canonical, fileOut);
if (status != NTLSTATUS_NOT_FOUND) return status;
if (context->SecondaryModuleRootHandle != SYS_HANDLE_INVALID) {
status = NtlpOpenDependencyAt(context->SecondaryModuleRootHandle,
context->SecondaryModuleDirectory, canonical, fileOut);
if (status != NTLSTATUS_NOT_FOUND) return status;
}
if (context->FallbackModuleRootHandle == SYS_HANDLE_INVALID) return NTLSTATUS_NOT_FOUND;
return NtlpOpenDependencyAt(context->FallbackModuleRootHandle,
context->FallbackModuleDirectory, canonical, fileOut);
}
// Publishes a mapped image before import binding so cyclic dependencies can resolve it.
static KSTATUS NtlpPreparedModule(void* opaque, const NTL_PE_IMAGE* image) {
NTL_MODULE_ENTRY* entry = (NTL_MODULE_ENTRY*)opaque;
if (!entry || !image || !image->Base || !image->Size) return NTLSTATUS_INVALID_PARAMETER;
entry->Base = image->Base;
entry->Size = image->Size;
entry->EntryPoint = image->EntryPoint;
return NTLSTATUS_SUCCESS;
}
// Resolves one imported symbol through the recursive userspace dependency registry.
static KSTATUS NtlpResolveImport(void* opaque,
SYS_STRING_VIEW moduleName,
SYS_STRING_VIEW symbolName,
uintptr_t* addressOut);
// Loads or reuses one dependency and import-pins it for the process lifetime.
static KSTATUS NtlpLoadDependency(NTL_MODULE_CONTEXT* context,
SYS_STRING_VIEW requested,
NTL_MODULE_ENTRY** entryOut) {
if (!context || !entryOut) return NTLSTATUS_INVALID_PARAMETER;
*entryOut = 0;
SYS_STRING_VIEW canonical;
KSTATUS status = NtlpCanonicalName(context, requested, &canonical);
if (!KSUCCESS(status)) return status;
NTL_MODULE_ENTRY* existing = NtlpFindName(context, canonical);
if (existing) {
existing->ImportPinned = 1u;
*entryOut = existing;
return NTLSTATUS_SUCCESS;
}
NTL_MODULE_ENTRY* entry = 0;
status = NtlpAllocZero(sizeof(*entry), (void**)&entry);
if (!KSUCCESS(status)) return status;
status = NtlpCopyName(canonical, entry->Name);
if (!KSUCCESS(status)) { NtlpFree(entry); return status; }
entry->ImportPinned = 1u;
entry->Next = context->Head;
context->Head = entry;
context->Count++;
SYS_HANDLE file = SYS_HANDLE_INVALID;
status = NtlpOpenDependency(context, canonical, &file);
if (KSUCCESS(status)) {
NTL_PE_IMAGE image;
memset(&image, 0, sizeof(image));
NTL_PE_LOAD_REQUEST load;
memset(&load, 0, sizeof(load));
load.Size = sizeof(load);
load.ProcessHandle = context->ProcessHandle;
load.FileHandle = file;
load.Flags = NTL_PE_LOAD_FLAG_NONE;
load.ResolveImport = NtlpResolveImport;
load.ResolveContext = context;
load.Prepared = NtlpPreparedModule;
load.PreparedContext = entry;
load.ImageOut = &image;
status = NtlLoadPeImage(&load);
(void)SysCloseHandle(file);
}
if (!KSUCCESS(status)) {
NTL_MODULE_ENTRY** link = &context->Head;
while (*link && *link != entry) link = &(*link)->Next;
if (*link == entry) *link = entry->Next;
if (context->Count) context->Count--;
NtlpFree(entry);
return status;
}
*entryOut = entry;
return NTLSTATUS_SUCCESS;
}
static KSTATUS NtlpResolveImport(void* opaque,
SYS_STRING_VIEW moduleName,
SYS_STRING_VIEW symbolName,
uintptr_t* addressOut) {
if (!opaque || !addressOut) return NTLSTATUS_INVALID_PARAMETER;
*addressOut = 0;
NTL_MODULE_ENTRY* entry = 0;
KSTATUS status = NtlpLoadDependency((NTL_MODULE_CONTEXT*)opaque, moduleName, &entry);
if (!KSUCCESS(status)) return status;
if (!entry->Base) return NTLSTATUS_WOULD_BLOCK;
return NtlGetPeExport(entry->Base, symbolName, addressOut);
}
// Creates loader state for a child process or seeds the current process from PEB loader data.
KSTATUS NtlpCreateModuleContext(SYS_HANDLE processHandle,
SYS_HANDLE primaryModuleRootHandle,
SYS_STRING_VIEW primaryModuleDirectory,
SYS_HANDLE secondaryModuleRootHandle,
SYS_STRING_VIEW secondaryModuleDirectory,
SYS_HANDLE fallbackModuleRootHandle,
SYS_STRING_VIEW fallbackModuleDirectory,
NTL_MODULE_NAME_RESOLVER resolver,
void* resolverContext,
bool seedCurrentProcess,
NTL_MODULE_CONTEXT** contextOut) {
if (!contextOut || primaryModuleRootHandle == SYS_HANDLE_INVALID ||
!SysSvIsValid(primaryModuleDirectory) || SysSvContainsByte(primaryModuleDirectory, '\0') ||
(secondaryModuleRootHandle != SYS_HANDLE_INVALID &&
(!SysSvIsValid(secondaryModuleDirectory) || SysSvContainsByte(secondaryModuleDirectory, '\0'))) ||
(fallbackModuleRootHandle != SYS_HANDLE_INVALID &&
(!SysSvIsValid(fallbackModuleDirectory) || SysSvContainsByte(fallbackModuleDirectory, '\0'))))
return NTLSTATUS_INVALID_PARAMETER;
*contextOut = 0;
NTL_MODULE_CONTEXT* context = 0;
KSTATUS status = NtlpAllocZero(sizeof(*context), (void**)&context);
if (!KSUCCESS(status)) return status;
context->Magic = NTL_MODULE_CONTEXT_MAGIC;
context->ProcessHandle = processHandle;
context->PrimaryModuleRootHandle = primaryModuleRootHandle;
context->PrimaryModuleDirectory = primaryModuleDirectory;
context->SecondaryModuleRootHandle = secondaryModuleRootHandle;
context->SecondaryModuleDirectory = secondaryModuleDirectory;
context->FallbackModuleRootHandle = fallbackModuleRootHandle;
context->FallbackModuleDirectory = fallbackModuleDirectory;
context->Resolver = resolver;
context->ResolverContext = resolverContext;
context->OwnsStorage = 1u;
if (seedCurrentProcess) {
SYS_PEB* peb = SysGetPeb();
if (!peb || peb->Magic != SYS_PEB_MAGIC || peb->Version != SYS_ENV_VERSION) {
NtlpFree(context);
return NTLSTATUS_FAILURE;
}
if (peb->LoaderData) {
SYS_VM_REGION_INFO region;
memset(&region, 0, sizeof(region));
status = SysQueryVm((void*)peb->LoaderData, &region);
if (!KSUCCESS(status) || !region.Base || !region.PageCount) {
NtlpFree(context);
return NTLSTATUS_FAILURE;
}
const NTL_LOADER_DATA* data = (const NTL_LOADER_DATA*)peb->LoaderData;
if (region.PageCount > SIZE_MAX / NTL_MODULE_PAGE_SIZE) {
NtlpFree(context);
return NTLSTATUS_FAILURE;
}
size_t regionBytes = region.PageCount * NTL_MODULE_PAGE_SIZE;
uintptr_t regionBase = (uintptr_t)region.Base;
uintptr_t dataBase = (uintptr_t)data;
if (dataBase < regionBase || dataBase - regionBase > regionBytes ||
regionBytes - (dataBase - regionBase) < sizeof(*data) ||
data->Magic != NTL_LOADER_DATA_MAGIC || data->Version != NTL_LOADER_DATA_VERSION ||
data->Size < sizeof(*data)) {
NtlpFree(context);
return NTLSTATUS_FAILURE;
}
size_t recordsBytes = (size_t)data->ImageCount * sizeof(NTL_LOADER_IMAGE_RECORD);
if (data->ImageCount && recordsBytes / sizeof(NTL_LOADER_IMAGE_RECORD) != data->ImageCount) {
NtlpFree(context);
return NTLSTATUS_FAILURE;
}
size_t recordsEnd = sizeof(*data) + recordsBytes;
if (recordsEnd < sizeof(*data) || data->Size < recordsEnd ||
data->Size > regionBytes - (dataBase - regionBase) ||
data->CurrentDirectoryOffset < recordsEnd ||
data->CurrentDirectoryLength == 0 ||
data->CurrentDirectoryOffset > data->Size ||
data->CurrentDirectoryLength > data->Size - data->CurrentDirectoryOffset ||
data->CurrentDirectoryLength == data->Size - data->CurrentDirectoryOffset ||
((const char*)data)[data->CurrentDirectoryOffset + data->CurrentDirectoryLength] != '\0' ||
(data->StartupDataLength == 0 && data->StartupDataOffset != 0) ||
(data->StartupDataLength != 0 &&
(data->StartupDataOffset < data->CurrentDirectoryOffset + data->CurrentDirectoryLength + 1u ||
data->StartupDataOffset > data->Size ||
data->StartupDataLength > data->Size - data->StartupDataOffset))) {
NtlpFree(context);
return NTLSTATUS_FAILURE;
}
for (uint32_t i = 0; i < data->ImageCount; ++i) {
const NTL_LOADER_IMAGE_RECORD* image = &data->Images[i];
size_t nameLength = 0;
while (nameLength < NTL_MODULE_NAME_MAX && image->Name[nameLength]) nameLength++;
if (!image->Base || !image->Size || nameLength == 0 || nameLength >= NTL_MODULE_NAME_MAX) {
NtlpDestroyModuleContext(context);
return NTLSTATUS_FAILURE;
}
NTL_MODULE_ENTRY* entry = 0;
status = NtlpAllocZero(sizeof(*entry), (void**)&entry);
if (!KSUCCESS(status)) { NtlpDestroyModuleContext(context); return status; }
entry->Base = image->Base;
entry->Size = image->Size;
entry->EntryPoint = image->EntryPoint;
entry->ImportPinned = 1u;
entry->Initial = 1u;
memcpy(entry->Name, image->Name, nameLength + 1u);
if (NtlpFindName(context, SysSvFromParts(entry->Name, nameLength))) {
NtlpFree(entry);
NtlpDestroyModuleContext(context);
return NTLSTATUS_OBJECT_NAME_COLLISION;
}
entry->Next = context->Head;
context->Head = entry;
context->Count++;
}
}
}
*contextOut = context;
return NTLSTATUS_SUCCESS;
}
// Releases loader metadata without unmapping images owned by the target process.
void NtlpDestroyModuleContext(NTL_MODULE_CONTEXT* context) {
if (!context || context->Magic != NTL_MODULE_CONTEXT_MAGIC) return;
NTL_MODULE_ENTRY* entry = context->Head;
while (entry) {
NTL_MODULE_ENTRY* next = entry->Next;
NtlpFree(entry);
entry = next;
}
context->Head = 0;
context->Magic = 0;
if (context->OwnsStorage) NtlpFree(context);
}
// Materializes the main PE while resolving all startup dependencies through this context.
KSTATUS NtlpLoadMainImage(NTL_MODULE_CONTEXT* context,
SYS_HANDLE fileHandle,
NTL_PE_IMAGE* imageOut) {
if (!context || context->Magic != NTL_MODULE_CONTEXT_MAGIC ||
fileHandle == SYS_HANDLE_INVALID || !imageOut)
return NTLSTATUS_INVALID_PARAMETER;
NTL_PE_LOAD_REQUEST load;
memset(&load, 0, sizeof(load));
load.Size = sizeof(load);
load.ProcessHandle = context->ProcessHandle;
load.FileHandle = fileHandle;
load.Flags = NTL_PE_LOAD_FLAG_NONE;
load.ResolveImport = NtlpResolveImport;
load.ResolveContext = context;
load.ImageOut = imageOut;
return NtlLoadPeImage(&load);
}
// Validates one caller-owned process string vector and returns its serialized string bytes.
static KSTATUS NtlpMeasureProcessVector(NTL_PROCESS_STRING_VECTOR vector, size_t* bytesOut) {
if (!bytesOut || (vector.Count && !vector.Entries)) return NTLSTATUS_INVALID_PARAMETER;
size_t bytes = 0;
for (uint32_t i = 0; i < vector.Count; ++i) {
SYS_STRING_VIEW entry = vector.Entries[i];
if (!SysSvIsValid(entry) || SysSvContainsByte(entry, '\0') || entry.Length > UINT32_MAX)
return NTLSTATUS_INVALID_PARAMETER;
if (entry.Length > SIZE_MAX - bytes - 1u) return NTLSTATUS_NO_MEMORY;
bytes += entry.Length + 1u;
}
*bytesOut = bytes;
return NTLSTATUS_SUCCESS;
}
// Serializes startup dependency metadata into a read-only child-owned VAD.
KSTATUS NtlpBuildLoaderData(NTL_MODULE_CONTEXT* context,
SYS_STRING_VIEW currentDirectory,
NTL_PROCESS_STRING_VECTOR arguments,
NTL_PROCESS_STRING_VECTOR environment,
const void* startupData,
size_t startupDataSize,
void** loaderDataOut) {
if (!context || context->Magic != NTL_MODULE_CONTEXT_MAGIC || !loaderDataOut ||
context->ProcessHandle == SYS_HANDLE_INVALID || !SysSvIsValid(currentDirectory) ||
currentDirectory.Length == 0 || currentDirectory.Length > SYS_PATH_MAX ||
currentDirectory.Data[0] != '/' || SysSvContainsByte(currentDirectory, '\\') ||
SysSvContainsByte(currentDirectory, '\0') || startupDataSize > NTL_PROCESS_STARTUP_DATA_MAX ||
(startupDataSize && !startupData))
return NTLSTATUS_INVALID_PARAMETER;
*loaderDataOut = 0;
size_t argumentStringBytes = 0;
size_t environmentStringBytes = 0;
KSTATUS status = NtlpMeasureProcessVector(arguments, &argumentStringBytes);
if (!KSUCCESS(status)) return status;
status = NtlpMeasureProcessVector(environment, &environmentStringBytes);
if (!KSUCCESS(status)) return status;
size_t recordsBytes = (size_t)context->Count * sizeof(NTL_LOADER_IMAGE_RECORD);
if (context->Count && recordsBytes / sizeof(NTL_LOADER_IMAGE_RECORD) != context->Count)
return NTLSTATUS_NO_MEMORY;
size_t bytes = sizeof(NTL_LOADER_DATA) + recordsBytes;
size_t argumentRecordsBytes = (size_t)arguments.Count * sizeof(NTL_LOADER_STRING_RECORD);
size_t environmentRecordsBytes = (size_t)environment.Count * sizeof(NTL_LOADER_STRING_RECORD);
if ((arguments.Count && argumentRecordsBytes / sizeof(NTL_LOADER_STRING_RECORD) != arguments.Count) ||
(environment.Count && environmentRecordsBytes / sizeof(NTL_LOADER_STRING_RECORD) != environment.Count) ||
argumentRecordsBytes > SIZE_MAX - bytes)
return NTLSTATUS_NO_MEMORY;
size_t argumentVectorOffset = arguments.Count ? bytes : 0u;
bytes += argumentRecordsBytes;
if (environmentRecordsBytes > SIZE_MAX - bytes) return NTLSTATUS_NO_MEMORY;
size_t environmentVectorOffset = environment.Count ? bytes : 0u;
bytes += environmentRecordsBytes;
if (bytes < sizeof(NTL_LOADER_DATA) || currentDirectory.Length > SIZE_MAX - bytes - 1u)
return NTLSTATUS_NO_MEMORY;
size_t currentDirectoryOffset = bytes;
bytes += currentDirectory.Length + 1u;
size_t startupDataOffset = 0;
if (startupDataSize) {
if (startupDataSize > SIZE_MAX - bytes) return NTLSTATUS_NO_MEMORY;
startupDataOffset = bytes;
bytes += startupDataSize;
}
if (argumentStringBytes > SIZE_MAX - bytes) return NTLSTATUS_NO_MEMORY;
size_t argumentStringsOffset = bytes;
bytes += argumentStringBytes;
if (environmentStringBytes > SIZE_MAX - bytes) return NTLSTATUS_NO_MEMORY;
size_t environmentStringsOffset = bytes;
bytes += environmentStringBytes;
if (bytes > UINT32_MAX) return NTLSTATUS_NO_MEMORY;
if (bytes > SIZE_MAX - (NTL_MODULE_PAGE_SIZE - 1u)) return NTLSTATUS_NO_MEMORY;
size_t pages = (bytes + NTL_MODULE_PAGE_SIZE - 1u) / NTL_MODULE_PAGE_SIZE;
void* base = 0;
SYS_ALLOC_PROCESS_VM_REQUEST alloc;
memset(&alloc, 0, sizeof(alloc));
alloc.Size = sizeof(alloc);
alloc.ProcessHandle = context->ProcessHandle;
alloc.Protect = SYS_VM_READWRITE;
alloc.Flags = SYS_VM_FLAG_NONE;
alloc.PageCount = pages;
alloc.BaseOut = &base;
status = SysAllocProcessVm(&alloc);
if (!KSUCCESS(status)) return status;
NTL_LOADER_DATA* data = (NTL_LOADER_DATA*)base;
memset(data, 0, pages * NTL_MODULE_PAGE_SIZE);
data->Magic = NTL_LOADER_DATA_MAGIC;
data->Version = NTL_LOADER_DATA_VERSION;
data->Size = (uint32_t)bytes;
data->ImageCount = context->Count;
data->CurrentDirectoryOffset = (uint32_t)currentDirectoryOffset;
data->CurrentDirectoryLength = (uint32_t)currentDirectory.Length;
data->StartupDataOffset = startupDataSize ? (uint32_t)startupDataOffset : 0u;
data->StartupDataLength = (uint32_t)startupDataSize;
data->ArgumentVectorOffset = arguments.Count ? (uint32_t)argumentVectorOffset : 0u;
data->ArgumentCount = arguments.Count;
data->EnvironmentVectorOffset = environment.Count ? (uint32_t)environmentVectorOffset : 0u;
data->EnvironmentCount = environment.Count;
uint32_t i = 0;
for (NTL_MODULE_ENTRY* entry = context->Head; entry; entry = entry->Next) {
NTL_LOADER_IMAGE_RECORD* image = &data->Images[i++];
image->Base = entry->Base;
image->Size = entry->Size;
image->EntryPoint = entry->EntryPoint;
strncpy(image->Name, entry->Name, sizeof(image->Name) - 1u);
}
char* cwd = (char*)data + currentDirectoryOffset;
memcpy(cwd, currentDirectory.Data, currentDirectory.Length);
cwd[currentDirectory.Length] = '\0';
if (startupDataSize)
memcpy((char*)data + startupDataOffset, startupData, startupDataSize);
NTL_LOADER_STRING_RECORD* argumentRecords = arguments.Count
? (NTL_LOADER_STRING_RECORD*)((char*)data + argumentVectorOffset) : 0;
size_t stringOffset = argumentStringsOffset;
for (uint32_t argumentIndex = 0; argumentIndex < arguments.Count; ++argumentIndex) {
SYS_STRING_VIEW entry = arguments.Entries[argumentIndex];
argumentRecords[argumentIndex].Offset = (uint32_t)stringOffset;
argumentRecords[argumentIndex].Length = (uint32_t)entry.Length;
if (entry.Length) memcpy((char*)data + stringOffset, entry.Data, entry.Length);
((char*)data)[stringOffset + entry.Length] = '\0';
stringOffset += entry.Length + 1u;
}
NTL_LOADER_STRING_RECORD* environmentRecords = environment.Count
? (NTL_LOADER_STRING_RECORD*)((char*)data + environmentVectorOffset) : 0;
stringOffset = environmentStringsOffset;
for (uint32_t environmentIndex = 0; environmentIndex < environment.Count; ++environmentIndex) {
SYS_STRING_VIEW entry = environment.Entries[environmentIndex];
environmentRecords[environmentIndex].Offset = (uint32_t)stringOffset;
environmentRecords[environmentIndex].Length = (uint32_t)entry.Length;
if (entry.Length) memcpy((char*)data + stringOffset, entry.Data, entry.Length);
((char*)data)[stringOffset + entry.Length] = '\0';
stringOffset += entry.Length + 1u;
}
status = SysProtectProcessVm(context->ProcessHandle, base, pages, SYS_VM_READ);
if (!KSUCCESS(status)) return status;
*loaderDataOut = base;
return NTLSTATUS_SUCCESS;
}
// Validates and returns the immutable loader block for the current process.
static KSTATUS NtlpGetCurrentLoaderData(const NTL_LOADER_DATA** dataOut) {
if (!dataOut) return NTLSTATUS_INVALID_PARAMETER;
*dataOut = 0;
SYS_PEB* peb = SysGetPeb();
if (!peb || peb->Magic != SYS_PEB_MAGIC || peb->Version != SYS_ENV_VERSION || !peb->LoaderData)
return NTLSTATUS_NOT_FOUND;
SYS_VM_REGION_INFO region;
memset(&region, 0, sizeof(region));
KSTATUS status = SysQueryVm((void*)peb->LoaderData, &region);
if (!KSUCCESS(status) || !region.Base || !region.PageCount ||
region.PageCount > SIZE_MAX / NTL_MODULE_PAGE_SIZE)
return NTLSTATUS_FAILURE;
size_t regionBytes = region.PageCount * NTL_MODULE_PAGE_SIZE;
uintptr_t regionBase = (uintptr_t)region.Base;
uintptr_t dataBase = (uintptr_t)peb->LoaderData;
if (dataBase < regionBase || dataBase - regionBase > regionBytes ||
regionBytes - (dataBase - regionBase) < sizeof(NTL_LOADER_DATA))
return NTLSTATUS_FAILURE;
const NTL_LOADER_DATA* data = (const NTL_LOADER_DATA*)peb->LoaderData;
if (data->Magic != NTL_LOADER_DATA_MAGIC || data->Version != NTL_LOADER_DATA_VERSION ||
data->Size < sizeof(*data) || data->Size > regionBytes - (dataBase - regionBase))
return NTLSTATUS_FAILURE;
*dataOut = data;
return NTLSTATUS_SUCCESS;
}
// Copies the opaque userspace bootstrap payload from the current process loader block.
KSTATUS NtlGetProcessStartupData(void* buffer, size_t bufferSize, size_t* sizeOut) {
if (!sizeOut || (bufferSize && !buffer)) return NTLSTATUS_INVALID_PARAMETER;
*sizeOut = 0;
const NTL_LOADER_DATA* data = 0;
KSTATUS status = NtlpGetCurrentLoaderData(&data);
if (!KSUCCESS(status)) return status;
if (data->StartupDataLength > NTL_PROCESS_STARTUP_DATA_MAX ||
(data->StartupDataLength == 0 && data->StartupDataOffset != 0) ||
(data->StartupDataLength != 0 &&
(data->StartupDataOffset < sizeof(*data) || data->StartupDataOffset > data->Size ||
data->StartupDataLength > data->Size - data->StartupDataOffset)))
return NTLSTATUS_FAILURE;
if (data->StartupDataLength == 0)
return NTLSTATUS_NOT_FOUND;
*sizeOut = data->StartupDataLength;
if (bufferSize < data->StartupDataLength)
return NTLSTATUS_BUFFER_TOO_SMALL;
memcpy(buffer, (const char*)data + data->StartupDataOffset, data->StartupDataLength);
return NTLSTATUS_SUCCESS;
}
// Returns one loader string vector count after validating its descriptor table.
static KSTATUS NtlpGetProcessVectorCount(int environment, uint32_t* countOut) {
if (!countOut) return NTLSTATUS_INVALID_PARAMETER;
*countOut = 0;
const NTL_LOADER_DATA* data = 0;
KSTATUS status = NtlpGetCurrentLoaderData(&data);
if (!KSUCCESS(status)) return status;
uint32_t count = environment ? data->EnvironmentCount : data->ArgumentCount;
uint32_t offset = environment ? data->EnvironmentVectorOffset : data->ArgumentVectorOffset;
if ((count == 0 && offset != 0) || (count != 0 && offset == 0)) return NTLSTATUS_FAILURE;
size_t recordsBytes = (size_t)count * sizeof(NTL_LOADER_STRING_RECORD);
if (count && recordsBytes / sizeof(NTL_LOADER_STRING_RECORD) != count)
return NTLSTATUS_FAILURE;
if (offset > data->Size || recordsBytes > data->Size - offset)
return NTLSTATUS_FAILURE;
*countOut = count;
return NTLSTATUS_SUCCESS;
}
// Returns one immutable loader string vector entry.
static KSTATUS NtlpGetProcessVectorEntry(int environment, uint32_t index, SYS_STRING_VIEW* entryOut) {
if (!entryOut) return NTLSTATUS_INVALID_PARAMETER;
*entryOut = SYS_SV_NULL;
const NTL_LOADER_DATA* data = 0;
KSTATUS status = NtlpGetCurrentLoaderData(&data);
if (!KSUCCESS(status)) return status;
uint32_t count = environment ? data->EnvironmentCount : data->ArgumentCount;
uint32_t offset = environment ? data->EnvironmentVectorOffset : data->ArgumentVectorOffset;
if (index >= count || !count || !offset) return NTLSTATUS_NOT_FOUND;
size_t recordsBytes = (size_t)count * sizeof(NTL_LOADER_STRING_RECORD);
if (recordsBytes / sizeof(NTL_LOADER_STRING_RECORD) != count ||
offset > data->Size || recordsBytes > data->Size - offset)
return NTLSTATUS_FAILURE;
const NTL_LOADER_STRING_RECORD* records =
(const NTL_LOADER_STRING_RECORD*)((const char*)data + offset);
NTL_LOADER_STRING_RECORD record = records[index];
if (record.Offset >= data->Size || record.Length > data->Size - record.Offset ||
record.Length == data->Size - record.Offset)
return NTLSTATUS_FAILURE;
const char* string = (const char*)data + record.Offset;
SYS_STRING_VIEW view = SysSvFromParts(string, record.Length);
if (string[record.Length] != '\0' || SysSvContainsByte(view, '\0'))
return NTLSTATUS_FAILURE;
*entryOut = view;
return NTLSTATUS_SUCCESS;
}
// Returns the number of serialized process arguments.
KSTATUS NtlGetProcessArgumentCount(uint32_t* countOut) {
return NtlpGetProcessVectorCount(0, countOut);
}
// Returns one serialized process argument.
KSTATUS NtlGetProcessArgument(uint32_t index, SYS_STRING_VIEW* argumentOut) {
return NtlpGetProcessVectorEntry(0, index, argumentOut);
}
// Returns the number of serialized environment entries.
KSTATUS NtlGetProcessEnvironmentCount(uint32_t* countOut) {
return NtlpGetProcessVectorCount(1, countOut);
}
// Returns one serialized environment entry.
KSTATUS NtlGetProcessEnvironment(uint32_t index, SYS_STRING_VIEW* entryOut) {
return NtlpGetProcessVectorEntry(1, index, entryOut);
}
// Returns the canonical inherited resource CWD stored in this process's loader data.
KSTATUS NtlpGetLoaderCurrentDirectory(SYS_STRING_VIEW* currentDirectoryOut) {
if (!currentDirectoryOut) return NTLSTATUS_INVALID_PARAMETER;
*currentDirectoryOut = SYS_SV_NULL;
const NTL_LOADER_DATA* data = 0;
KSTATUS status = NtlpGetCurrentLoaderData(&data);
if (!KSUCCESS(status)) return status;
if (data->CurrentDirectoryOffset < sizeof(*data) ||
data->CurrentDirectoryLength == 0 ||
data->CurrentDirectoryOffset > data->Size ||
data->CurrentDirectoryLength > data->Size - data->CurrentDirectoryOffset ||
data->CurrentDirectoryLength == data->Size - data->CurrentDirectoryOffset)
return NTLSTATUS_FAILURE;
const char* cwd = (const char*)data + data->CurrentDirectoryOffset;
SYS_STRING_VIEW view = SysSvFromParts(cwd, data->CurrentDirectoryLength);
if (cwd[data->CurrentDirectoryLength] != '\0' || cwd[0] != '/' ||
SysSvContainsByte(view, '\\') || SysSvContainsByte(view, '\0'))
return NTLSTATUS_FAILURE;
*currentDirectoryOut = view;
return NTLSTATUS_SUCCESS;
}
// Lazily seeds the current-process registry and refreshes per-call resolution authority.
static KSTATUS NtlpEnsureCurrentContext(SYS_HANDLE moduleRoot,
SYS_STRING_VIEW moduleDirectory,
NTL_MODULE_NAME_RESOLVER resolver,
void* resolverContext,
NTL_MODULE_CONTEXT** contextOut) {
if (!contextOut) return NTLSTATUS_INVALID_PARAMETER;
if (NtlAtomicCompareExchange32(&gCurrentContextState, 1, 0) == 0) {
NTL_MODULE_CONTEXT* temporary = 0;
KSTATUS status = NtlpCreateModuleContext(SYS_HANDLE_INVALID, moduleRoot, moduleDirectory,
SYS_HANDLE_INVALID, SysSvFromParts((const char*)0, 0),
SYS_HANDLE_INVALID, SysSvFromParts((const char*)0, 0),
resolver, resolverContext, 1, &temporary);
if (!KSUCCESS(status)) {
NtlAtomicExchange32(&gCurrentContextState, 0);
return status;
}
gCurrentContext = *temporary;
gCurrentContext.OwnsStorage = 0u;
temporary->Head = 0;
temporary->Magic = 0;
NtlpFree(temporary);
NtlAtomicExchange32(&gCurrentContextState, 2);
} else {
while (NtlAtomicCompareExchange32(&gCurrentContextState, 2, 2) == 1) SysYield();
if (NtlAtomicCompareExchange32(&gCurrentContextState, 2, 2) != 2)
return NTLSTATUS_FAILURE;
gCurrentContext.PrimaryModuleRootHandle = moduleRoot;
gCurrentContext.PrimaryModuleDirectory = moduleDirectory;
gCurrentContext.SecondaryModuleRootHandle = SYS_HANDLE_INVALID;
gCurrentContext.SecondaryModuleDirectory = SysSvFromParts((const char*)0, 0);
gCurrentContext.FallbackModuleRootHandle = SYS_HANDLE_INVALID;
gCurrentContext.FallbackModuleDirectory = SysSvFromParts((const char*)0, 0);
gCurrentContext.Resolver = resolver;
gCurrentContext.ResolverContext = resolverContext;
}
*contextOut = &gCurrentContext;
return NTLSTATUS_SUCCESS;
}
// Serializes current-process module mutations because loader metadata is process-global.
static KSTATUS NtlpEnterCurrent(void) {
while (NtlAtomicCompareExchange32(&gCurrentContextBusy, 1, 0) != 0) SysYield();
return NTLSTATUS_SUCCESS;
}
// Releases the current-process loader serialization guard.
static void NtlpLeaveCurrent(void) {
NtlAtomicExchange32(&gCurrentContextBusy, 0);
}
// Loads or references one DLL-like PE in the current process through capability-relative I/O.
KSTATUS NtlLoadModuleAt(const NTL_LOAD_MODULE_AT_REQUEST* request) {
if (!request || request->Size != sizeof(*request) || request->Flags != NTL_MODULE_LOAD_FLAG_NONE ||
request->RootHandle == SYS_HANDLE_INVALID || request->ModuleRootHandle == SYS_HANDLE_INVALID ||
!SysSvIsValid(request->Path) || request->Path.Length == 0 || !request->ModuleHandleOut)
return NTLSTATUS_INVALID_PARAMETER;
*request->ModuleHandleOut = NTL_MODULE_HANDLE_INVALID;
KSTATUS status = NtlpEnterCurrent();
if (!KSUCCESS(status)) return status;
NTL_MODULE_CONTEXT* context = 0;
status = NtlpEnsureCurrentContext(request->ModuleRootHandle, request->ModuleDirectory,
request->ResolveModuleName, request->ResolveContext, &context);
if (!KSUCCESS(status)) { NtlpLeaveCurrent(); return status; }
SYS_STRING_VIEW baseName = NtlpBasename(request->Path);
SYS_STRING_VIEW canonical = baseName;
status = NtlpCanonicalName(context, baseName, &canonical);
if (!KSUCCESS(status)) { NtlpLeaveCurrent(); return status; }
NTL_MODULE_ENTRY* existing = NtlpFindName(context, canonical);
if (existing) {
existing->PublicRefCount++;
*request->ModuleHandleOut = (NTL_MODULE_HANDLE)(uintptr_t)existing;
if (request->InfoOut) NtlpFillInfo(existing, request->InfoOut);
NtlpLeaveCurrent();
return NTLSTATUS_SUCCESS;
}
char canonicalPath[SYS_PATH_MAX];
SYS_STRING_VIEW openPath;
status = NtlpBuildCanonicalPath(request->Path, canonical, canonicalPath, &openPath);
if (!KSUCCESS(status)) { NtlpLeaveCurrent(); return status; }
SYS_HANDLE file = SYS_HANDLE_INVALID;
SYS_OPEN_FILE_AT_REQUEST open = SysMakeOpenFileAtRequest(request->RootHandle, openPath,
SYS_FILE_ACCESS_READ | SYS_FILE_ACCESS_QUERY,
SYS_FILE_OPEN_EXISTING, SYS_FILE_FLAG_NONE, &file);
status = SysOpenFileAt(&open);
if (!KSUCCESS(status)) { NtlpLeaveCurrent(); return status; }
NTL_MODULE_ENTRY* entry = 0;
status = NtlpAllocZero(sizeof(*entry), (void**)&entry);
if (KSUCCESS(status)) status = NtlpCopyName(canonical, entry->Name);
if (!KSUCCESS(status)) {
(void)SysCloseHandle(file);
NtlpFree(entry);
NtlpLeaveCurrent();
return status;
}
entry->PublicRefCount = 1u;
entry->Next = context->Head;
context->Head = entry;
context->Count++;
NTL_PE_IMAGE image;
memset(&image, 0, sizeof(image));
NTL_PE_LOAD_REQUEST load;
memset(&load, 0, sizeof(load));
load.Size = sizeof(load);
load.ProcessHandle = SYS_HANDLE_INVALID;
load.FileHandle = file;
load.Flags = NTL_PE_LOAD_FLAG_NONE;
load.ResolveImport = NtlpResolveImport;
load.ResolveContext = context;
load.Prepared = NtlpPreparedModule;
load.PreparedContext = entry;
load.ImageOut = &image;
status = NtlLoadPeImage(&load);
(void)SysCloseHandle(file);
if (!KSUCCESS(status)) {
context->Head = entry->Next;
context->Count--;
NtlpFree(entry);
NtlpLeaveCurrent();
return status;
}
*request->ModuleHandleOut = (NTL_MODULE_HANDLE)(uintptr_t)entry;
if (request->InfoOut) NtlpFillInfo(entry, request->InfoOut);
NtlpLeaveCurrent();
return NTLSTATUS_SUCCESS;
}
// Releases one public module reference and unmaps modules that are no longer pinned.
KSTATUS NtlUnloadModule(NTL_MODULE_HANDLE module) {
KSTATUS status = NtlpEnterCurrent();
if (!KSUCCESS(status)) return status;
if (NtlAtomicCompareExchange32(&gCurrentContextState, 2, 2) != 2) {
NtlpLeaveCurrent();
return NTLSTATUS_INVALID_HANDLE;
}
NTL_MODULE_ENTRY* entry = NtlpFindHandle(&gCurrentContext, module);
if (!entry || entry->PublicRefCount == 0) { NtlpLeaveCurrent(); return NTLSTATUS_INVALID_HANDLE; }
entry->PublicRefCount--;
if (entry->PublicRefCount == 0 && !entry->ImportPinned && !entry->Initial) {
NTL_MODULE_ENTRY** link = &gCurrentContext.Head;
while (*link && *link != entry) link = &(*link)->Next;
if (*link == entry) *link = entry->Next;
gCurrentContext.Count--;
if (entry->Base) (void)SysFreeVm((void*)entry->Base);
NtlpFree(entry);
}
NtlpLeaveCurrent();
return NTLSTATUS_SUCCESS;
}
// Queries one live userspace module handle.
KSTATUS NtlGetModuleInfo(NTL_MODULE_HANDLE module, NTL_MODULE_INFO* infoOut) {
if (!infoOut) return NTLSTATUS_INVALID_PARAMETER;
KSTATUS status = NtlpEnterCurrent();
if (!KSUCCESS(status)) return status;
if (NtlAtomicCompareExchange32(&gCurrentContextState, 2, 2) != 2) {
NtlpLeaveCurrent();
return NTLSTATUS_INVALID_HANDLE;
}
NTL_MODULE_ENTRY* entry = NtlpFindHandle(&gCurrentContext, module);
if (!entry) { NtlpLeaveCurrent(); return NTLSTATUS_INVALID_HANDLE; }
NtlpFillInfo(entry, infoOut);
NtlpLeaveCurrent();
return NTLSTATUS_SUCCESS;
}
// Resolves one named export from a live userspace module handle.
KSTATUS NtlGetModuleExport(NTL_MODULE_HANDLE module, SYS_STRING_VIEW name, uintptr_t* addressOut) {
if (!addressOut) return NTLSTATUS_INVALID_PARAMETER;
KSTATUS status = NtlpEnterCurrent();
if (!KSUCCESS(status)) return status;
if (NtlAtomicCompareExchange32(&gCurrentContextState, 2, 2) != 2) {
NtlpLeaveCurrent();
return NTLSTATUS_INVALID_HANDLE;
}
NTL_MODULE_ENTRY* entry = NtlpFindHandle(&gCurrentContext, module);
if (!entry) { NtlpLeaveCurrent(); return NTLSTATUS_INVALID_HANDLE; }
status = NtlGetPeExport(entry->Base, name, addressOut);
NtlpLeaveCurrent();
return status;
}
// Snapshots the current process-local userspace module registry.
KSTATUS NtlGetModuleList(NTL_MODULE_INFO* modulesOut, size_t maxCount, size_t* countOut) {
if (!countOut) return NTLSTATUS_INVALID_PARAMETER;
KSTATUS status = NtlpEnterCurrent();
if (!KSUCCESS(status)) return status;
if (NtlAtomicCompareExchange32(&gCurrentContextState, 2, 2) != 2) {
*countOut = 0;
NtlpLeaveCurrent();
return NTLSTATUS_SUCCESS;
}
*countOut = gCurrentContext.Count;
if (gCurrentContext.Count > maxCount) { NtlpLeaveCurrent(); return NTLSTATUS_BUFFER_TOO_SMALL; }
if (gCurrentContext.Count && !modulesOut) { NtlpLeaveCurrent(); return NTLSTATUS_INVALID_PARAMETER; }
size_t i = 0;
for (NTL_MODULE_ENTRY* entry = gCurrentContext.Head; entry; entry = entry->Next)
NtlpFillInfo(entry, &modulesOut[i++]);
NtlpLeaveCurrent();
return NTLSTATUS_SUCCESS;
}