Jump to page: 1 24  
Page
Thread overview
Turning a SIGSEGV into a regular function call under Linux, allowing throw
Mar 13, 2012
FeepingCreature
Mar 13, 2012
deadalnix
Mar 13, 2012
FeepingCreature
Mar 13, 2012
Vladimir Panteleev
Mar 14, 2012
FeepingCreature
Mar 14, 2012
Vladimir Panteleev
Mar 14, 2012
deadalnix
Mar 14, 2012
Vladimir Panteleev
Mar 14, 2012
deadalnix
Mar 14, 2012
deadalnix
Mar 14, 2012
FeepingCreature
Mar 14, 2012
deadalnix
Mar 14, 2012
Vladimir Panteleev
Mar 14, 2012
deadalnix
Mar 14, 2012
Vladimir Panteleev
Mar 14, 2012
deadalnix
Mar 14, 2012
Vladimir Panteleev
Mar 14, 2012
deadalnix
Mar 14, 2012
Vladimir Panteleev
Mar 14, 2012
deadalnix
Mar 14, 2012
Vladimir Panteleev
Mar 14, 2012
deadalnix
Mar 14, 2012
H. S. Teoh
Mar 13, 2012
H. S. Teoh
Mar 14, 2012
Don Clugston
Mar 14, 2012
Don Clugston
Mar 14, 2012
deadalnix
Mar 14, 2012
H. S. Teoh
Mar 14, 2012
FeepingCreature
Mar 14, 2012
Sean Kelly
Mar 14, 2012
deadalnix
Mar 14, 2012
Don Clugston
Mar 15, 2012
deadalnix
Mar 15, 2012
Kagamin
Mar 15, 2012
FeepingCreature
Mar 17, 2012
deadalnix
March 13, 2012
Note: I worked out this method for my own language, Neat, but the basic approach should be portable to D's exceptions as well.

I've seen it argued a lot over the years (even argued it myself) that it's impossible to throw from Linux signal handlers. This is basically correct, because they constitute an interruption in the stack that breaks exceptions' ability to unroll properly.

However, there is a method to turn a signal handler into a regular function call that you can throw from.

Basically, what we need to do is similar to a stack buffer overflow exploit. Under Linux, the extended signal handler that is set with sigaction is called with three arguments: the signal, a siginfo_t* and a ucontext_t* as the third.

The third parameter is what we're interested in. Deep inside the ucontext_t struct is uc.mcontext.gregs[REG_EIP], the address of the instruction that caused the segfault. This is the location that execution returns to when the signal handler returns. By overwriting this location, we can turn a return into a function call.

First, gregs[REG_EAX] = gregs[REG_EIP];

We can safely assume that the function that caused the segfault doesn't really need its EAX anymore, so we can reuse it to reconstruct a proper stackframe to throw from later.

Second, gregs[REG_EIP] = cast(void*) &sigsegv_userspace_handler;

Note that the naked attribute was not used. If used, it can make this code slightly easier.

