mirror of
https://github.com/dlang/dmd.git
synced 2025-04-26 21:21:48 +03:00
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:
parent
e49b67e969
commit
d115713410
8 changed files with 552 additions and 298 deletions
65
changelog/druntime.segfault-message.dd
Normal file
65
changelog/druntime.segfault-message.dd
Normal file
|
@ -0,0 +1,65 @@
|
|||
New segfault handler showing backtraces for null access / call stack overflow on linux
|
||||
|
||||
While buffer overflows are usually caught by array bounds checks, there are still other situations where a segmentation fault occurs in D programs:
|
||||
|
||||
- `null` pointer dereference
|
||||
- Corrupted or dangling pointer dereference in `@system` code
|
||||
- Call stack overflow (infinite recursion)
|
||||
|
||||
These result in an uninformative runtime error such as:
|
||||
|
||||
$(CONSOLE
|
||||
[1] 37856 segmentation fault (core dumped) ./app
|
||||
)
|
||||
|
||||
In order to find the cause of the error, the program needs to be run again in a debugger like gdb.
|
||||
|
||||
There is the `registerMemoryErrorHandler` function in `etc.linux.memoryerror`, which catches `SIGSEGV` signals and transforms them into a thrown `InvalidPointerError`, providing a better message.
|
||||
However, it doesn't work on call stack overflow, because it uses stack memory itself, so the segfault handler segfaults.
|
||||
It also relies on inline assembly, limiting it to the x86 architecture.
|
||||
|
||||
A new function `registerMemoryAssertHandler` has been introduced, which does handle stack overflow by setting up an [altstack](https://man7.org/linux/man-pages/man2/sigaltstack.2.html).
|
||||
It uses `assert(0)` instead of throwing an `Error` object, so the result corresponds to the chosen `-checkaction=[D|C|halt|context]` setting.
|
||||
|
||||
Example:
|
||||
|
||||
---
|
||||
void main()
|
||||
{
|
||||
version (linux)
|
||||
{
|
||||
import etc.linux.memoryerror;
|
||||
registerMemoryAssertHandler();
|
||||
}
|
||||
int* p = null;
|
||||
int* q = cast(int*) 0xDEADBEEF;
|
||||
|
||||
// int a = *p; // segmentation fault: null pointer read/write operation
|
||||
// int b = *q; // segmentation fault: invalid pointer read/write operation
|
||||
recurse(); // segmentation fault: call stack overflow
|
||||
}
|
||||
|
||||
void recurse()
|
||||
{
|
||||
recurse();
|
||||
}
|
||||
---
|
||||
|
||||
Output with `dmd -g -run app.d`:
|
||||
|
||||
$(CONSOLE
|
||||
core.exception.AssertError@src/etc/linux/memoryerror.d(82): segmentation fault: call stack overflow
|
||||
$(NDASH)$(NDASH)$(NDASH)$(NDASH)$(NDASH)$(NDASH)$(NDASH)$(NDASH)$(NDASH)$(NDASH)
|
||||
src/core/exception.d:587 onAssertErrorMsg [0x58e270d2802d]
|
||||
src/core/exception.d:803 _d_assert_msg [0x58e270d1fb64]
|
||||
src/etc/linux/memoryerror.d:82 _d_handleSignalAssert [0x58e270d1f48d]
|
||||
??:? [0x7004139e876f]
|
||||
./app.d:16 void scratch.recurse() [0x58e270d1d757]
|
||||
./app.d:18 void scratch.recurse() [0x58e270d1d75c]
|
||||
./app.d:18 void scratch.recurse() [0x58e270d1d75c]
|
||||
./app.d:18 void scratch.recurse() [0x58e270d1d75c]
|
||||
./app.d:18 void scratch.recurse() [0x58e270d1d75c]
|
||||
...
|
||||
...
|
||||
...
|
||||
)
|
|
@ -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,45 +10,95 @@
|
|||
|
||||
module etc.linux.memoryerror;
|
||||
|
||||
version (CRuntime_Glibc)
|
||||
version (linux)
|
||||
{
|
||||
version (DigitalMars)
|
||||
{
|
||||
version (CRuntime_Glibc)
|
||||
{
|
||||
version (X86)
|
||||
version = MemoryErrorSupported;
|
||||
version (X86_64)
|
||||
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)
|
||||
{
|
||||
import core.sys.posix.signal : SA_ONSTACK, sigaltstack, SIGSTKSZ, stack_t;
|
||||
}
|
||||
|
||||
@system:
|
||||
|
||||
// The first 64Kb are reserved for detecting null pointer dereferences.
|
||||
// TODO: this is a platform-specific assumption, can be made more robust
|
||||
private enum size_t MEMORY_RESERVED_FOR_NULL_DEREFERENCE = 4096 * 16;
|
||||
|
||||
version (MemoryErrorSupported)
|
||||
{
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
sigaction_t action;
|
||||
action.sa_sigaction = &handleSignal;
|
||||
action.sa_flags = SA_SIGINFO;
|
||||
|
||||
auto oldptr = &old_sigaction;
|
||||
auto oldptr = &oldSigactionMemoryError;
|
||||
|
||||
return !sigaction(SIGSEGV, &action, oldptr);
|
||||
}
|
||||
}
|
||||
|
||||
bool deregisterMemoryErrorHandler() nothrow
|
||||
{
|
||||
auto oldptr = &old_sigaction;
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
class InvalidPointerError : Error
|
||||
{
|
||||
this(string file = __FILE__, size_t line = __LINE__, Throwable next = null) nothrow
|
||||
{
|
||||
super("", file, line, next);
|
||||
|
@ -62,13 +108,13 @@ class InvalidPointerError : Error
|
|||
{
|
||||
super("", file, line, next);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Thrown on null pointer dereferences.
|
||||
*/
|
||||
class NullPointerError : InvalidPointerError
|
||||
{
|
||||
class NullPointerError : InvalidPointerError
|
||||
{
|
||||
this(string file = __FILE__, size_t line = __LINE__, Throwable next = null) nothrow
|
||||
{
|
||||
super(file, line, next);
|
||||
|
@ -78,10 +124,10 @@ class NullPointerError : InvalidPointerError
|
|||
{
|
||||
super(file, line, next);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unittest
|
||||
{
|
||||
unittest
|
||||
{
|
||||
int* getNull() { return null; }
|
||||
|
||||
assert(registerMemoryErrorHandler());
|
||||
|
@ -113,18 +159,18 @@ unittest
|
|||
assert(b);
|
||||
|
||||
assert(deregisterMemoryErrorHandler());
|
||||
}
|
||||
}
|
||||
|
||||
// Signal handler space.
|
||||
// Signal handler space.
|
||||
|
||||
private:
|
||||
private:
|
||||
|
||||
__gshared sigaction_t old_sigaction;
|
||||
__gshared sigaction_t oldSigactionMemoryError;
|
||||
|
||||
alias typeof(ucontext.ucontext_t.init.uc_mcontext.gregs[0]) RegType;
|
||||
alias RegType = typeof(ucontext.ucontext_t.init.uc_mcontext.gregs[0]);
|
||||
|
||||
version (X86_64)
|
||||
{
|
||||
version (X86_64)
|
||||
{
|
||||
static RegType savedRDI, savedRSI;
|
||||
|
||||
extern(C)
|
||||
|
@ -219,9 +265,9 @@ version (X86_64)
|
|||
{
|
||||
return savedRSI;
|
||||
}
|
||||
}
|
||||
else version (X86)
|
||||
{
|
||||
}
|
||||
else version (X86)
|
||||
{
|
||||
static RegType savedEAX, savedEDX;
|
||||
|
||||
extern(C)
|
||||
|
@ -253,7 +299,7 @@ else version (X86)
|
|||
// 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 [ESP + 8], EBP;
|
||||
mov EBP, ESP;
|
||||
|
||||
jmp sigsegvDataHandler;
|
||||
|
@ -299,22 +345,15 @@ else version (X86)
|
|||
|
||||
return restore;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
}
|
||||
else
|
||||
{
|
||||
static assert(false, "Unsupported architecture.");
|
||||
}
|
||||
}
|
||||
|
||||
// This should be calculated by druntime.
|
||||
// TODO: Add a core.memory function for this.
|
||||
enum PAGE_SIZE = 4096;
|
||||
|
||||
// The first 64Kb are reserved for detecting null pointer dereferences.
|
||||
enum MEMORY_RESERVED_FOR_NULL_DEREFERENCE = 4096 * 16;
|
||||
|
||||
// User space handler
|
||||
void sigsegvUserspaceProcess(void* address)
|
||||
{
|
||||
// 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)
|
||||
|
@ -323,4 +362,92 @@ void sigsegvUserspaceProcess(void* address)
|
|||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 $*
|
||||
|
|
9
druntime/test/exceptions/src/memoryerror_null_call.d
Normal file
9
druntime/test/exceptions/src/memoryerror_null_call.d
Normal file
|
@ -0,0 +1,9 @@
|
|||
import etc.linux.memoryerror;
|
||||
|
||||
void function() foo = null;
|
||||
|
||||
void main()
|
||||
{
|
||||
registerMemoryAssertHandler;
|
||||
foo();
|
||||
}
|
9
druntime/test/exceptions/src/memoryerror_null_read.d
Normal file
9
druntime/test/exceptions/src/memoryerror_null_read.d
Normal file
|
@ -0,0 +1,9 @@
|
|||
import etc.linux.memoryerror;
|
||||
|
||||
int* x = null;
|
||||
|
||||
void main()
|
||||
{
|
||||
registerMemoryAssertHandler;
|
||||
*x = 3;
|
||||
}
|
9
druntime/test/exceptions/src/memoryerror_null_write.d
Normal file
9
druntime/test/exceptions/src/memoryerror_null_write.d
Normal file
|
@ -0,0 +1,9 @@
|
|||
import etc.linux.memoryerror;
|
||||
|
||||
int* x = null;
|
||||
|
||||
int main()
|
||||
{
|
||||
registerMemoryAssertHandler;
|
||||
return *x;
|
||||
}
|
22
druntime/test/exceptions/src/memoryerror_stackoverflow.d
Normal file
22
druntime/test/exceptions/src/memoryerror_stackoverflow.d
Normal 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);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue