PSEH

From ReactOS Wiki
Revision as of 16:24, 10 December 2008 by Hto (talk | contribs)
Jump to: navigation, search

PSEH is a thin library to enable the handling of system exceptions in Windows and ReactOS without compiler support for SEH (Structured Exception Handling). It's being developed and maintained by KJK::Hyperion. It has been proven reliable and portable. It's single-handedly what keeps ReactOS from crashing even more often, and if you'll ever write kernel code for ReactOS, basic knowledge of PSEH is pretty much a requirement.

This document assumes a certain degree of familiarity with SEH.

TODO: PSEH2.

New rules from KJK

Big PSEH revamp. If God is kind and merciful, this might be the last revision to PSEH ever

!!! RULE CHANGE !!! Obsoleted _SEH_NO_NATIVE_NLG, do NOT use it anymore, it will now cause fatal compile-time errors

!!! RULE CHANGE !!! As a side effect to the fix for a bug where a _SEH_TRY nested in a _SEH_HANDLE would lead to stack corruption, using "return" or "goto" from anywhere inside a PSEH block is now FORBIDDEN; all code that already did has been fixed in this revision

!!! NEW FEATURE !!! To leave a PSEH block from anywhere inside it, use the new _SEH_YIELD(<statement>) macro; examples: _SEH_YIELD(return value), _SEH_YIELD(goto label), _SEH_YIELD(returnvalue = value; goto label); ALWAYS ensure a _SEH_YIELD() leads outside the top-level _SEH_TRY block - do NOT goto into an ancestor _SEH_TRY block!!! Also note that _SEH_YIELD() disables SEH protection for the enclosed statement, so do NOT perform operations that might throw exceptions inside a _SEH_YIELD(); finally, ensure the enclosed statement does NOT allow execution to continue, or _SEH_YIELD() will get in an infinite loop; bear with me, for I have done the impossible, so don't expect miracles

Syntax

PSEH has two distinct syntaxes. They're completely equivalent and can be freely mixed in the same function. Virtually no functional difference exists. See the Design guide section for information beyond mere syntax.

Old syntax

The old syntax is slightly more efficient, but it's harder to translate native SEH into it (native SEH is the built-in compiler support for SEH as implemented by Visual C++, Borland C++ and others).

Click on an element to read more information about it.

_SEH_TRY_FILTER_FINALLY(FilterFunc, FinallyFunc)
{
   // Try block (code that might throw an exception)
   ...
}
_SEH_HANDLE
{
   // Handler (code executed if FilterFunc catches the exception)
   ...
}
_SEH_END;

Several short-hand forms are available:

  • Never catch exceptions, always execute a finally function:
 // Same as passing NULL for FilterFunc
 _SEH_TRY_FINALLY(FinallyFunc)
 {
 }
 _SEH_END_FINALLY;
  • Catch exceptions depending on a filter function, handle caught exceptions:
 // Same as passing NULL for FinallyFunc
 _SEH_TRY_FILTER(FilterFunc)
 {
 }
 _SEH_HANDLE
 {
 }
 _SEH_END;
  • Always handle exceptions, always execute a finally function:
 // Same as passing _SEH_STATIC_FILTER(_SEH_EXECUTE_HANDLER) for FilterFunc
 _SEH_TRY_HANDLE_FINALLY(FinallyFunc)
 {
 }
 _SEH_HANDLE
 {
 }
 _SEH_END;
  • Always handle exceptions:
 // Same as passing _SEH_STATIC_FILTER(_SEH_EXECUTE_HANDLER) for FilterFunc and
 // NULL for FinallyFunc
 _SEH_TRY
 {
 }
 _SEH_HANDLE
 {
 }
 _SEH_END;

New syntax

The new syntax was developed to cater for the need to translate existing code based on native SEH. With native SEH it shares the limitation of either a filter or a finally function for each try block. The syntax also doesn't reflect directly the actual flow of code, unlike the old syntax, so it is slightly less efficient.

Click on an element to read more information about it.

_SEH_TRY
{
   // Try block (code that might throw an exception)
   ...
}
_SEH_EXCEPT(FilterFunc)
{
   // Handler (code executed if FilterFunc catches the exception)
   ...
}
_SEH_END;
_SEH_TRY
{
   // Try block
   ...
}
_SEH_FINALLY(FinallyFunc)
_SEH_END;

Design guide

How to write try blocks

The try block contains the code that you expect to raise exceptions. Exceptions are expected when you deal with unsafe data (for example, buffers passed from user mode), or when you're calling functions that signal failure by raising an exception (examples are ProbeForRead, MmProbeAndLockPages and ExAllocatePoolWithQuotaTag). Do not just put all code inside a try block: unexpected exceptions are best left unhandled, so the system will halt and the bug causing the exception will be exposed.