extern(C) void sigsegv_userspace_handler() {
  // done implicitly
  // asm { push ebp; }
  // asm { mov ebp, esp; }
  asm { mov ebx, [esp]; } // backup the pushed ebp
  asm { mov [esp], eax; } // replace it with the correct return address
                          // which was originally left out due to the
                          // irregular way we entered this function (via a ret).
  asm { push ebx; }       // recreate the pushed ebp
  asm { mov ebp, esp; }   // complete stackframe.
  // originally, our stackframe (because we entered this function via a ret)
  // was [ebp]. Now, it's [return address][ebp], as is proper for cdecl.
  // at this point, we can safely throw
  // (or invoke any other non-handler-safe function).
  throw new SignalException("SIGSEGV");
}
March 13, 2012
Le 13/03/2012 11:09, FeepingCreature a écrit :
> Note: I worked out this method for my own language, Neat, but the basic approach should be portable to D's exceptions as well.
>
> I've seen it argued a lot over the years (even argued it myself) that it's impossible to throw from Linux signal handlers. This is basically correct, because they constitute an interruption in the stack that breaks exceptions' ability to unroll properly.
>
> However, there is a method to turn a signal handler into a regular function call that you can throw from.
>
> Basically, what we need to do is similar to a stack buffer overflow exploit. Under Linux, the extended signal handler that is set with sigaction is called with three arguments: the signal, a siginfo_t* and a ucontext_t* as the third.
>
> The third parameter is what we're interested in. Deep inside the ucontext_t struct is uc.mcontext.gregs[REG_EIP], the address of the instruction that caused the segfault. This is the location that execution returns to when the signal handler returns. By overwriting this location, we can turn a return into a function call.
>
> First, gregs[REG_EAX] = gregs[REG_EIP];
>
> We can safely assume that the function that caused the segfault doesn't really need its EAX anymore, so we can reuse it to reconstruct a proper stackframe to throw from later.
>
> Second, gregs[REG_EIP] = cast(void*)&sigsegv_userspace_handler;
>
> Note that the naked attribute was not used. If used, it can make this code slightly easier.
>
> extern(C) void sigsegv_userspace_handler() {
>    // done implicitly
>    // asm { push ebp; }
>    // asm { mov ebp, esp; }
>    asm { mov ebx, [esp]; } // backup the pushed ebp
>    asm { mov [esp], eax; } // replace it with the correct return address
>                            // which was originally left out due to the
>                            // irregular way we entered this function (via a ret).
>    asm { push ebx; }       // recreate the pushed ebp
>    asm { mov ebp, esp; }   // complete stackframe.
>    // originally, our stackframe (because we entered this function via a ret)
>    // was [ebp]. Now, it's [return address][ebp], as is proper for cdecl.
>    // at this point, we can safely throw
>    // (or invoke any other non-handler-safe function).
>    throw new SignalException("SIGSEGV");
> }

And is this Exception recoverable in a safe way ?

The ucontext_t struct is system dependent. So this is tricky.

The Exception should be an Error to comply with nothrow spec.
March 13, 2012
On 03/13/12 11:23, deadalnix wrote:
> Le 13/03/2012 11:09, FeepingCreature a écrit :
>> Note: I worked out this method for my own language, Neat, but the basic approach should be portable to D's exceptions as well.
>>
>> I've seen it argued a lot over the years (even argued it myself) that it's impossible to throw from Linux signal handlers. This is basically correct, because they constitute an interruption in the stack that breaks exceptions' ability to unroll properly.
>>
>> However, there is a method to turn a signal handler into a regular function call that you can throw from.
>>
>> Basically, what we need to do is similar to a stack buffer overflow exploit. Under Linux, the extended signal handler that is set with sigaction is called with three arguments: the signal, a siginfo_t* and a ucontext_t* as the third.
>>
>> The third parameter is what we're interested in. Deep inside the ucontext_t struct is uc.mcontext.gregs[REG_EIP], the address of the instruction that caused the segfault. This is the location that execution returns to when the signal handler returns. By overwriting this location, we can turn a return into a function call.
>>
>> First, gregs[REG_EAX] = gregs[REG_EIP];
>>
>> We can safely assume that the function that caused the segfault doesn't really need its EAX anymore, so we can reuse it to reconstruct a proper stackframe to throw from later.
>>
>> Second, gregs[REG_EIP] = cast(void*)&sigsegv_userspace_handler;
>>
>> Note that the naked attribute was not used. If used, it can make this code slightly easier.
>>
>> extern(C) void sigsegv_userspace_handler() {
>>    // done implicitly
>>    // asm { push ebp; }
>>    // asm { mov ebp, esp; }
>>    asm { mov ebx, [esp]; } // backup the pushed ebp
>>    asm { mov [esp], eax; } // replace it with the correct return address
>>                            // which was originally left out due to the
>>                            // irregular way we entered this function (via a ret).
>>    asm { push ebx; }       // recreate the pushed ebp
>>    asm { mov ebp, esp; }   // complete stackframe.
>>    // originally, our stackframe (because we entered this function via a ret)
>>    // was [ebp]. Now, it's [return address][ebp], as is proper for cdecl.
>>    // at this point, we can safely throw
>>    // (or invoke any other non-handler-safe function).
>>    throw new SignalException("SIGSEGV");
>> }
> 
> And is this Exception recoverable in a safe way ?
> 

