Add an assert-based segfault handler to etc.linux.memoryerror (#20643)

* Add an assert-based segfault handler to `etc.linux.memoryerror`

* Commit memoryAssertError review feedback

* Indent the MemoryErrorSupported version block

* Fix a bad ucontext_t in memoryerror.d

* Fix bad imports in memoryerror.d

* Use a module-scope version: in memoryerror.d

* Add a memoryerror.d unittest

* Prefer version-else-version... in memoryerror.d
This commit is contained in:
Jonas Meeuws 2025-01-18 22:38:54 +01:00 committed by GitHub
parent e49b67e969
commit d115713410
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 552 additions and 298 deletions

View file

@ -1,9 +1,5 @@
/**
* Handle page protection errors using D errors (exceptions). $(D NullPointerError) is
* thrown when dereferencing null pointers. A system-dependent error is thrown in other
* cases.
*
* Note: Only x86 and x86_64 are supported for now.
* Handle page protection errors using D errors (exceptions) or asserts.
*
* License: Distributed under the
* $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost Software License 1.0).
@ -14,313 +10,444 @@
module etc.linux.memoryerror;
version (CRuntime_Glibc)
version (linux)
{
version (X86)
version = MemoryErrorSupported;
version (X86_64)
version = MemoryErrorSupported;
version (DigitalMars)
{
version (CRuntime_Glibc)
{
version (X86)
version = MemoryErrorSupported;
else version (X86_64)
version = MemoryErrorSupported;
}
}
}
version (MemoryErrorSupported):
@system:
version (linux)
{
version (X86)
version = MemoryAssertSupported;
else version (X86_64)
version = MemoryAssertSupported;
else version (ARM)
version = MemoryAssertSupported;
else version (AArch64)
version = MemoryAssertSupported;
else version (PPC64)
version = MemoryAssertSupported;
}
version (MemoryErrorSupported)
version = AnySupported;
else version (MemoryErrorSupported)
version = AnySupported;
version (AnySupported):
import core.sys.posix.signal : SA_SIGINFO, sigaction, sigaction_t, siginfo_t, SIGSEGV;
import ucontext = core.sys.posix.ucontext;
// Register and unregister memory error handler.
bool registerMemoryErrorHandler() nothrow
version (MemoryAssertSupported)
{
sigaction_t action;
action.sa_sigaction = &handleSignal;
action.sa_flags = SA_SIGINFO;
auto oldptr = &old_sigaction;
return !sigaction(SIGSEGV, &action, oldptr);
import core.sys.posix.signal : SA_ONSTACK, sigaltstack, SIGSTKSZ, stack_t;
}
bool deregisterMemoryErrorHandler() nothrow
{
auto oldptr = &old_sigaction;
return !sigaction(SIGSEGV, oldptr, null);
}
/**
* Thrown on POSIX systems when a SIGSEGV signal is received.
*/
class InvalidPointerError : Error
{
this(string file = __FILE__, size_t line = __LINE__, Throwable next = null) nothrow
{
super("", file, line, next);
}
this(Throwable next, string file = __FILE__, size_t line = __LINE__) nothrow
{
super("", file, line, next);
}
}
/**
* Thrown on null pointer dereferences.
*/
class NullPointerError : InvalidPointerError
{
this(string file = __FILE__, size_t line = __LINE__, Throwable next = null) nothrow
{
super(file, line, next);
}
this(Throwable next, string file = __FILE__, size_t line = __LINE__) nothrow
{
super(file, line, next);
}
}
unittest
{
int* getNull() { return null; }
assert(registerMemoryErrorHandler());
bool b;
try
{
*getNull() = 42;
}
catch (NullPointerError)
{
b = true;
}
assert(b);
b = false;
try
{
*getNull() = 42;
}
catch (InvalidPointerError)
{
b = true;
}
assert(b);
assert(deregisterMemoryErrorHandler());
}
// Signal handler space.
private:
__gshared sigaction_t old_sigaction;
alias typeof(ucontext.ucontext_t.init.uc_mcontext.gregs[0]) RegType;
version (X86_64)
{
static RegType savedRDI, savedRSI;
extern(C)
void handleSignal(int signum, siginfo_t* info, void* contextPtr) nothrow
{
auto context = cast(ucontext.ucontext_t*)contextPtr;
// Save registers into global thread local, to allow recovery.
savedRDI = context.uc_mcontext.gregs[ucontext.REG_RDI];
savedRSI = context.uc_mcontext.gregs[ucontext.REG_RSI];
// Hijack current context so we call our handler.
auto rip = context.uc_mcontext.gregs[ucontext.REG_RIP];
auto addr = cast(RegType) info.si_addr;
context.uc_mcontext.gregs[ucontext.REG_RDI] = addr;
context.uc_mcontext.gregs[ucontext.REG_RSI] = rip;
context.uc_mcontext.gregs[ucontext.REG_RIP] = cast(RegType) ((rip != addr)?&sigsegvDataHandler:&sigsegvCodeHandler);
}
// All handler functions must be called with faulting address in RDI and original RIP in RSI.
// This function is called when the segfault's cause is to call an invalid function pointer.
void sigsegvCodeHandler()
{
asm
{
naked;
// Handle the stack for an invalid function call (segfault at RIP).
// With the return pointer, the stack is now alligned.
push RBP;
mov RBP, RSP;
jmp sigsegvDataHandler;
}
}
void sigsegvDataHandler()
{
asm
{
naked;
push RSI; // return address (original RIP).
push RBP; // old RBP
mov RBP, RSP;
pushfq; // Save flags.
push RAX; // RAX, RCX, RDX, and R8 to R11 are trash registers and must be preserved as local variables.
push RCX;
push RDX;
push R8;
push R9;
push R10;
push R11; // With 10 pushes, the stack is still aligned.
// Parameter address is already set as RAX.
call sigsegvUserspaceProcess;
// Restore RDI and RSI values.
call restoreRDI;
push RAX; // RDI is in RAX. It is pushed and will be poped back to RDI.
call restoreRSI;
mov RSI, RAX;
pop RDI;
// Restore trash registers value.
pop R11;
pop R10;
pop R9;
pop R8;
pop RDX;
pop RCX;
pop RAX;
popfq; // Restore flags.
// Return
pop RBP;
ret;
}
}
// The return value is stored in EAX and EDX, so this function restore the correct value for theses registers.
RegType restoreRDI()
{
return savedRDI;
}
RegType restoreRSI()
{
return savedRSI;
}
}
else version (X86)
{
static RegType savedEAX, savedEDX;
extern(C)
void handleSignal(int signum, siginfo_t* info, void* contextPtr) nothrow
{
auto context = cast(ucontext.ucontext_t*)contextPtr;
// Save registers into global thread local, to allow recovery.
savedEAX = context.uc_mcontext.gregs[ucontext.REG_EAX];
savedEDX = context.uc_mcontext.gregs[ucontext.REG_EDX];
// Hijack current context so we call our handler.
auto eip = context.uc_mcontext.gregs[ucontext.REG_EIP];
auto addr = cast(RegType) info.si_addr;
context.uc_mcontext.gregs[ucontext.REG_EAX] = addr;
context.uc_mcontext.gregs[ucontext.REG_EDX] = eip;
context.uc_mcontext.gregs[ucontext.REG_EIP] = cast(RegType) ((eip != addr)?&sigsegvDataHandler:&sigsegvCodeHandler);
}
// All handler functions must be called with faulting address in EAX and original EIP in EDX.
// This function is called when the segfault's cause is to call an invalid function pointer.
void sigsegvCodeHandler()
{
asm
{
naked;
// Handle the stack for an invalid function call (segfault at EIP).
// 4 bytes are used for function pointer; We need 12 byte to keep stack aligned.
sub ESP, 12;
mov 8[ESP], EBP;
mov EBP, ESP;
jmp sigsegvDataHandler;
}
}
void sigsegvDataHandler()
{
asm
{
naked;
// We jump directly here if we are in a valid function call case.
push EDX; // return address (original EIP).
push EBP; // old EBP
mov EBP, ESP;
pushfd; // Save flags.
push ECX; // ECX is a trash register and must be preserved as local variable.
// 4 pushes have been done. The stack is aligned.
// Parameter address is already set as EAX.
call sigsegvUserspaceProcess;
// Restore register values and return.
call restoreRegisters;
pop ECX;
popfd; // Restore flags.
// Return
pop EBP;
ret;
}
}
// The return value is stored in EAX and EDX, so this function restore the correct value for theses registers.
RegType[2] restoreRegisters()
{
RegType[2] restore;
restore[0] = savedEAX;
restore[1] = savedEDX;
return restore;
}
}
else
{
static assert(false, "Unsupported architecture.");
}
// This should be calculated by druntime.
// TODO: Add a core.memory function for this.
enum PAGE_SIZE = 4096;
@system:
// The first 64Kb are reserved for detecting null pointer dereferences.
enum MEMORY_RESERVED_FOR_NULL_DEREFERENCE = 4096 * 16;
// TODO: this is a platform-specific assumption, can be made more robust
private enum size_t MEMORY_RESERVED_FOR_NULL_DEREFERENCE = 4096 * 16;
// User space handler
void sigsegvUserspaceProcess(void* address)
version (MemoryErrorSupported)
{
// SEGV_MAPERR, SEGV_ACCERR.
// The first page is protected to detect null dereferences.
if ((cast(size_t) address) < MEMORY_RESERVED_FOR_NULL_DEREFERENCE)
/**
* Register memory error handler, store the old handler.
*
* `NullPointerError` is thrown when dereferencing null pointers.
* A generic `InvalidPointerError` error is thrown in other cases.
*
* Returns: whether the registration was successful
*
* Limitations: Only x86 and x86_64 are supported for now.
*/
bool registerMemoryErrorHandler() nothrow
{
throw new NullPointerError();
sigaction_t action;
action.sa_sigaction = &handleSignal;
action.sa_flags = SA_SIGINFO;
auto oldptr = &oldSigactionMemoryError;
return !sigaction(SIGSEGV, &action, oldptr);
}
throw new InvalidPointerError();
/**
* Revert the memory error handler back to the one from before calling `registerMemoryErrorHandler()`.
*
* Returns: whether the registration of the old handler was successful
*/
bool deregisterMemoryErrorHandler() nothrow
{
auto oldptr = &oldSigactionMemoryError;
return !sigaction(SIGSEGV, oldptr, null);
}
/**
* Thrown on POSIX systems when a SIGSEGV signal is received.
*/
class InvalidPointerError : Error
{
this(string file = __FILE__, size_t line = __LINE__, Throwable next = null) nothrow
{
super("", file, line, next);
}
this(Throwable next, string file = __FILE__, size_t line = __LINE__) nothrow
{
super("", file, line, next);
}
}
/**
* Thrown on null pointer dereferences.
*/
class NullPointerError : InvalidPointerError
{
this(string file = __FILE__, size_t line = __LINE__, Throwable next = null) nothrow
{
super(file, line, next);
}
this(Throwable next, string file = __FILE__, size_t line = __LINE__) nothrow
{
super(file, line, next);
}
}
unittest
{
int* getNull() { return null; }
assert(registerMemoryErrorHandler());
bool b;
try
{
*getNull() = 42;
}
catch (NullPointerError)
{
b = true;
}
assert(b);
b = false;
try
{
*getNull() = 42;
}
catch (InvalidPointerError)
{
b = true;
}
assert(b);
assert(deregisterMemoryErrorHandler());
}
// Signal handler space.
private:
__gshared sigaction_t oldSigactionMemoryError;
alias RegType = typeof(ucontext.ucontext_t.init.uc_mcontext.gregs[0]);
version (X86_64)
{
static RegType savedRDI, savedRSI;
extern(C)
void handleSignal(int signum, siginfo_t* info, void* contextPtr) nothrow
{
auto context = cast(ucontext.ucontext_t*)contextPtr;
// Save registers into global thread local, to allow recovery.
savedRDI = context.uc_mcontext.gregs[ucontext.REG_RDI];
savedRSI = context.uc_mcontext.gregs[ucontext.REG_RSI];
// Hijack current context so we call our handler.
auto rip = context.uc_mcontext.gregs[ucontext.REG_RIP];
auto addr = cast(RegType) info.si_addr;
context.uc_mcontext.gregs[ucontext.REG_RDI] = addr;
context.uc_mcontext.gregs[ucontext.REG_RSI] = rip;
context.uc_mcontext.gregs[ucontext.REG_RIP] = cast(RegType) ((rip != addr)?&sigsegvDataHandler:&sigsegvCodeHandler);
}
// All handler functions must be called with faulting address in RDI and original RIP in RSI.
// This function is called when the segfault's cause is to call an invalid function pointer.
void sigsegvCodeHandler()
{
asm
{
naked;
// Handle the stack for an invalid function call (segfault at RIP).
// With the return pointer, the stack is now alligned.
push RBP;
mov RBP, RSP;
jmp sigsegvDataHandler;
}
}
void sigsegvDataHandler()
{
asm
{
naked;
push RSI; // return address (original RIP).
push RBP; // old RBP
mov RBP, RSP;
pushfq; // Save flags.
push RAX; // RAX, RCX, RDX, and R8 to R11 are trash registers and must be preserved as local variables.
push RCX;
push RDX;
push R8;
push R9;
push R10;
push R11; // With 10 pushes, the stack is still aligned.
// Parameter address is already set as RAX.
call sigsegvUserspaceProcess;
// Restore RDI and RSI values.
call restoreRDI;
push RAX; // RDI is in RAX. It is pushed and will be poped back to RDI.
call restoreRSI;
mov RSI, RAX;
pop RDI;
// Restore trash registers value.
pop R11;
pop R10;
pop R9;
pop R8;
pop RDX;
pop RCX;
pop RAX;
popfq; // Restore flags.
// Return
pop RBP;
ret;
}
}
// The return value is stored in EAX and EDX, so this function restore the correct value for theses registers.
RegType restoreRDI()
{
return savedRDI;
}
RegType restoreRSI()
{
return savedRSI;
}
}
else version (X86)
{
static RegType savedEAX, savedEDX;
extern(C)
void handleSignal(int signum, siginfo_t* info, void* contextPtr) nothrow
{
auto context = cast(ucontext.ucontext_t*)contextPtr;
// Save registers into global thread local, to allow recovery.
savedEAX = context.uc_mcontext.gregs[ucontext.REG_EAX];
savedEDX = context.uc_mcontext.gregs[ucontext.REG_EDX];
// Hijack current context so we call our handler.
auto eip = context.uc_mcontext.gregs[ucontext.REG_EIP];
auto addr = cast(RegType) info.si_addr;
context.uc_mcontext.gregs[ucontext.REG_EAX] = addr;
context.uc_mcontext.gregs[ucontext.REG_EDX] = eip;
context.uc_mcontext.gregs[ucontext.REG_EIP] = cast(RegType) ((eip != addr)?&sigsegvDataHandler:&sigsegvCodeHandler);
}
// All handler functions must be called with faulting address in EAX and original EIP in EDX.
// This function is called when the segfault's cause is to call an invalid function pointer.
void sigsegvCodeHandler()
{
asm
{
naked;
// Handle the stack for an invalid function call (segfault at EIP).
// 4 bytes are used for function pointer; We need 12 byte to keep stack aligned.
sub ESP, 12;
mov [ESP + 8], EBP;
mov EBP, ESP;
jmp sigsegvDataHandler;
}
}
void sigsegvDataHandler()
{
asm
{
naked;
// We jump directly here if we are in a valid function call case.
push EDX; // return address (original EIP).
push EBP; // old EBP
mov EBP, ESP;
pushfd; // Save flags.
push ECX; // ECX is a trash register and must be preserved as local variable.
// 4 pushes have been done. The stack is aligned.
// Parameter address is already set as EAX.
call sigsegvUserspaceProcess;
// Restore register values and return.
call restoreRegisters;
pop ECX;
popfd; // Restore flags.
// Return
pop EBP;
ret;
}
}
// The return value is stored in EAX and EDX, so this function restore the correct value for theses registers.
RegType[2] restoreRegisters()
{
RegType[2] restore;
restore[0] = savedEAX;
restore[1] = savedEDX;
return restore;
}
}
else
{
static assert(false, "Unsupported architecture.");
}
// User space handler
void sigsegvUserspaceProcess(void* address)
{
// SEGV_MAPERR, SEGV_ACCERR.
// The first page is protected to detect null dereferences.
if ((cast(size_t) address) < MEMORY_RESERVED_FOR_NULL_DEREFERENCE)
{
throw new NullPointerError();
}
throw new InvalidPointerError();
}
}
version (MemoryAssertSupported)
{
private __gshared sigaction_t oldSigactionMemoryAssert; // sigaction before calling `registerMemoryAssertHandler`
/**
* Registers a signal handler for SIGSEGV that turns them into an assertion failure,
* providing a more descriptive error message and stack trace if the program is
* compiled with debug info and D assertions (as opposed to C assertions).
*
* Differences with the `registerMemoryErrorHandler` version are:
* - The handler is registered with SA_ONSTACK, so it can handle stack overflows.
* - It uses `assert(0)` instead of `throw new Error` and doesn't support catching the error.
* - This is a template so that the -check and -checkaction flags of the compiled program are used,
* instead of the ones used for compiling druntime.
*
* Returns: whether the registration was successful
*/
bool registerMemoryAssertHandler()()
{
nothrow @nogc extern(C)
void _d_handleSignalAssert(int signum, siginfo_t* info, void* contextPtr)
{
// Guess the reason for the segfault by seeing if the faulting address
// is close to the stack pointer or the null pointer.
const void* segfaultingPtr = info.si_addr;
auto context = cast(ucontext.ucontext_t*) contextPtr;
version (X86_64)
const stackPtr = cast(void*) context.uc_mcontext.gregs[ucontext.REG_RSP];
else version (X86)
const stackPtr = cast(void*) context.uc_mcontext.gregs[ucontext.REG_ESP];
else version (ARM)
const stackPtr = cast(void*) context.uc_mcontext.arm_sp;
else version (AArch64)
const stackPtr = cast(void*) context.uc_mcontext.sp;
else version (PPC64)
const stackPtr = cast(void*) context.uc_mcontext.regs.gpr[1];
else
static assert(false, "Unsupported architecture."); // TODO: other architectures
auto distanceToStack = cast(ptrdiff_t) (stackPtr - segfaultingPtr);
if (distanceToStack < 0)
distanceToStack = -distanceToStack;
if (stackPtr && distanceToStack <= 4096)
assert(false, "segmentation fault: call stack overflow");
else if (cast(size_t) segfaultingPtr < MEMORY_RESERVED_FOR_NULL_DEREFERENCE)
assert(false, "segmentation fault: null pointer read/write operation");
else
assert(false, "segmentation fault: invalid pointer read/write operation");
}
sigaction_t action;
action.sa_sigaction = &_d_handleSignalAssert;
action.sa_flags = SA_SIGINFO | SA_ONSTACK;
// Set up alternate stack, because segfaults can be caused by stack overflow,
// in which case the stack is already exhausted
__gshared ubyte[SIGSTKSZ] altStack;
stack_t ss;
ss.ss_sp = altStack.ptr;
ss.ss_size = altStack.length;
ss.ss_flags = 0;
if (sigaltstack(&ss, null) == -1)
return false;
return !sigaction(SIGSEGV, &action, &oldSigactionMemoryAssert);
}
/**
* Revert the memory error handler back to the one from before calling `registerMemoryAssertHandler()`.
*
* Returns: whether the registration of the old handler was successful
*/
bool deregisterMemoryAssertHandler()
{
return !sigaction(SIGSEGV, &oldSigactionMemoryAssert, null);
}
unittest
{
// Testing actual memory errors is done in the test suite
assert(registerMemoryAssertHandler());
assert(deregisterMemoryAssertHandler());
}
}

View file

@ -460,6 +460,13 @@ private extern (C) int _d_run_main2(char[][] args, size_t totalArgsLength, MainF
useExceptionTrap = false;
}
version (none)
{
// Causes test failures related to Fibers, not enabled by default yet
import etc.linux.memoryerror;
cast(void) registerMemoryAssertHandler();
}
void tryExec(scope void delegate() dg)
{
if (useExceptionTrap)

View file

@ -12,7 +12,8 @@ SED:=sed
GDB:=gdb
ifeq ($(OS),linux)
TESTS+=line_trace line_trace_21656 long_backtrace_trunc rt_trap_exceptions cpp_demangle
TESTS+=line_trace line_trace_21656 long_backtrace_trunc rt_trap_exceptions cpp_demangle \
memoryerror_null_read memoryerror_null_write memoryerror_null_call memoryerror_stackoverflow
line_trace_dflags:=-L--export-dynamic
endif
@ -88,6 +89,11 @@ $(ROOT)/rt_trap_exceptions.done: stderr_exp2="src/rt_trap_exceptions.d:8 main"
$(ROOT)/assert_fail.done: stderr_exp="success."
$(ROOT)/cpp_demangle.done: stderr_exp="thrower(int)"
$(ROOT)/message_with_null.done: stderr_exp=" world"
$(ROOT)/memoryerror_null_read.done: stderr_exp="segmentation fault: null pointer read/write operation"
$(ROOT)/memoryerror_null_write.done: stderr_exp="segmentation fault: null pointer read/write operation"
$(ROOT)/memoryerror_null_call.done: stderr_exp="segmentation fault: null pointer read/write operation"
$(ROOT)/memoryerror_null_call.done: stderr_exp2="uncaught exception reached top of stack"
$(ROOT)/memoryerror_stackoverflow.done: stderr_exp="segmentation fault: call stack overflow"
$(ROOT)/%.done: $(ROOT)/%$(DOTEXE)
@echo Testing $*

View file

@ -0,0 +1,9 @@
import etc.linux.memoryerror;
void function() foo = null;
void main()
{
registerMemoryAssertHandler;
foo();
}

View file

@ -0,0 +1,9 @@
import etc.linux.memoryerror;
int* x = null;
void main()
{
registerMemoryAssertHandler;
*x = 3;
}

View file

@ -0,0 +1,9 @@
import etc.linux.memoryerror;
int* x = null;
int main()
{
registerMemoryAssertHandler;
return *x;
}

View file

@ -0,0 +1,22 @@
import etc.linux.memoryerror;
pragma(inline, false):
void f(ref ubyte[1024] buf)
{
ubyte[1024] cpy = buf;
g(cpy);
}
void g(ref ubyte[1024] buf)
{
ubyte[1024] cpy = buf;
f(cpy);
}
void main()
{
registerMemoryAssertHandler;
ubyte[1024] buf;
f(buf);
}