Debugging How GUI Thread Conversion on Svr03 Breaks the SEH Chain

The Windows kernel maintains two types of threads – Non-GUI threads, and GUI threads. Non-GUI threads threads use the default stack size of 12KB (on i386, which this this discussion applies to) and the default System Service Descriptor table (SSDT), KeServiceDescriptorTable. GUI threads, in contrast, are expected to have much larger stack requirements and thus use an extended stack size of 60 KB (Note: these are the numbers for Svr03 and may vary among releases). More importantly, however, GUI threads use a different SSDT – KeServiceDescriptorTableShadow. Unlike KeServiceDescriptorTable, which only supports the basic set of system calls, this SSDT also includes all the User and GDI system services.

All threads start off as Non-GUI threads. Once the application makes a call to a system service that does not fall within the default range, however, the NT kernel will suspect this thread to be about to do GUI stuff – and will convert the thread into a GUI thread.

Converting a thread to a GUI thread naturally has to entail two things – swapping the SSDT, and enlarging the stack. While swapping the SSDT is not really interesting, enlarging the stack size poses a challenge – you cannot really enlarge a stack as the nearby pages that would need to be acquired may not be available.

As a consequence, enlarging the stack works by swapping the stack. The old, small stack is exchanged against a newly allocated, larger stack. Now swapping a stack is not really a common thing to do and is pretty easy to get wrong. And well, as it turns out, the Svr03 kernel did in fact get it wrong.

But let’s start at the beginning.

When the number of the requested system service is found to be beyond the range supported by the default SSDT, KiConvertToGuiThread is called to perform the thread conversion. KiConvertToGuiThread itself is pretty dumb and lets PsConvertToGuiThread do the actual work.

The following pseudo code illustrates what PsConvertToGuiThread does:

    NTSTATUS PsConvertToGuiThread()
    {
      //
      // Create the new stack.
      //
      LargeStack = MmCreateKernelStack( ... )
      
      if ( LargeStack == NULL )
      {
        __try
        {
          //
          // Allocation failed -- set last error value.
          //
          NtCurrentTeb()->LastErrorValue = ERROR_NOT_ENOUGH_MEMORY;
        }
        __except( ... )
        {
        }
        
        //
        // N.B. We are still on the old stack.
        //
        
        //
        // This will copy the old thread's contents to the new stack and 
        // migrate the context of the current thread to the new stack.
        //
        SmallStack = KeSwitchKernelStack( LargeStack, ... );
    
        //
        // Now we are on the new stack.
        //
        MmDeleteKernelStack( SmallStack, ... );
      }
      ...
      //
      // Notify Win32k.
      //
      
      ( PspW32ProcessCallout )( ... )
      ...
      ( PspW32ThreadCallout ) ( ... )
      
      ...
    }
    

This code looks innocent enough, but infact, it is lying. Too see why, you have to recall how Structured Exception Handling is implemented on i386 and how the C compiler makes use of it (I think I have spent way too much time with SEH over the past months…): The try/except-block at the top of the routine will cause to the compiler to emit the typical SEH prolog at the beginning of the function. The purpose of this prolog is to set up an EXCEPTION_REGISTRATION_RECORD and to put this record onto the current thread’s SEH chain, which in turn is rooted in the PCR. In the same way, the compiler will put an appropriate epilog to the end of the routine.

So while the code above suggests that the SEH stuff is scoped to the very beginning of the function, it will not be until the end of the function has been reached that the EXCEPTION_REGISTRATION_RECORD is torn down and removed from the SEH chain.

And at this point, it should become clear why this becomes a problem in the context of stack swapping. At the point where KeSwitchKernelStack is called, the EXCEPTION_REGISTRATION_RECORD will still be listed in the SEH chain, although it does not serve any particular purpose any more. So KeSwitchKernelStack is called, which will, as indicated before, copy the contents of the old stack to the new stack – which, of course, includes the EXCEPTION_REGISTRATION_RECORD.

But…

neither KeSwitchKernelStack, nor PsConvertToGuiThread updates the SEH pointer in the PCR! After the swapping has been conducted and MmDeleteKernelStack has returned, the root of the SEH chain will point to freed memory – memory where the EXCEPTION_REGISTRATION_RECORD once has been.

Now two things are worth noting. First, PsConvertToGuiThread can be expected to occupy the bottommost stack frame of the kernel stack. A situation where the dangling pointer could harm a caller of PsConvertToGuiThread is thus not possible.

Secondly, PsConvertToGuiThread makes callouts to Win32k by invoking the callbacks pointed to by PspW32ProcessCallout and PspW32ThreadCallout. And in fact, it is only PsConvertToGuiThread’s luck that these routines are so well behaved that they do not cause the system to bugcheck because of the dangling pointer. If one of these routines (or routines called by these) did anything with the SEH chain going beyond adding another record to the chain and removing it later, odds were that this routine would dereference a stray pointer… and would bugcheck the system…

_It is worth noting that the implementation of PsConvertToGuiThread has changed in Windows Vista, so that the above discussion does not apply to this and later releases. _

Any opinions expressed on this blog are Johannes' own. Refer to the respective vendor’s product documentation for authoritative information.
« Back to home