I'm not familiar with recovering. Note that you can _not_ safely return from the userspace handler, because we overwrote EAX to make space for our ESI backup.

You'd need to find somewhere else to stick that backup, like a TLS global variable or some known part of the stack.

> The ucontext_t struct is system dependent. So this is tricky.
> 

Yeah, this is Linux only.
March 13, 2012
On Tuesday, 13 March 2012 at 10:09:55 UTC, FeepingCreature wrote:
> However, there is a method to turn a signal handler into a regular function call that you can throw from.

Very nice!

The only similarity with a buffer overflow exploit is that we're overriding the continuation address. There is no execution of data, so it's closer to a "return-to-libc" attack. This is a very clean (and Neat) solution.

Here's a D implementation without inline assembler. It's DMD-specific due to a weirdness of its codegen.
http://dump.thecybershadow.net/20f792fa05c020e561137cfaf3d65d7a/sigthrow_32.d

The 64-bit version is a hack, in that it clobbers the last word on the stack. If the exception was thrown right after a stack frame was created, things might go ugly. The same trick as in my 32-bit implementation (creating a new stack frame with an extern(C) helper) won't work here, and I don't know enough about x64 exception handling to know how to fix it.
http://dump.thecybershadow.net/121efc460a01fb4597926ec76352a674/sigthrow_64.d

I think something like this needs to end up in Druntime, at least for Linux x86 and x64.
March 13, 2012
On Tue, Mar 13, 2012 at 11:09:54AM +0100, FeepingCreature wrote: [...]
> I've seen it argued a lot over the years (even argued it myself) that it's impossible to throw from Linux signal handlers. This is basically correct, because they constitute an interruption in the stack that breaks exceptions' ability to unroll properly.
> 
> However, there is a method to turn a signal handler into a regular function call that you can throw from.
> 
> Basically, what we need to do is similar to a stack buffer overflow exploit. Under Linux, the extended signal handler that is set with sigaction is called with three arguments: the signal, a siginfo_t* and a ucontext_t* as the third.
> 
> The third parameter is what we're interested in. Deep inside the ucontext_t struct is uc.mcontext.gregs[REG_EIP], the address of the instruction that caused the segfault. This is the location that execution returns to when the signal handler returns. By overwriting this location, we can turn a return into a function call.
> 
> First, gregs[REG_EAX] = gregs[REG_EIP];
> 
> We can safely assume that the function that caused the segfault doesn't really need its EAX anymore, so we can reuse it to reconstruct a proper stackframe to throw from later.
> 
> Second, gregs[REG_EIP] = cast(void*) &sigsegv_userspace_handler;
> 
> Note that the naked attribute was not used. If used, it can make this code slightly easier.
> 
> extern(C) void sigsegv_userspace_handler() {
>   // done implicitly
>   // asm { push ebp; }
>   // asm { mov ebp, esp; }
>   asm { mov ebx, [esp]; } // backup the pushed ebp
>   asm { mov [esp], eax; } // replace it with the correct return address
>                           // which was originally left out due to the
>                           // irregular way we entered this function (via a ret).
>   asm { push ebx; }       // recreate the pushed ebp
>   asm { mov ebp, esp; }   // complete stackframe.
>   // originally, our stackframe (because we entered this function via a ret)
>   // was [ebp]. Now, it's [return address][ebp], as is proper for cdecl.
>   // at this point, we can safely throw
>   // (or invoke any other non-handler-safe function).
>   throw new SignalException("SIGSEGV");
> }

Nice!! So basically you allow the signal handler to return cleanly so that we're out of signal-handling context, but overwrite the return address so that instead of returning to where the signal happened, it gets diverted to a special handler that reconstructs a stack frame and then throws. Cool beans!

The only drawback is, this only works on x86 Linux. I think it should be possible to make it work on non-x86 Linux by writing machine-specific code along the same principles. But I'm pretty sure it won't work for other unixen though.  They'll probably need their own system-specific hacks.


