Answered by:
A Bug in Windows Timer Management?

Question
-
Hi,
I recently used the WRK sources to figure out some details about Windows timer management, when I recognized the following comment in file base\ntos\inc\ntosdef.h on lines 618 thru 624:
Code Snippet00618 //
00619 // Define timer table size.
00620 //
00621 // N.B. The size of the timer table must be less than or equal to 256 and a
00622 // power of 2 in size.
00623
00624 #define TIMER_TABLE_SIZE 512Obviously, the definition of TIMER_TABLE_SIZE is inconsistent with the comment and it spent me some hours to find out that this might lead to a bug. I will briefly present my findings here, a more detailed version is available here.
When Windows inserts a timer object in its corresponding timer table entry, it stores the index of this entry in the timer's DISPATCHER_HEADER structure, i.e. DISPATCHER_HEADER::Hand. The index is calculated with the formula: Hand = (Due time / Maximum time) % TIMER_TABLE_SIZE. That means, Hand can be in range between 0 and 511. In function KeComputeDueTime() (see base\ntos\ke\ki.h), this index is cast into a UCHAR value, so index might overflow, because in a UCHAR you can only encode values between 0 and 255!
Code Snippet01219 *Hand = KiComputeTimerTableIndex(DueTime);
01220 Timer->Header.Hand = (UCHAR)*Hand;
01221 Timer->Header.Inserted = TRUE;
01222 return TRUE;This cast becomes a problem when Windows removes the timer from its timer list (KiRemoveEntryTimer in base\ntos\ke\ki.h):
Code Snippet01968 ULONG Hand;
01969 PKTIMER_TABLE_ENTRY TableEntry;
01970
01971 //
01972 // Remove the timer from the timer table. If the timer table list is
01973 // empty, then set the respective timer table due time to an infinite
01974 // absolute time.
01975 //
01976 // N.B. It is the responsibility of the caller to set the timer inserted
01977 // state.
01978 //
01979
01980 Hand = Timer->Header.Hand;
01981 if (RemoveEntryList(&Timer->TimerListEntry) != FALSE) {
01982 TableEntry = &KiTimerTableListHead[Hand];
01983 if (&TableEntry->Entry == TableEntry->Entry.Flink) {
01984 TableEntry->Time.HighPart = 0xffffffff;
01985 }
01986 }In the code snippet above, Header.Hand is used to identify the timer table entry. If the timer list gets empty, the due time must be set to an infinite absolute due time. Unfortunately, if Hand was originally, let's say, 257, Hand in the above code is actually 1 (257 % 256)! So, Windows checks whether the timer list KiTimerTableListHead[1] is empty, although it should have checked KiTimerTableListHead[257]. If KiTimerTablerListHead[1] is not empty, the Time field in KiTimerTableListHead[257] keeps its old value, i.e., an absolute due time in the past.
Now refer to KeUpdateSystemTime() in base\ntos\ke\arch\clockint.asm. The following code is used to determine, whether a timer has expired:
Code Snippet00212 ;
00213 ; Check to determine if a timer has expired.
00214 ; (edi:esi) = KiInterruptTime
00215 ; (eax) = bucket
00216 ; (ebx) = KeTickCount.LowPart
00217 ;
00218
00219 kust10: and eax,TIMER_TABLE_SIZE-1 ; isolate current hand value
00220 shl eax, 4 ; compute timer entry offset
00221 cmp esi,[eax]+_KiTimerTableListHead+TtTime+4 ; compare high due time
00222 jb kustxx ; if below, timer has not expired
00223 ja short kust15 ; if above, timer has expired
00224 cmp edi,[eax]+_KiTimerTableListHead+TtTime ; compare low due time
00225 jb kustxx ; if below, timer has not expired
00226 kust15: ;
00227As the Time field remains a valid but past due time, this code results in enqueuing a DPC routine to handle expired timers, although the timer list is empty!
Another critical routine is KiRemoveTreeTimer() (see base\ntos\ke\ki.h). This function does the same like KiRemoveEntryTimer, but additionaly, it also acquires the timer list lock. Again, the 8-bit wide Hand field is used to identify the lock of the timer list. When there was an overflow in Hand, the wrong list lock is acquired which might lead to corrupt timer list.
So, would you agree this is a bug or did I simply just overlook an important detail?
Regards,
Alexander
Friday, August 10, 2007 3:18 PM
Answers
-
In the array of size N the index of the element runs from 0 to N-1. So if N == 512 (power of 2), the maximal index value is 511 (0x1FF, 9 bits to encode). Casting it to UCHAR (8 bits wide) leaves only lower 8 bits, so effectively the index runs from 0 to 255 (0x0 to 0xFF), which would be good if the size of the array was 64 or 128 or 256 (note the comment to that effect in the code defining the array size). So Alex is absolutely correct.
- Proposed as answer by Shems Sunday, August 23, 2009 7:32 PM
- Marked as answer by A. Schmidt Monday, November 2, 2009 8:14 PM
Monday, October 13, 2008 8:33 PM
All replies
-
Ok, I have to reply here because it's been breaking my head for quite a while. At first glance, to me, it seemed mathematically unsound. I've actually translated the statement on the size of the table so as not to be erroneous. You state that the range would be between 0 to/and 511; which both are not powers of 2. The first power of 2 equals 1. Which in term means, given the statement, the range would be from 1 to/and 256. That would allow for a call on 257 to return to 1. In other words, I think what is meant here, is that there should be two tables in range 1 to/and 512.
Regards, Shems.
"When the machine breaks down, we break down."
Tuesday, October 7, 2008 3:02 PM -
In the array of size N the index of the element runs from 0 to N-1. So if N == 512 (power of 2), the maximal index value is 511 (0x1FF, 9 bits to encode). Casting it to UCHAR (8 bits wide) leaves only lower 8 bits, so effectively the index runs from 0 to 255 (0x0 to 0xFF), which would be good if the size of the array was 64 or 128 or 256 (note the comment to that effect in the code defining the array size). So Alex is absolutely correct.
- Proposed as answer by Shems Sunday, August 23, 2009 7:32 PM
- Marked as answer by A. Schmidt Monday, November 2, 2009 8:14 PM
Monday, October 13, 2008 8:33 PM