
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 timeout
Unlike 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 theKeWaitForMultipleObjects
function, or implicitly during theKeWaitForSingleObject
call, among others.Further more, if one invokes the
AcquireSRWLockExclusive
user-mode API:C++[Copy]SRWLOCK lock {}; AcquireSRWLockExclusive(&lock); AcquireSRWLockExclusive(&lock); //Needed to put thread into the kernel wait state
It may pass the user-mode address of the
SRWLOCK
struct to the kernel via theNtWaitForAlertByThreadId
call (if it needs to enter the wait state in the kernel) that will keep that user-mode address in theKTHREAD::KWAIT_BLOCK.Object
for 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
-10000LL
constant. Also note theLL
suffix 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 timeout
Return 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 theNtAlertThreadByThreadId
function to alert a thread from the same process only. Otherwise it will returnSTATUS_ACCESS_DENIED
error.
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 theNtWaitForAlertByThreadId
function makes a thread wait for, or thatNtAlertThreadByThreadId
sends to a thread.The alert that
NtAlertResumeThread
uses happens when a thread was in a wait state, say after entering it with a call to theZwWaitForSingleObject
function. In that case alerting such a thread with a call toKeAlertThread
will result in it exiting the wait state with theSTATUS_ALERTED
return 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.