.NET Inside Out Part 29 – Terminating some existing thread with jumps

This is the twentieth ninth part of the .NET Inside Out series. For your convenience you can find other parts in the table of contents in Part 1 – Virtual and non-virtual calls in C#

Last time we discussed ways to terminate a thread. We mentioned a method based on unwinding the stack and preserving the registers. Today we’re going to implement this solution. I’m using .NET 5 on Windows 10 x64.

We start with the following code:

We create a thread which runs forever and we want to terminate it.

First, we need to modify the entry point of the thread and capture registers once it starts. Let’s do it:

First, we use a helper method GetArrayPosition to extract the address of array’s content. We first use it to get the address of an array to store registers which need to be preserved in x64 calling convention (these are rsp, rbp, rsi, rdi, rbx, r12, r13, r14, r15).

Next, we get the address of the entrypoint of the thread (lines 6-11). This is user’s code.

Now, we need to modify the entrypoint. We want to make it jump to our code so we need an array to hold the preamble. 12 bytes are enough (line 14).

Next, we craft the machine code to create a set jump callsite. Let’s go through it line by line:

Lines 20-21: we store the address of the array holding registers in the rax register so we can use it.
Lines 22-30: we copy registers one by one to the array.
Lines 31-32: we copy the address of the original entrypoint to the rax register so we can restore the entrypoint.
Lines 33-38: we copy 12 bytes from the array to the original entrypoint.
Lines 39-40: we jump back to the original (now restored) entrypoint.

Where do these 12 bytes come from? It’s in the HijackMethod which pushes address of the jump target (lines 62-65) and then jumps (lines 67-68). This is 12 bytes in total.

Okay, we have the entrypoint. We now go to the termination method:

First, we attach the debugger and dump threads (lines 5-17). We then extract the native thread id of the thread we want to kill (lines 21-27).

Next, we want to dump the stack to extract the return address (lines 29-51). What we do is we dump the output of !clrstack command, get line showing the ThreadHelper.ThreadStart_Context method which calls our entrypoint, and get its return address.

Finally, we create a command which restores all the addresses. Note that we can hardcode values in the command as we can read them from the array just like that. Notice also that we need to increase the stack pointer to drop the return address of the original entrypoint from it (line 57)

Code works and makes the thread to terminate gracefully. However, it doesn’t call any finally blocks. It just disappears. Combine this with our code for handling stack overflow issue and we now have a pretty robust test runner which can run your unit tests without risking StackOverflowException or other infinite loops.