Why Windows Timers May Not Fire
Windows Research Kernel @ HPIThis article is motivated by a discussion with Max, one of our readers, who commented on my previous post on an anomaly in Windows timer management (read the article). So, thanks again Max!
Please have a look on the following program sample.
(For the sake of simplicity I removed all the error handling code ;))
#define _WIN32_WINNT 0x0400 #include <windows.h> #include <stdio.h> #define SECONDS (10*1000*1000) int main(int argc, char * argv[]) { HANDLE Timer; LARGE_INTEGER DueTime; FILETIME CurrentTime; long time = 15; if (argc > 1) time = atol(argv[1]); // Create a timer Timer = CreateWaitableTimer(NULL, FALSE, NULL); // Set the timer to an absolute value 15 seconds in the future GetSystemTimeAsFileTime((LPFILETIME) &DueTime); DueTime.QuadPart += (LONGLONG) (time * SECONDS); SetWaitableTimer(Timer, &DueTime, 0, NULL, NULL, 0) // Wait for the timer WaitForSingleObject(Timer, INFINITE); printf("Timer was signaled.\n"); return 0; }
If you compile this code and execute in on your machine, the expected behavior is that the application will wait for about 15 seconds and will then return. If you modify the system time while your application is waiting in such a way that you advance the system time by a time span that is greater than the time span your machine is running, you will experience that your application will wait forever. How can this happen?
To answer the question, I reviewed the source files, in particular base\ntos\ke\miscc.c. I provide the interesting lines here:
00359 // Acquire the timer table lock, remove all absolute timers from the 00360 // timer queue so their due time can be recomputed, and release the 00361 // timer table lock. 00362 // 00363 00364 InitializeListHead(&AbsoluteListHead); 00365 for (Index = 0; Index < TIMER_TABLE_SIZE; Index += 1) { 00366 ListHead = &KiTimerTableListHead[Index].Entry; 00367 LockQueue = KiAcquireTimerTableLock(Index); 00368 NextEntry = ListHead->Flink; 00369 while (NextEntry != ListHead) { 00370 Timer = CONTAINING_RECORD(NextEntry, KTIMER, TimerListEntry); 00371 NextEntry = NextEntry->Flink; 00372 if (Timer->Header.Absolute != FALSE) { 00373 KiRemoveEntryTimer(Timer); 00374 InsertTailList(&AbsoluteListHead, &Timer->TimerListEntry); 00375 } 00376 } 00377 00378 KiReleaseTimerTableLock(LockQueue); 00379 }
There, the function KeSetSystemTime
is responsible
for setting the system time. When the system time is modified,
Windows has to scan through the list of timers and must determine
all absolute timers (lines 365 to 379). This is because the due time
is expressed in interrupt time, not in system time. The interrupt
time counts the time in units of 100 ns since the system was booted.
This time is not adjusted when the system time is modified. If you
set an absolute timer, e.g., with SetWaitableTimer()
,
Windows calculates the corresponding interrupt time, sets it as due
time, and inserts the timer into the timer table. If the system time
is modified, this calculation must be repeated as the delta between
interrupt time and system time has changed. Here is the code for the
recalculation:
00381 // 00382 // Recompute the due time and reinsert all absolute timers in the timer 00383 // tree. If a timer has already expired, then insert the timer in the 00384 // expired timer list. 00385 // 00386 00387 InitializeListHead(&ExpiredListHead); 00388 while (AbsoluteListHead.Flink != &AbsoluteListHead) { 00389 Timer = CONTAINING_RECORD(AbsoluteListHead.Flink, KTIMER, TimerListEntry); 00390 RemoveEntryList(&Timer->TimerListEntry); 00391 Timer->DueTime.QuadPart -= TimeDelta.QuadPart; 00392 Hand = KiComputeTimerTableIndex(Timer->DueTime.QuadPart); 00393 Timer->Header.Hand = (UCHAR)Hand; 00394 LockQueue = KiAcquireTimerTableLock(Hand); 00395 if (KiInsertTimerTable(Timer, Hand) == TRUE) { 00396 KiRemoveEntryTimer(Timer); 00397 InsertTailList(&ExpiredListHead, &Timer->TimerListEntry); 00398 } 00399 00400 KiReleaseTimerTableLock(LockQueue); 00401 }
The important line here is line 391. Note that
Timer->DueTime
is unsigned while
TimeDelta
is signed. If TimeDelta
is
greater than DueTime
, DueTime
will
wrap-around, i.e., the due time becomes practically
infinite, because all timer related code will interpret the
due time as an unsigned value! This wrap-around prevents the timer
from being fired.
I ran the above program on both platforms Windows Vista and Windows Server 2003. While the anomaly presented here occurred on Windows Server 2003, it did not on Windows Vista. So the anomaly must have been fixed, but probably unintentionally, because we could not find any Knowledge Base article describing any fixes to the problem.
Comments
One Response to "Why Windows Timers May Not Fire"
don`t know is it interesting, but one twin bug in Win2003 X64(at least in first versions, i not have and
check latest updates, also it fix in Vista X64) wich can set very big problem to Debuggers.
I write self usermode(and partially kernelmode on x86) Debugger(for x86 and x64).but on x64 got
some problems: not interactive code debugged is ok, but when i attach to Ui app(notepad for example)
the window caption begin draw incorect, and when i select 'open' menu item, notepad is crached.
At first i think that error in my code(ms visual studio debugger 2005 work correct), but at finish, i found
that error in system : in function KiSystemServiceExit. look for this code :
mov [rbp-50h],eax
KiRestoreDebugRegisterState
mov eax,[rbp-50]
this code is execute only if DR registers is active on thread.if this (DR 0..3,7 not zero) before
call NtApi, KiSaveDebugRegisterState is called, and values in DRx is replaced, to sys defaults.
and at exit called KiRestoreDebugRegisterState. But at this point in rax register is return value
from api call.it must be saved before call KiRestoreDebugRegisterState, and than restored.
so right code must be
mov [rbp-50h],rax
KiRestoreDebugRegisterState
mov rax,[rbp-50]
this is on Vista X64, but on Win2003 X64 saved only eax, so high path of rax is lost.it not problem
for api from ntoskrnl becouse all it return NTSTATUS wich is DWORD size, but win32k.sys can return
full 64bit pointer.as result error is happens.for example you call SetWindowLongPtr for GWLP_WNDPROC
to set new WndProc, and save return value (OldWndProc) to call it from your new WndProc.but if
address of new OldProc is grether than 0xffffffff you got incorect value.in my example with notepad
in OpenDialog box combobox is subclussed.subclass code save old combobox wndproc, wich on my system
is 00007FF7F03FB40, but SetWindowLongPtr return 00000007F03FB40.and when from new proc old proc is
called by address 00000007F03FB40, exception is raised.the simpliest way to see this bug :
set DR registers on thread(by SetContextThread(CONTEXT_DEBUG_REGISTERS), nonzero Dr7 is enath),
then create test window and call
SetWindowLong(hwnd, GWLP_USERDATA, 0×123456789abcdef0);
SetWindowLong(hwnd, GWLP_USERDATA, 0×123456789abcdef0);
first call return 0, and second call return 0×000000009abcdef0( must return 0×123456789abcdef0),
hower if you call GetWindowLong(hwnd, GWLP_USERDATA), you got correct value 0×123456789abcdef0,
becouse this function can work in usermode only(in case only usermode code). the
ms visual studio debugger work correct becouse it not use DRx registers.for example to step over
function call, it set breakpoint in code, but i use Drx for this