A try block should be considered atomic. Don't try to jump into one, or jump out of one. Do not use continue or break directly inside a try block - try blocks are actually loops, so that won't have the effect you expect. The only accepted way to jump out of a try block is the _SEH_LEAVE macro: it will immediately terminate the current try block. The macro should be used just like break, and as such it must not be used inside a loop. Absolutely do not use return inside a try block: this will lead to crashes, or worse undetectable corruption, because some per-thread system state will still refer to local variables of the function you've returned from. Should this become a necessity, a _SEH_RETURN() macro will be provided. Contact the maintainer of PSEH if you need it.

A try block is optionally associated with a filter function and a finally function. The filter function is executed as soon as an exception is raised inside the try block (or inside any function called by the try block), or when another try block nested inside it declines to handle an exception, and its return value controls further processing of the exception. The finally function is called as soon as the try block goes out of scope, unless this happens in a way PSEH cannot control, such as jumping or returning out of the block.

Try blocks can be safely nested. Mixing try blocks in the old syntax with blocks in the new syntax is allowed too.

Try blocks aren't light-weight. They require quite some amount of precious stack (the kernel-mode stack has a size between 16 and 64 KB, versus the 1 MB in user-mode), and entering and leaving them involves a bit of manipulation. Use them sparingly, pretty much only when they're your only line of defense against system crashes. Try blocks nested into one another are significantly less expensive, but each level of nesting still has a cost - try to avoid nesting as well. Try not to use sibling try blocks in the same function: it's supported, but wastes CPU time and possibly stack space. Instead, try folding the two blocks into one, and using a state variable to track in which part of the block you are.

Examples

_SEH_TRY
{
  /* Probe the pointer */
  ProbeForRead(UnsafeObject, sizeof(OBJECT), 1);
  /* Copy to local */
  RtlCopyMemory(SafeObject, UnsafeObject, sizeof(OBJECT));
 }
 _SEH_HANDLE
 {
   Status = _SEH_GetExcetionCode();
 }
 _SEH_END
 if (!NT_SUCCESS(Status))
 {
   DoCleanup();
   return Status;
 }

How to write Filter functions

A Filter function is a function called when an exception is raised in a try block, or when the previous Filter function declines to handle an exception. The value returned from the Filter function specifies how the processing of the exception should continue:

_SEH_CONTINUE_SEARCH 
The handler cannot handle this exception: the next Filter function in the stack is called, and so on until one returns a value other than _SEH_CONTINUE_SEARCH. If all filter functions in the current thread return _SEH_CONTINUE_SEARCH, abnormal termination occurs. In user mode, the current process is terminated abnormally with the exception code as the return code. In kernel mode, the system stops with error 0x7E (SYSTEM_THREAD_EXCEPTION_NOT_HANDLED) if the current thread is a kernel-mode thread, or 0x8E (KERNEL_MODE_EXCEPTION_NOT_HANDLED) if the current thread is an user-mode thread executing in kernel mode. Despite this risk, returning _SEH_CONTINUE_SEARCH is not necessarily bad. If the exception is truly unexpected, it's better to crash the system, so the bug behind it will be exposed and can be tracked down.
_SEH_EXECUTE_HANDLER 
The handler can handle this exception: the processing of the exception stops, the faulting try block is interrupted just before the instruction that raised the exception, all the frames between the faulting try block and the previous try block are unwound (calling any Finally functions found along the way), and execution is resumed in the current try block's handler.
Especially if you have used native SEH support before, there is an important and non-obvious quirk of exception handling: as soon as an exception is handled, the current function's state is reset to the state before the try block was executed (this is a side effect of using the standard C function setjmp). This may or may not include the contents of any local variables, and any assumption in this sense is unreliable. In particular, after you catch an exception, you cannot read the contents of local variables and expect to read values written in the try block. There are two possible solutions:
  1. declaring the variables as volatile; the effectiveness of this greatly depends on the compiler;
  2. using the PSEH macros for sharing variables across functions. See the How to share variables section for more information. This can be cumbersome, but it's the safest way.
_SEH_CONTINUE_EXECUTION 
The Filter function can fix the error and cause the exception not to be raised again: the processing of the exception stops, and the interrupted try block is resumed just before the faulting instruction. This condition is hard to handle: don't return this code if you aren't sure the exception won't be raised again. Returning _SEH_CONTINUE_EXECUTION unconditionally, or without properly removing the cause of the exception, can hang the current thread in an endless loop.
Some exceptions, such as those raised explicitely with RaiseException, RtlRaiseException or ExRaiseStatus, aren't continuable in any way: explicit raises are considered unconditional jumps by most compilers, and there may well be no instruction at all after the one that raised the exception. You cannot continue such a non-continuable exception by returning _SEH_CONTINUE_EXECUTION. If you do, a STATUS_NONCONTINUABLE_EXCEPTION exception is raised.

