537 lines
18 KiB
C++
537 lines
18 KiB
C++
/*
|
|
* Copyright (C) 2008 The Android Open Source Project
|
|
* All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* * Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* * Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in
|
|
* the documentation and/or other materials provided with the
|
|
* distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
|
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
|
* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
|
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
|
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
|
|
* OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
|
|
* AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
|
|
* OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
|
* SUCH DAMAGE.
|
|
*/
|
|
|
|
#include "debuggerd/handler.h"
|
|
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <inttypes.h>
|
|
#include <linux/futex.h>
|
|
#include <pthread.h>
|
|
#include <sched.h>
|
|
#include <signal.h>
|
|
#include <stddef.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/capability.h>
|
|
#include <sys/mman.h>
|
|
#include <sys/prctl.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/syscall.h>
|
|
#include <sys/un.h>
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
|
|
#include <async_safe/log.h>
|
|
|
|
#include "dump_type.h"
|
|
|
|
// see man(2) prctl, specifically the section about PR_GET_NAME
|
|
#define MAX_TASK_NAME_LEN (16)
|
|
|
|
#if defined(__LP64__)
|
|
#define CRASH_DUMP_NAME "crash_dump64"
|
|
#else
|
|
#define CRASH_DUMP_NAME "crash_dump32"
|
|
#endif
|
|
|
|
#define CRASH_DUMP_PATH "/system/bin/" CRASH_DUMP_NAME
|
|
|
|
// Wrappers that directly invoke the respective syscalls, in case the cached values are invalid.
|
|
#pragma GCC poison getpid gettid
|
|
static pid_t __getpid() {
|
|
return syscall(__NR_getpid);
|
|
}
|
|
|
|
static pid_t __gettid() {
|
|
return syscall(__NR_gettid);
|
|
}
|
|
|
|
static inline void futex_wait(volatile void* ftx, int value) {
|
|
syscall(__NR_futex, ftx, FUTEX_WAIT, value, nullptr, nullptr, 0);
|
|
}
|
|
|
|
class ErrnoRestorer {
|
|
public:
|
|
ErrnoRestorer() : saved_errno_(errno) {
|
|
}
|
|
|
|
~ErrnoRestorer() {
|
|
errno = saved_errno_;
|
|
}
|
|
|
|
private:
|
|
int saved_errno_;
|
|
};
|
|
|
|
extern "C" void debuggerd_fallback_handler(siginfo_t*, ucontext_t*, void*);
|
|
|
|
static debuggerd_callbacks_t g_callbacks;
|
|
|
|
// Mutex to ensure only one crashing thread dumps itself.
|
|
static pthread_mutex_t crash_mutex = PTHREAD_MUTEX_INITIALIZER;
|
|
|
|
// Don't use async_safe_fatal because it exits via abort, which might put us back into
|
|
// a signal handler.
|
|
static void __noreturn __printflike(1, 2) fatal(const char* fmt, ...) {
|
|
va_list args;
|
|
va_start(args, fmt);
|
|
async_safe_format_log_va_list(ANDROID_LOG_FATAL, "libc", fmt, args);
|
|
_exit(1);
|
|
}
|
|
|
|
static void __noreturn __printflike(1, 2) fatal_errno(const char* fmt, ...) {
|
|
int err = errno;
|
|
va_list args;
|
|
va_start(args, fmt);
|
|
|
|
char buf[4096];
|
|
async_safe_format_buffer_va_list(buf, sizeof(buf), fmt, args);
|
|
fatal("%s: %s", buf, strerror(err));
|
|
}
|
|
|
|
static bool get_main_thread_name(char* buf, size_t len) {
|
|
int fd = open("/proc/self/comm", O_RDONLY | O_CLOEXEC);
|
|
if (fd == -1) {
|
|
return false;
|
|
}
|
|
|
|
ssize_t rc = read(fd, buf, len);
|
|
close(fd);
|
|
if (rc == -1) {
|
|
return false;
|
|
} else if (rc == 0) {
|
|
// Should never happen?
|
|
return false;
|
|
}
|
|
|
|
// There's a trailing newline, replace it with a NUL.
|
|
buf[rc - 1] = '\0';
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
* Writes a summary of the signal to the log file. We do this so that, if
|
|
* for some reason we're not able to contact debuggerd, there is still some
|
|
* indication of the failure in the log.
|
|
*
|
|
* We could be here as a result of native heap corruption, or while a
|
|
* mutex is being held, so we don't want to use any libc functions that
|
|
* could allocate memory or hold a lock.
|
|
*/
|
|
static void log_signal_summary(int signum, const siginfo_t* info) {
|
|
char thread_name[MAX_TASK_NAME_LEN + 1]; // one more for termination
|
|
if (prctl(PR_GET_NAME, reinterpret_cast<unsigned long>(thread_name), 0, 0, 0) != 0) {
|
|
strcpy(thread_name, "<name unknown>");
|
|
} else {
|
|
// short names are null terminated by prctl, but the man page
|
|
// implies that 16 byte names are not.
|
|
thread_name[MAX_TASK_NAME_LEN] = 0;
|
|
}
|
|
|
|
if (signum == DEBUGGER_SIGNAL) {
|
|
async_safe_format_log(ANDROID_LOG_INFO, "libc", "Requested dump for tid %d (%s)", __gettid(),
|
|
thread_name);
|
|
return;
|
|
}
|
|
|
|
const char* signal_name = "???";
|
|
bool has_address = false;
|
|
switch (signum) {
|
|
case SIGABRT:
|
|
signal_name = "SIGABRT";
|
|
break;
|
|
case SIGBUS:
|
|
signal_name = "SIGBUS";
|
|
has_address = true;
|
|
break;
|
|
case SIGFPE:
|
|
signal_name = "SIGFPE";
|
|
has_address = true;
|
|
break;
|
|
case SIGILL:
|
|
signal_name = "SIGILL";
|
|
has_address = true;
|
|
break;
|
|
case SIGSEGV:
|
|
signal_name = "SIGSEGV";
|
|
has_address = true;
|
|
break;
|
|
#if defined(SIGSTKFLT)
|
|
case SIGSTKFLT:
|
|
signal_name = "SIGSTKFLT";
|
|
break;
|
|
#endif
|
|
case SIGSYS:
|
|
signal_name = "SIGSYS";
|
|
break;
|
|
case SIGTRAP:
|
|
signal_name = "SIGTRAP";
|
|
break;
|
|
}
|
|
|
|
// "info" will be null if the siginfo_t information was not available.
|
|
// Many signals don't have an address or a code.
|
|
char code_desc[32]; // ", code -6"
|
|
char addr_desc[32]; // ", fault addr 0x1234"
|
|
addr_desc[0] = code_desc[0] = 0;
|
|
if (info != nullptr) {
|
|
async_safe_format_buffer(code_desc, sizeof(code_desc), ", code %d", info->si_code);
|
|
if (has_address) {
|
|
async_safe_format_buffer(addr_desc, sizeof(addr_desc), ", fault addr %p", info->si_addr);
|
|
}
|
|
}
|
|
|
|
char main_thread_name[MAX_TASK_NAME_LEN + 1];
|
|
if (!get_main_thread_name(main_thread_name, sizeof(main_thread_name))) {
|
|
strncpy(main_thread_name, "<unknown>", sizeof(main_thread_name));
|
|
}
|
|
|
|
async_safe_format_log(
|
|
ANDROID_LOG_FATAL, "libc", "Fatal signal %d (%s)%s%s in tid %d (%s), pid %d (%s)", signum,
|
|
signal_name, code_desc, addr_desc, __gettid(), thread_name, __getpid(), main_thread_name);
|
|
}
|
|
|
|
/*
|
|
* Returns true if the handler for signal "signum" has SA_SIGINFO set.
|
|
*/
|
|
static bool have_siginfo(int signum) {
|
|
struct sigaction old_action;
|
|
if (sigaction(signum, nullptr, &old_action) < 0) {
|
|
async_safe_format_log(ANDROID_LOG_WARN, "libc", "Failed testing for SA_SIGINFO: %s",
|
|
strerror(errno));
|
|
return false;
|
|
}
|
|
return (old_action.sa_flags & SA_SIGINFO) != 0;
|
|
}
|
|
|
|
static void raise_caps() {
|
|
// Raise CapInh to match CapPrm, so that we can set the ambient bits.
|
|
__user_cap_header_struct capheader;
|
|
memset(&capheader, 0, sizeof(capheader));
|
|
capheader.version = _LINUX_CAPABILITY_VERSION_3;
|
|
capheader.pid = 0;
|
|
|
|
__user_cap_data_struct capdata[2];
|
|
if (capget(&capheader, &capdata[0]) == -1) {
|
|
fatal_errno("capget failed");
|
|
}
|
|
|
|
if (capdata[0].permitted != capdata[0].inheritable ||
|
|
capdata[1].permitted != capdata[1].inheritable) {
|
|
capdata[0].inheritable = capdata[0].permitted;
|
|
capdata[1].inheritable = capdata[1].permitted;
|
|
|
|
if (capset(&capheader, &capdata[0]) == -1) {
|
|
async_safe_format_log(ANDROID_LOG_ERROR, "libc", "capset failed: %s", strerror(errno));
|
|
}
|
|
}
|
|
|
|
// Set the ambient capability bits so that crash_dump gets all of our caps and can ptrace us.
|
|
uint64_t capmask = capdata[0].inheritable;
|
|
capmask |= static_cast<uint64_t>(capdata[1].inheritable) << 32;
|
|
for (unsigned long i = 0; i < 64; ++i) {
|
|
if (capmask & (1ULL << i)) {
|
|
if (prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, i, 0, 0) != 0) {
|
|
async_safe_format_log(ANDROID_LOG_ERROR, "libc",
|
|
"failed to raise ambient capability %lu: %s", i, strerror(errno));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct debugger_thread_info {
|
|
bool crash_dump_started;
|
|
pid_t crashing_tid;
|
|
pid_t pseudothread_tid;
|
|
int signal_number;
|
|
siginfo_t* info;
|
|
};
|
|
|
|
// Logging and contacting debuggerd requires free file descriptors, which we might not have.
|
|
// Work around this by spawning a "thread" that shares its parent's address space, but not its file
|
|
// descriptor table, so that we can close random file descriptors without affecting the original
|
|
// process. Note that this doesn't go through pthread_create, so TLS is shared with the spawning
|
|
// process.
|
|
static void* pseudothread_stack;
|
|
|
|
static DebuggerdDumpType get_dump_type(const debugger_thread_info* thread_info) {
|
|
if (thread_info->signal_number == DEBUGGER_SIGNAL && thread_info->info->si_value.sival_int) {
|
|
return kDebuggerdNativeBacktrace;
|
|
}
|
|
|
|
return kDebuggerdTombstone;
|
|
}
|
|
|
|
static int debuggerd_dispatch_pseudothread(void* arg) {
|
|
debugger_thread_info* thread_info = static_cast<debugger_thread_info*>(arg);
|
|
|
|
for (int i = 0; i < 1024; ++i) {
|
|
close(i);
|
|
}
|
|
|
|
int devnull = TEMP_FAILURE_RETRY(open("/dev/null", O_RDWR));
|
|
|
|
// devnull will be 0.
|
|
TEMP_FAILURE_RETRY(dup2(devnull, STDOUT_FILENO));
|
|
TEMP_FAILURE_RETRY(dup2(devnull, STDERR_FILENO));
|
|
|
|
int pipefds[2];
|
|
if (pipe(pipefds) != 0) {
|
|
fatal_errno("failed to create pipe");
|
|
}
|
|
|
|
// Don't use fork(2) to avoid calling pthread_atfork handlers.
|
|
int forkpid = clone(nullptr, nullptr, 0, nullptr);
|
|
if (forkpid == -1) {
|
|
async_safe_format_log(ANDROID_LOG_FATAL, "libc",
|
|
"failed to fork in debuggerd signal handler: %s", strerror(errno));
|
|
} else if (forkpid == 0) {
|
|
TEMP_FAILURE_RETRY(dup2(pipefds[1], STDOUT_FILENO));
|
|
close(pipefds[0]);
|
|
close(pipefds[1]);
|
|
|
|
raise_caps();
|
|
|
|
char main_tid[10];
|
|
char pseudothread_tid[10];
|
|
char debuggerd_dump_type[10];
|
|
async_safe_format_buffer(main_tid, sizeof(main_tid), "%d", thread_info->crashing_tid);
|
|
async_safe_format_buffer(pseudothread_tid, sizeof(pseudothread_tid), "%d",
|
|
thread_info->pseudothread_tid);
|
|
async_safe_format_buffer(debuggerd_dump_type, sizeof(debuggerd_dump_type), "%d",
|
|
get_dump_type(thread_info));
|
|
|
|
execl(CRASH_DUMP_PATH, CRASH_DUMP_NAME, main_tid, pseudothread_tid, debuggerd_dump_type,
|
|
nullptr);
|
|
|
|
fatal_errno("exec failed");
|
|
} else {
|
|
close(pipefds[1]);
|
|
char buf[4];
|
|
ssize_t rc = TEMP_FAILURE_RETRY(read(pipefds[0], &buf, sizeof(buf)));
|
|
if (rc == -1) {
|
|
async_safe_format_log(ANDROID_LOG_FATAL, "libc", "read of IPC pipe failed: %s",
|
|
strerror(errno));
|
|
} else if (rc == 0) {
|
|
async_safe_format_log(ANDROID_LOG_FATAL, "libc", "crash_dump helper failed to exec");
|
|
} else if (rc != 1) {
|
|
async_safe_format_log(ANDROID_LOG_FATAL, "libc",
|
|
"read of IPC pipe returned unexpected value: %zd", rc);
|
|
} else {
|
|
if (buf[0] != '\1') {
|
|
async_safe_format_log(ANDROID_LOG_FATAL, "libc", "crash_dump helper reported failure");
|
|
} else {
|
|
thread_info->crash_dump_started = true;
|
|
}
|
|
}
|
|
close(pipefds[0]);
|
|
|
|
// Don't leave a zombie child.
|
|
int status;
|
|
if (TEMP_FAILURE_RETRY(waitpid(forkpid, &status, 0)) == -1) {
|
|
async_safe_format_log(ANDROID_LOG_FATAL, "libc", "failed to wait for crash_dump helper: %s",
|
|
strerror(errno));
|
|
} else if (WIFSTOPPED(status) || WIFSIGNALED(status)) {
|
|
async_safe_format_log(ANDROID_LOG_FATAL, "libc", "crash_dump helper crashed or stopped");
|
|
thread_info->crash_dump_started = false;
|
|
}
|
|
}
|
|
|
|
syscall(__NR_exit, 0);
|
|
return 0;
|
|
}
|
|
|
|
static void resend_signal(siginfo_t* info, bool crash_dump_started) {
|
|
// Signals can either be fatal or nonfatal.
|
|
// For fatal signals, crash_dump will send us the signal we crashed with
|
|
// before resuming us, so that processes using waitpid on us will see that we
|
|
// exited with the correct exit status (e.g. so that sh will report
|
|
// "Segmentation fault" instead of "Killed"). For this to work, we need
|
|
// to deregister our signal handler for that signal before continuing.
|
|
if (info->si_signo != DEBUGGER_SIGNAL) {
|
|
signal(info->si_signo, SIG_DFL);
|
|
}
|
|
|
|
// We need to return from our signal handler so that crash_dump can see the
|
|
// signal via ptrace and dump the thread that crashed. However, returning
|
|
// does not guarantee that the signal will be thrown again, even for SIGSEGV
|
|
// and friends, since the signal could have been sent manually. We blocked
|
|
// all signals when registering the handler, so resending the signal (using
|
|
// rt_tgsigqueueinfo(2) to preserve SA_SIGINFO) will cause it to be delivered
|
|
// when our signal handler returns.
|
|
if (crash_dump_started || info->si_signo != DEBUGGER_SIGNAL) {
|
|
int rc = syscall(SYS_rt_tgsigqueueinfo, __getpid(), __gettid(), info->si_signo, info);
|
|
if (rc != 0) {
|
|
fatal_errno("failed to resend signal during crash");
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handler that does crash dumping by forking and doing the processing in the child.
|
|
// Do this by ptracing the relevant thread, and then execing debuggerd to do the actual dump.
|
|
static void debuggerd_signal_handler(int signal_number, siginfo_t* info, void* context) {
|
|
// Make sure we don't change the value of errno, in case a signal comes in between the process
|
|
// making a syscall and checking errno.
|
|
ErrnoRestorer restorer;
|
|
|
|
// It's possible somebody cleared the SA_SIGINFO flag, which would mean
|
|
// our "info" arg holds an undefined value.
|
|
if (!have_siginfo(signal_number)) {
|
|
info = nullptr;
|
|
}
|
|
|
|
struct siginfo si = {};
|
|
if (!info) {
|
|
memset(&si, 0, sizeof(si));
|
|
si.si_signo = signal_number;
|
|
si.si_code = SI_USER;
|
|
si.si_pid = __getpid();
|
|
si.si_uid = getuid();
|
|
info = &si;
|
|
} else if (info->si_code >= 0 || info->si_code == SI_TKILL) {
|
|
// rt_tgsigqueueinfo(2)'s documentation appears to be incorrect on kernels
|
|
// that contain commit 66dd34a (3.9+). The manpage claims to only allow
|
|
// negative si_code values that are not SI_TKILL, but 66dd34a changed the
|
|
// check to allow all si_code values in calls coming from inside the house.
|
|
}
|
|
|
|
void* abort_message = nullptr;
|
|
if (g_callbacks.get_abort_message) {
|
|
abort_message = g_callbacks.get_abort_message();
|
|
}
|
|
|
|
if (prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0) == 1) {
|
|
// This check might be racy if another thread sets NO_NEW_PRIVS, but this should be unlikely,
|
|
// you can only set NO_NEW_PRIVS to 1, and the effect should be at worst a single missing
|
|
// ANR trace.
|
|
debuggerd_fallback_handler(info, static_cast<ucontext_t*>(context), abort_message);
|
|
resend_signal(info, false);
|
|
return;
|
|
}
|
|
|
|
// Only allow one thread to handle a signal at a time.
|
|
int ret = pthread_mutex_lock(&crash_mutex);
|
|
if (ret != 0) {
|
|
async_safe_format_log(ANDROID_LOG_INFO, "libc", "pthread_mutex_lock failed: %s", strerror(ret));
|
|
return;
|
|
}
|
|
|
|
log_signal_summary(signal_number, info);
|
|
|
|
// If this was a fatal crash, populate si_value with the abort message address if possible.
|
|
// Note that applications can set an abort message without aborting.
|
|
if (abort_message && signal_number != DEBUGGER_SIGNAL) {
|
|
info->si_value.sival_ptr = abort_message;
|
|
}
|
|
|
|
debugger_thread_info thread_info = {
|
|
.crash_dump_started = false,
|
|
.pseudothread_tid = -1,
|
|
.crashing_tid = __gettid(),
|
|
.signal_number = signal_number,
|
|
.info = info
|
|
};
|
|
|
|
// Set PR_SET_DUMPABLE to 1, so that crash_dump can ptrace us.
|
|
int orig_dumpable = prctl(PR_GET_DUMPABLE);
|
|
if (prctl(PR_SET_DUMPABLE, 1) != 0) {
|
|
fatal_errno("failed to set dumpable");
|
|
}
|
|
|
|
// Essentially pthread_create without CLONE_FILES (see debuggerd_dispatch_pseudothread).
|
|
pid_t child_pid =
|
|
clone(debuggerd_dispatch_pseudothread, pseudothread_stack,
|
|
CLONE_THREAD | CLONE_SIGHAND | CLONE_VM | CLONE_CHILD_SETTID | CLONE_CHILD_CLEARTID,
|
|
&thread_info, nullptr, nullptr, &thread_info.pseudothread_tid);
|
|
if (child_pid == -1) {
|
|
fatal_errno("failed to spawn debuggerd dispatch thread");
|
|
}
|
|
|
|
// Wait for the child to start...
|
|
futex_wait(&thread_info.pseudothread_tid, -1);
|
|
|
|
// and then wait for it to finish.
|
|
futex_wait(&thread_info.pseudothread_tid, child_pid);
|
|
|
|
// Restore PR_SET_DUMPABLE to its original value.
|
|
if (prctl(PR_SET_DUMPABLE, orig_dumpable) != 0) {
|
|
fatal_errno("failed to restore dumpable");
|
|
}
|
|
|
|
// Signals can either be fatal or nonfatal.
|
|
// For fatal signals, crash_dump will PTRACE_CONT us with the signal we
|
|
// crashed with, so that processes using waitpid on us will see that we
|
|
// exited with the correct exit status (e.g. so that sh will report
|
|
// "Segmentation fault" instead of "Killed"). For this to work, we need
|
|
// to deregister our signal handler for that signal before continuing.
|
|
if (signal_number != DEBUGGER_SIGNAL) {
|
|
signal(signal_number, SIG_DFL);
|
|
}
|
|
|
|
resend_signal(info, thread_info.crash_dump_started);
|
|
if (info->si_signo == DEBUGGER_SIGNAL) {
|
|
// If the signal is fatal, don't unlock the mutex to prevent other crashing threads from
|
|
// starting to dump right before our death.
|
|
pthread_mutex_unlock(&crash_mutex);
|
|
}
|
|
}
|
|
|
|
void debuggerd_init(debuggerd_callbacks_t* callbacks) {
|
|
if (callbacks) {
|
|
g_callbacks = *callbacks;
|
|
}
|
|
|
|
void* thread_stack_allocation =
|
|
mmap(nullptr, PAGE_SIZE * 3, PROT_NONE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
|
|
if (thread_stack_allocation == MAP_FAILED) {
|
|
fatal_errno("failed to allocate debuggerd thread stack");
|
|
}
|
|
|
|
char* stack = static_cast<char*>(thread_stack_allocation) + PAGE_SIZE;
|
|
if (mprotect(stack, PAGE_SIZE, PROT_READ | PROT_WRITE) != 0) {
|
|
fatal_errno("failed to mprotect debuggerd thread stack");
|
|
}
|
|
|
|
// Stack grows negatively, set it to the last byte in the page...
|
|
stack = (stack + PAGE_SIZE - 1);
|
|
// and align it.
|
|
stack -= 15;
|
|
pseudothread_stack = stack;
|
|
|
|
struct sigaction action;
|
|
memset(&action, 0, sizeof(action));
|
|
sigfillset(&action.sa_mask);
|
|
action.sa_sigaction = debuggerd_signal_handler;
|
|
action.sa_flags = SA_RESTART | SA_SIGINFO;
|
|
|
|
// Use the alternate signal stack if available so we can catch stack overflows.
|
|
action.sa_flags |= SA_ONSTACK;
|
|
debuggerd_register_handlers(&action);
|
|
}
|