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

  1. Verify the host is a supported Windows version (XP or later); NtQueueApcThread has existed since XP, so a missing export indicates an unsupported environment.
  2. Fall back to a different injection method (e.g. CreateRemoteThread) when the APC path is unavailable.
  3. Check that no DLL-injection/anti-cheat software is unhooking or hiding ntdll exports.
  4. 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

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


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)