Passing NULL as the Filter function has the same effect of a function that always returns _SEH_CONTINUE_SEARCH, but it's faster. Likewise, _SEH_STATIC_FILTER(_SEH_EXECUTE_HANDLER) is equivalent to a function that always returns _SEH_EXECUTE_HANDLER.

The Filter function is the only piece of code that can inspect what happened and where, or even alter the conditions that caused the exception. If you need to examine any data about the current exception other than the exception code, such as what instruction caused it, you have to do it in the Filter function. To this end, you can use two macros:

_SEH_GetExceptionPointers() 
Returns a pointer to an EXCEPTION_POINTERS structure. EXCEPTION_POINTERS is defined as:
typedef struct _EXCEPTION_POINTERS
{
   PEXCEPTION_RECORD ExceptionRecord;
   PCONTEXT ContextRecord;
}
EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;
ExceptionRecord points to an EXCEPTION_RECORD structure, containing all the basic information about the exception. EXCEPTION_RECORD is defined as:
typedef struct _EXCEPTION_RECORD
{
   NTSTATUS ExceptionCode;
   ULONG ExceptionFlags;
   struct _EXCEPTION_RECORD * ExceptionRecord;
   PVOID ExceptionAddress;
   ULONG NumberParameters;
   ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
}
EXCEPTION_RECORD;
The fields have the following meaning:
ExceptionCode 
The kind of exception that was raised. The most common exception codes are:
  • STATUS_ACCESS_VIOLATION: some code attempted to access inaccessible memory, for example it tried reading from unallocated memory or writing to read-only memory;
  • STATUS_DATATYPE_MISALIGNMENT: a data structure wasn't properly aligned;
  • STATUS_BREAKPOINT: a breakpoint instruction was met;
  • STATUS_INVALID_HANDLE: CloseHandle or ZwClose was called with an invalid handle;
ExceptionFlags 
Can be zero or EXCEPTION_NONCONTINUABLE. If EXCEPTION_NONCONTINUABLE is set, the exception cannot be continued by returning _SEH_CONTINUE_EXECUTION.
ExceptionRecord 
The exception that caused this exception. For example, if you try to continue a non-continuable exception, the STATUS_NONCONTINUABLE_EXCEPTION exception raised afterwards has this field pointing to the exception you attempted to continue.
ExceptionAddress 
Pointer to the instruction that raised this exception.
NumberParameters 
Number of valid elements in ExceptionInformation.
ExceptionInformation 
A maximum of 15 additional parameters for this exception. For example, STATUS_ACCESS_VIOLATION has two parameters. The first is zero if the inaccessible memory was read from, or non-zero if the inaccessible memory was written to. The second contains a pointer to the inaccessible memory.
ContextRecord points to a structure containing a copy of the CPU status at the time the exception was raised. The contents of the structure can be altered, and if the faulting code is continued after its alteration, it will continue with a CPU state altered in the same way. For more information about this structure, consult the Microsoft Platform SDK.
The structure returned by _SEH_GetExceptionPointers, and all the structures it points to, are only valid until the Filter function returns. If you need to access them later, you must make copies.
If you use _SEH_GetExceptionPointers, you need to include the system header that defines EXCEPTION_POINTERS and EXCEPTION_RECORD. The header for user mode is <windows.h>, and the header for kernel mode is the appropriate master header for your driver's model (<ntddk.h> for standard drivers, <wdm.h> for WDM drivers, etc.).
_SEH_GetExceptionCode() 
Returns the exception code as a signed long. This macro is faster than the functional equivalent _SEH_GetExceptionPointers()->ExceptionRecord.ExceptionCode. If all you need is the exception code (sufficient in most cases), use this macro.

Examples

[TODO]

How to write handlers

A handler is associated to a try block, and it's where execution resumes when the Filter function returns _SEH_EXECUTE_HANDLER. Please see the note about _SEH_EXECUTE_HANDLER in the How to write Filter functions section: it applies to handlers as well.

A handler executes outside of the protection of the associated try block: obviously, it cannot be used to handle exceptions it raised itself. A new try block can be created inside a handler, but this is not encouraged.

Handlers should be small, and limit themselves to signal the error condition so it can be handled in a central place. Any other processing should be done in a Filter function, where all the information about the exception is readily available, while a handler can only get the exception code with _SEH_GetExceptionCode(). Since NT error codes and exceptions share the same namespace, a common pattern for handlers is:

NTSTATUS Function(VOID)
{
   NTSTATUS Status;

   _SEH_TRY
   {
     Status = OtherFunction();

     if(!NT_SUCCESS(Status))
         _SEH_LEAVE;

      Status = YetAnotherFunction();
   }
   _SEH_HANDLE
   {
     Status = _SEH_GetExceptionCode();
   } 
   _SEH_END;

   if(!NT_SUCCESS(Status))
      LogError(Status);

   return Status;
}

Examples

[TODO]

How to write Finally functions

[TODO]

How to share variables

[TODO]