
This article contains functions and features that are not documented by the original manufacturer. By following advice in this article, you're doing so at your own risk. The methods presented in this article may rely on internal implementation and may not work in the future.
Intro
While working on my blog post about the internals of a syscall in Windows I stumbled upon an interesting way how Microsoft were placing a thread into a wait state and also how they were waking it up from that state for their internal implementation of a critical section, or for their slim reader/writer locks.
I am somewhat perplexed as to why Microsoft did not document it, as it seems like a very fast and efficient method versus the documented way.
With the help from Rbmm we were able to reverse engineer it. I'll share our findings with my readers below.
NtWaitForAlertByThreadId
This function is exported from the native ntdll.dll. It has been available since Windows 8.1 and it is declared as such:
NTSTATUS NTAPI NtWaitForAlertByThreadId( // Has the alias of ZwWaitForAlertByThreadId in user-mode
_In_ HANDLE reserved, // Should be 0
_In_opt_ PLARGE_INTEGER Timeout); // Pointer to a timeoutUnlike its name, NtWaitForAlertByThreadId seems to ignore its first parameter and waits only for the thread that calls it. Thus I would pass NULL into it.
Upon further research Rbmm suggested that the first parameter is actually passed unchanged into the kernel (without any seeming checks) and is stored there in theKTHREAD::KWAIT_BLOCK.Object. That location is used to store the address of wait object(s) and is later accessed during wait operations on kernel dispatcher objects, for instance, explicitly when you call theKeWaitForMultipleObjectsfunction, or implicitly during theKeWaitForSingleObjectcall, among others.Further more, if one invokes the
AcquireSRWLockExclusiveuser-mode API:C++[Copy]SRWLOCK lock {}; AcquireSRWLockExclusive(&lock); AcquireSRWLockExclusive(&lock); //Needed to put thread into the kernel wait stateIt may pass the user-mode address of the
SRWLOCKstruct to the kernel via theNtWaitForAlertByThreadIdcall (if it needs to enter the wait state in the kernel) that will keep that user-mode address in theKTHREAD::KWAIT_BLOCK.Objectfor the thread.
The second parameter, Timeout could be a pointer to the LARGE_INTEGER struct that specifies the time-out for the wait. Its meaning is similar to the FILETIME structure in Win32: a negative value represents a time relative to now, measured in 100-nanosecond intervals. A positive number specifies an absolute date & time.
For instance, to set a time-out of 3 seconds, you'd set it to:Note that I'm using a negative number with the
-10000LLconstant. Also note theLLsuffix to specify a 64-bit integer.
If you set the Timeout parameter to point to a 0 value, this will mean to return immediately. Or, if you set it to a NULL, this will mean to wait for as long as it takes:
NtWaitForAlertByThreadId(NULL, NULL); // Put the current thread into a waiting state with an infinite timeoutReturn Value
The NtWaitForAlertByThreadId function returns an NTSTATUS with the result, which may be:
STATUS_ALERTED(or0x101) if the alert was set. Or, success, in other words.STATUS_TIMEOUT(or0x102) if the wait timed out and the current thread was not alerted.
NtAlertThreadByThreadId
Then there's also a sister function to NtWaitForAlertByThreadId that is declared as such:
NTSTATUS NTAPI NtAlertThreadByThreadId( // Has the alias of ZwAlertThreadByThreadId in user-mode
_In_ HANDLE ThreadId // Thread ID
);It takes a thread ID to alert. This function can be used to alert, or to "wake up" another thread that is currently in the kernel wait state, initiated by a call to the NtWaitForAlertByThreadId function.
You can use theNtAlertThreadByThreadIdfunction to alert a thread from the same process only. Otherwise it will returnSTATUS_ACCESS_DENIEDerror.
Return Value
It returns an NTSTATUS with the result, most common one being a 0, or STATUS_SUCCESS.
What's The Big Deal?
So what's the thing about these functions?
What makes them handy is that they don't require a handle to put a thread into a waiting state. For instance, if you call the WaitForSingleObject function it will require a kernel object handle to wait on. In contrast, neither NtWaitForAlertByThreadId nor NtAlertThreadByThreadId require any handles. All they need is a thread ID.
There's one limitation though, both functions work only with threads in the same process where they are invoked from.
Keep in mind that both functions will make a trip to the kernel to initiate the wait, or to wake a thread up.
NtAlertResumeThread, a Bastard Child
If you search the ntdll.dll module for other exported functions that deal with alerts, you'll notice the NtAlertResumeThread function. Rbmm challenged me, so I quickly reverse engineered it.
It is declared as such:
NTSTATUS NTAPI NtAlertResumeThread( // Has the alias of ZwAlertResumeThread in user-mode
_In_ HANDLE hThread,
_Inout_opt_ DWORD* p_dwPrevSuspendCount
);It makes a trip to the kernel and, as its name suggests, it calls two kernel APIs in this order:
The first call alerts a thread and the second one resumes it (if it was suspended.)
Note that Microsoft does not give the world any justice by overusing the term "alert". The alert in this case has nothing to do with the alert that theNtWaitForAlertByThreadIdfunction makes a thread wait for, or thatNtAlertThreadByThreadIdsends to a thread.The alert that
NtAlertResumeThreaduses happens when a thread was in a wait state, say after entering it with a call to theZwWaitForSingleObjectfunction. In that case alerting such a thread with a call toKeAlertThreadwill result in it exiting the wait state with theSTATUS_ALERTEDreturn status.
Unlike the previous examples that I gave above, the first parameter to NtAlertResumeThread must be a thread handle.
The second optional parameter p_dwPrevSuspendCount may be set to an address of a DWORD that will receive thread's previous suspend count, or what would ResumeThread API may return.
So in a sense this API does two things in one call.
Note that we could not find any practical use for the NtAlertResumeThread function. It is not imported into any system DLL either.
Conclusion
I'm sure I don't need to tell you that the undocumented functions that I showed above can change at any time in the future. So if you decide to use them, please do so at your own risk.
This blog post is intended just as an informational resource to demonstrate an interesting mechanism that Microsoft are using internally for their synchronization locks.