T

-- 
If you compete with slaves, you become a slave. -- Norbert Wiener
March 14, 2012
On 03/13/12 23:24, Vladimir Panteleev wrote:
> On Tuesday, 13 March 2012 at 10:09:55 UTC, FeepingCreature wrote:
>> However, there is a method to turn a signal handler into a regular function call that you can throw from.
> 
> Very nice!
> 
> The only similarity with a buffer overflow exploit is that we're overriding the continuation address. There is no execution of data, so it's closer to a "return-to-libc" attack.
> 

Argh. Yeah, that's the one I was thinking of.

> Here's a D implementation without inline assembler. It's DMD-specific due to a weirdness of its codegen. http://dump.thecybershadow.net/20f792fa05c020e561137cfaf3d65d7a/sigthrow_32.d
> 
> The 64-bit version is a hack, in that it clobbers the last word on the stack. If the exception was thrown right after a stack frame was created, things might go ugly. The same trick as in my 32-bit implementation (creating a new stack frame with an extern(C) helper) won't work here, and I don't know enough about x64 exception handling to know how to fix it.
> http://dump.thecybershadow.net/121efc460a01fb4597926ec76352a674/sigthrow_64.d
> 

Sweet. Yeah, I think you need to use naked and reconstruct the stackframe. Not sure how it'd look; I'm not familiar with the x86_64 ABI.
> I think something like this needs to end up in Druntime, at least for Linux x86 and x64.

Would be nice. I mean, Windows already has segfault-as-exception, doesn't it? It's only fair :)
March 14, 2012
Le 13/03/2012 23:24, Vladimir Panteleev a écrit :
> On Tuesday, 13 March 2012 at 10:09:55 UTC, FeepingCreature wrote:
>> However, there is a method to turn a signal handler into a regular
>> function call that you can throw from.
>
> Very nice!
>
> The only similarity with a buffer overflow exploit is that we're
> overriding the continuation address. There is no execution of data, so
> it's closer to a "return-to-libc" attack. This is a very clean (and
> Neat) solution.
>
> Here's a D implementation without inline assembler. It's DMD-specific
> due to a weirdness of its codegen.
> http://dump.thecybershadow.net/20f792fa05c020e561137cfaf3d65d7a/sigthrow_32.d
>
>
> The 64-bit version is a hack, in that it clobbers the last word on the
> stack. If the exception was thrown right after a stack frame was
> created, things might go ugly. The same trick as in my 32-bit
> implementation (creating a new stack frame with an extern(C) helper)
> won't work here, and I don't know enough about x64 exception handling to
> know how to fix it.
> http://dump.thecybershadow.net/121efc460a01fb4597926ec76352a674/sigthrow_64.d
>
>
> I think something like this needs to end up in Druntime, at least for
> Linux x86 and x64.

You are loosing EAX in the process.
March 14, 2012
On 03/14/12 12:13, deadalnix wrote:
> Le 13/03/2012 23:24, Vladimir Panteleev a écrit :
>> I think something like this needs to end up in Druntime, at least for Linux x86 and x64.
> 
> You are loosing EAX in the process.
It's somewhat unavoidable. One way or another, you need to find _some_ threadlocal spot to stick those extra size_t.sizeof bytes, since you mustn't lose data, but the hack works by _overwriting_ the return address.
March 14, 2012
On Wednesday, 14 March 2012 at 11:11:54 UTC, deadalnix wrote:
> You are loosing EAX in the process.

When would this matter? EAX is a scratch register per ABIs, no?
March 14, 2012
On Wednesday, 14 March 2012 at 07:35:50 UTC, FeepingCreature wrote:
> Sweet. Yeah, I think you need to use naked and reconstruct the stackframe. Not sure how it'd look; I'm not familiar with the x86_64 ABI.

I think it might be safe to just reconstruct the stack frame in the signal handler, and set gregs[REG_EIP] to &_d_throw directly. It should also use a pre-allocated exception object (like how it's done with OutofMemoryError and InvalidMemoryOperationError), in case there's data corruption in the GC.
« First   ‹ Prev
1 2 3 4