ramensoftware/windhawk · error · std::runtime_error
NtQueueApcThread not found
Error message
NtQueueApcThread not found
What it means
MyQueueUserAPC queues a cross-process APC to inject a DLL, but it needs the undocumented ntdll export NtQueueApcThread. It resolves the function once via GET_PROC_ADDRESS_ONCE; if ntdll does not export it, the code throws runtime_error instead of proceeding. Without this API the APC-based injection path cannot work.
Solutions
- Verify the host is a supported Windows version (XP or later); NtQueueApcThread has existed since XP, so a missing export indicates an unsupported environment.
- Fall back to a different injection method (e.g. CreateRemoteThread) when the APC path is unavailable.
- Check that no DLL-injection/anti-cheat software is unhooking or hiding ntdll exports.
- Report the Windows version and environment to the vendor if it occurs on a normal modern Windows install.
Example fix
// before
GET_PROC_ADDRESS_ONCE(NtQueueApcThread_t, pNtQueueApcThread, L"ntdll.dll", "NtQueueApcThread");
if (!pNtQueueApcThread) {
throw std::runtime_error("NtQueueApcThread not found");
}
// after
GET_PROC_ADDRESS_ONCE(NtQueueApcThread_t, pNtQueueApcThread, L"ntdll.dll", "NtQueueApcThread");
if (!pNtQueueApcThread) {
// fall back to a supported injection path instead of failing
return InjectViaCreateRemoteThread(...);
} Defensive patterns
Strategy: try-catch
Validate before calling
auto hNtdll = GetModuleHandleW(L"ntdll.dll");
bool canUseApcInjection =
hNtdll && GetProcAddress(hNtdll, "NtQueueApcThread") != nullptr; Type guard
bool HasNtQueueApcThread() {
auto h = GetModuleHandleW(L"ntdll.dll");
return h && GetProcAddress(h, "NtQueueApcThread") != nullptr;
} Try / catch
try {
DllInject(..., /*method=*/InjectionMethod::Apc, ...);
} catch (const std::runtime_error& e) {
if (std::string_view(e.what()) == "NtQueueApcThread not found") {
DllInject(..., /*method=*/InjectionMethod::CreateRemoteThread, ...);
}
} Prevention
- Check NtQueueApcThread availability before choosing the APC injection method.
- Restrict injection to supported Windows versions (XP+).
- Keep a fallback injection method implemented in your code path.
- Avoid environments where security software strips ntdll exports.
When it happens
Trigger: DllInject selecting the APC injection method on a system where GetProcAddress(GetModuleHandle(L"ntdll.dll"), "NtQueueApcThread") returns NULL — essentially only on very old (pre-Windows XP) or heavily locked-down/modified systems where the export is missing.
Common situations: Running the injector on an unsupported or stripped-down Windows variant; security/hardening products that filter ntdll exports; mismatched injection method chosen for the target OS.
Understand the failure class
Background: "not installed", "pip install", "required for": how missing-dependency errors surface across open-source libraries — this error's family across 34 libraries.
Related errors
- throw ResultException(failure)
- throw PortableSettingsException(error)
- value name contains a NUL
- GetPrivateProfileString
- value name
AI-assisted analysis of ramensoftware/windhawk@61d99ed8e1 (2026-09-12).
Data as JSON: /api/errors/53c9d58a71c276ba.
Report an issue: GitHub.
Appendix: source
Thrown at src/windhawk/engine/dll_inject.cpp:619
// If you try to queue an APC from a 32 bit process to a 64 bit
// process and you use a 32 bit address, you'll get this status code:
// [...] STATUS_INVALID_HANDLE"
// https://repnz.github.io/posts/apc/wow64-user-apc/
NtQueueApcThread64(hThread, pfnAPC, data, nullptr, nullptr);
return;
}
#endif // _WIN64
using NtQueueApcThread_t = NTSTATUS(NTAPI*)(
_In_ HANDLE ThreadHandle, _In_ PPS_APC_ROUTINE ApcRoutine,
_In_opt_ PVOID ApcArgument1, _In_opt_ PVOID ApcArgument2,
_In_opt_ PVOID ApcArgument3);
GET_PROC_ADDRESS_ONCE(NtQueueApcThread_t, pNtQueueApcThread, L"ntdll.dll",
"NtQueueApcThread");
if (!pNtQueueApcThread) {
throw std::runtime_error("NtQueueApcThread not found");
}
#ifdef _WIN64
if (targetProcessArch == IMAGE_FILE_MACHINE_I386) {
// x64 native to WOW64, encode address.
pfnAPC = (PPS_APC_ROUTINE)EncodeWow64ApcRoutine((ULONG64)pfnAPC);
}
#endif // _WIN64
THROW_IF_NTSTATUS_FAILED(
pNtQueueApcThread(hThread, pfnAPC, data, nullptr, nullptr));
}
USHORT GetProcessArch(HANDLE hProcess) {
using GetProcessInformation_t = BOOL(WINAPI*)(
HANDLE hProcess, PROCESS_INFORMATION_CLASS ProcessInformationClass,
LPVOID ProcessInformation, DWORD ProcessInformationSize);
View on GitHub (pinned to 61d99ed8e1)