Implementing SEH in Rust and why you shouldn't do it
-->
-->
# What is SEH?
Structured Exception Handling or SEH for short is a language extension to C for handling low level exceptions as well as the commonly known name for the entire exception handling model of Windows. In this article we will refer to the model in general as SEH unless noted otherwise, as the C language extension will be rarely relevant. Additionally, we will only talk about the model implemented by x64 and ARM64 architectures. x86 SEH uses an old setjmp-longjmp style non-zero-cost model, while other architectures use older but similar table-based versions of modern SEH. Even though it might not be strictly necessary, it is recommended that you read x64 exception handling from the Microsoft Docs.
# Why would I need SEH in Rust?
There are various problems that can only be solved with SEH. For one such instance, if you were to write a Windows driver and wanted to touch any usermode memory, you cannot reliably do that without SEH as there is no other way to ensure the memory isn’t being deallocated mid-access, all other ways cause some variation of the TOCTOU bug class. Additionally, several functions like MmProbeAndLockPages will raise exceptions by design.
Another common use case for SEH is continuable exceptions - exceptions where you can “solve” the cause and continue execution of the code. This is often used for implementing allocations on demand, custom CoW-like logic, etc..
# Our sample code
We chose a simple example that raises a debug exception that is ignored and continued from the next instruction. In C with SEH (language extension) the implementation looks like this:
putchar('1');<br>__try {<br>putchar('2');<br>__debugbreak();<br>putchar('3');<br>} __except(GetExceptionInformation()->ContextRecord->Rip++,<br>EXCEPTION_CONTINUE_EXECUTION) {}<br>putchar('4');
This will print 1234. While it looks pretty simple on the surface, the implementation is significantly more complex.
# Implementing in LLVM
Clang seems to have (limited but enough) support for SEH, so the first idea that’d come to mind is somehow using the LLVM IR’s features to implement the same for Rust. Unfortunately Rust doesn’t have any sort of “inline LLVM” or support for compiling LLVM IR modules next to Rust files. We’d need to require installing Clang which feels like avoiding the issue at hand.
# Implementing in assembly
After a bit of searching, it turns out that gas supports macros similar to the MASM ones provided by Microsoft for unwinding and exception handling. Their existence and usage is conveniently documented in a random mailing list post from 2009 that you can find somewhere near the last page of Google. LLVM’s assembler happens to also implement these macros. Let’s try implementing our example code in C++ with inline assembly to practice its behavior a bit.
extern "C" __attribute__((used)) EXCEPTION_DISPOSITION my_handler(<br>_In_ PEXCEPTION_RECORD ExceptionRecord,<br>_In_ ULONG64 EstablisherFrame,<br>_Inout_ PCONTEXT ContextRecord,<br>_Inout_ PDISPATCHER_CONTEXT DispatcherContext<br>ContextRecord->Rip++;<br>return ExceptionContinueExecution;
__asm__(R"(<br>.extern my_handler<br>.extern putchar
.text<br>.balign 16<br>.globl main<br>.def main; .scl 2; .type 32; .endef<br>.seh_proc main<br>main:<br>sub $0x28, %rsp<br>.seh_stackalloc 0x28<br>.seh_endprologue<br>.seh_handler my_handler, @except<br>mov $0x31, %ecx<br>call putchar<br>mov $0x32, %ecx<br>call putchar<br>int3<br>mov $0x33, %ecx<br>call putchar<br>mov $0x34, %ecx<br>call putchar<br>xor %eax, %eax<br>add $0x28, %rsp<br>ret<br>.seh_endproc<br>)");
Now you might say: Okay, this looks pretty terrible, but what’s up with that my_handler? It looks nothing like the __except block from earlier! And how does it know the ranges of the __try?
And you’re right! What we saw earlier was using the much simpler interface implemented by __C_specific_handler that handles all the jazz about ranges of __try blocks, filters, except blocks, etc.. This implementation also isn’t equivalent, as if the first putchar was raising an exception we’d accidentally skip it, as the handler applies to the entire function, not just the range.
Fortunately __C_specific_handler is implemented in ReactOS so we can easily take a peek at it to find out what kind of handler data it expects from us. The relevant types copied here, commented for your convenience:
typedef struct _SCOPE_TABLE_AMD64<br>// count of records following<br>DWORD Count;<br>struct<br>// start rva of the range<br>DWORD BeginAddress;<br>// end rva of the range<br>DWORD EndAddress;<br>// if JumpTarget != 0:<br>// filter funclet rva OR one of:<br>// EXCEPTION_EXECUTE_HANDLER = 1<br>// EXCEPTION_CONTINUE_SEARCH = 0<br>// EXCEPTION_CONTINUE_EXECUTION = -1<br>// else:<br>// __finally handler (destructor)<br>DWORD HandlerAddress;<br>// rva of __except<br>DWORD JumpTarget;<br>} ScopeRecord[1];<br>} SCOPE_TABLE_AMD64, *PSCOPE_TABLE_AMD64;
struct _EXCEPTION_POINTERS;<br>typedef<br>LONG<br>(*PEXCEPTION_FILTER) (<br>struct _EXCEPTION_POINTERS *ExceptionPointers,<br>PVOID EstablisherFrame);
typedef<br>VOID<br>(*PTERMINATION_HANDLER) (<br>BOOLEAN...