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');
__try {
putchar('2');
__debugbreak();
putchar('3');
} __except(GetExceptionInformation()->ContextRecord->Rip++,
EXCEPTION_CONTINUE_EXECUTION) {}
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(
_In_ PEXCEPTION_RECORD ExceptionRecord,
_In_ ULONG64 EstablisherFrame,
_Inout_ PCONTEXT ContextRecord,
_Inout_ PDISPATCHER_CONTEXT DispatcherContext
)
{
ContextRecord->Rip++;
return ExceptionContinueExecution;
}
__asm__(R"(
.extern my_handler
.extern putchar
.text
.balign 16
.globl main
.def main; .scl 2; .type 32; .endef
.seh_proc main
main:
sub $0x28, %rsp
.seh_stackalloc 0x28
.seh_endprologue
.seh_handler my_handler, @except
mov $0x31, %ecx
call putchar
mov $0x32, %ecx
call putchar
int3
mov $0x33, %ecx
call putchar
mov $0x34, %ecx
call putchar
xor %eax, %eax
add $0x28, %rsp
ret
.seh_endproc
)");
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
{
// count of records following
DWORD Count;
struct
{
// start rva of the range
DWORD BeginAddress;
// end rva of the range
DWORD EndAddress;
// if JumpTarget != 0:
// filter funclet rva OR one of:
// EXCEPTION_EXECUTE_HANDLER = 1
// EXCEPTION_CONTINUE_SEARCH = 0
// EXCEPTION_CONTINUE_EXECUTION = -1
// else:
// __finally handler (destructor)
DWORD HandlerAddress;
// rva of __except
DWORD JumpTarget;
} ScopeRecord[1];
} SCOPE_TABLE_AMD64, *PSCOPE_TABLE_AMD64;
struct _EXCEPTION_POINTERS;
typedef
LONG
(*PEXCEPTION_FILTER) (
struct _EXCEPTION_POINTERS *ExceptionPointers,
PVOID EstablisherFrame);
typedef
VOID
(*PTERMINATION_HANDLER) (
BOOLEAN AbnormalTermination,
PVOID EstablisherFrame);
# Implementing in assembly, round two
With the knowledge of how __C_specific_handler
operates we can implement a near-exact replica of our original C code using gas only:
extern "C" __attribute__((used)) LONG my_filter(
struct _EXCEPTION_POINTERS* ExceptionPointers,
PVOID EstablisherFrame
)
{
ExceptionPointers->ContextRecord->Rip++;
return EXCEPTION_CONTINUE_EXECUTION;
}
__asm__(R"(
.extern my_c_handler
.extern putchar
.extern __C_specific_handler
.text
.balign 16
.globl main
.def main; .scl 2; .type 32; .endef
.seh_proc main
main:
sub $0x28, %rsp
.seh_stackalloc 0x28
.seh_endprologue
.seh_handler __C_specific_handler, @except
.seh_handlerdata
.long 1
.long (seh_begin)@IMGREL
.long (seh_end)@IMGREL
.long (my_filter)@IMGREL
.long (seh_except)@IMGREL
.text
mov $0x31, %ecx
call putchar
seh_begin:
mov $0x32, %ecx
call putchar
int3
mov $0x33, %ecx
call putchar
nop
seh_end:
seh_except:
mov $0x34, %ecx
call putchar
xor %eax, %eax
add $0x28, %rsp
ret
.seh_endproc
)");
This looks quite a bit more like the C version. You might have noticed the extra nop
that was added after the call. This is not accidental, and MSVC does the exact same thing too. The reason is that due to how unwinding works, exceptions raised in callees will manifest at the return address, so after the call
instruction. Therefore a SEH range may never end on the end of a call
if you intend to catch exceptions raised inside.
# Rust ❤️ asm!
Now that we have the basics down, it’s time to move to Rust and inline assembly:
extern "C" fn my_putchar(c: u32) {
print!("{}", c as u8 as char);
}
unsafe extern "C" fn my_c_handler(
exception_pointers :PEXCEPTION_POINTERS,
establisher_frame :usize
) -> i32 {
let mut context = exception_pointers.as_mut().unwrap()
.ContextRecord.as_mut().unwrap();
context.Rip += 1;
winapi::vc::excpt::EXCEPTION_CONTINUE_EXECUTION
}
// LLVM doesn't know of the different EH personality
#[inline(never)]
// We need this because .seh_handlerdata switches sections
#[link_section = ".text"]
pub unsafe fn my_main() {
my_putchar('1' as u32);
asm!(
"2:",
"mov $0x32, %ecx",
"call {}",
"int3",
"mov $0x33, %ecx",
"call {}",
"nop",
"3:",
"nop",
".seh_handler __C_specific_handler, @except",
".seh_handlerdata",
".long 1",
".long (2b)@IMGREL",
".long (3b)@IMGREL",
".long ({})@IMGREL",
".long (3b)@IMGREL",
".text",
sym my_putchar,
sym my_putchar,
sym my_c_handler,
options(att_syntax),
);
my_putchar('4' as u32);
}
fn main() {
unsafe {
my_main();
}
}
The biggest change here is probably using inline assembly instead of the entire function being written in assembly. Due to asm!
’s requirements we need to use numbered labels with direction indicators, but otherwise the code looks very similar to the C / gas version.
# Catching exceptions
So far we’ve only been stepping over breakpoints. But what if we want to catch some exceptions too? Let’s try doing that. Unfortunately we can’t control the in-binary order of labels or parts of code, so we need to factor out the exception raising to a called function.
unsafe extern "C" fn my_c_handler(
exception_pointers :PEXCEPTION_POINTERS,
establisher_frame :usize
) -> i32 {
winapi::vc::excpt::EXCEPTION_EXECUTE_HANDLER
}
unsafe extern "C" fn my_try() {
println!("executed!");
asm!("int3");
println!("not executed!");
}
#[inline(never)]
#[link_section = ".text"]
pub unsafe fn my_main() {
asm!(
"2:",
"call {}",
"nop",
"3:",
"nop",
".seh_handler __C_specific_handler, @except",
".seh_handlerdata",
".long 1",
".long (2b)@IMGREL",
".long (3b)@IMGREL",
".long ({})@IMGREL",
".long (3b)@IMGREL",
".text",
sym my_try,
sym my_c_handler,
options(att_syntax),
);
}
This prints the expected executed!
followed by exiting.
# Hidden undesired behavior
From the previous example, you might have noticed a potential problem: Rust implements resource management using RAII. What happens to the objects that need to be dropped that would happen at the end of their lifetime when returning from the function? Let’s test it out!
struct MyDrop;
impl Drop for MyDrop {
fn drop(&mut self) {
println!("dropped!");
}
}
unsafe extern "C" fn my_try() {
let x = MyDrop;
println!("created!");
asm!("int3");
}
Output:
created!
Process finished with exit code 0
Well, this is pretty bad. This is clearly some undesired behavior that we have no way to test for and is very easy to accidentally trigger. However Rust does not consider resource leaks unsafe, and makes no guarantees on Drop
being called on anything in unsafe code. Still, hopefully we can do better.
# The solution of C++
This raises another question: How is this handled in C++ and could we copy the same in Rust?
void raise()
{
struct X {
X() { printf("constructed!\n"); }
~X() { printf("destructed!\n"); }
} x;
__debugbreak();
}
int main()
{
__try {
raise();
} __except(EXCEPTION_EXECUTE_HANDLER) {}
}
Compiling and running this in the default configuration produces the following output:
constructed!
Process finished with exit code 0
However after digging through some settings of the MSVC compiler in Visual Studio you can find the option for exception handling model (Enable C++ Exceptions
), which lists several options:
- Yes with SEH Exceptions (
/EHa
) - Yes (
/EHsc
) (default) - Yes with Extern C functions (
/EHs
) - No
After retrying with the first option, we receive a much better output:
constructed!
destructed!
Process finished with exit code 0
Upon investigating the binary, we can see what’s actually happening: The destructor funclets that are generated for C++ exceptions getting thrown in same-or-lower frame and caught in a higher frame trigger also when a SEH exception travels through the frame the same way. This makes the Windows exception handling model shine in an unexpected way: You can have various exception models in different functions or modules of a single call chain as long as the throwing and the catching side have the same expectations. Unfortunately this flexibility excludes languages that don’t have any sort of exceptions, like Rust, so using this is not really a possibility.
# Conclusions
While it is certainly possible to implement SEH in Rust, it actually provides worse experience than C++ (as leaking resources is usually undesired). The best bet is to implement wrappers in C++ and compile using /EHa
. Then we can call these from Rust.
bool WrapMmProbeAndLockPages(
PMDL MemoryDescriptorList,
KPROCESSOR_MODE AccessMode,
LOCK_OPERATION Operation
)
{
__try {
MmProbeAndLockPages(MemoryDescriptorList, AccessMode, Operation);
} __except (GetExceptionCode() == EXCEPTION_ACCESS_VIOLATION
? EXCEPTION_EXECUTE_HANDLER
: EXCEPTION_CONTINUE_SEARCH) {
return false;
}
return true;
}
In some specific scenarios (like ones that never execute the handler) it might however be reasonable to implement SEH fully in Rust.
Update: Not dropping is not considered unsafe in Rust. Thanks u/CrazyKilla15