qt6-bb10/src/testlib/qtestcrashhandler.cpp

670 lines
20 KiB
C++

// Copyright (C) 2024 The Qt Company Ltd.
// Copyright (C) 2024 Intel Corporation.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#include <QtTest/qtestcase.h>
#include <QtTest/private/qtestcrashhandler_p.h>
#include <QtTest/qtestassert.h>
#include <QtCore/qbytearray.h>
#include <QtCore/qcoreapplication.h>
#include <QtCore/qdebug.h>
#include <QtCore/qdir.h>
#include <QtCore/qdiriterator.h>
#include <QtCore/qfile.h>
#include <QtCore/qfileinfo.h>
#include <QtCore/qfloat16.h>
#include <QtCore/qlibraryinfo.h>
#include <QtCore/qlist.h>
#include <QtCore/qmetaobject.h>
#include <QtCore/qobject.h>
#include <QtCore/qstringlist.h>
#include <QtCore/qtemporarydir.h>
#include <QtCore/qthread.h>
#include <QtCore/qvarlengtharray.h>
#include <QtCore/private/qlocking_p.h>
#include <QtCore/private/qtools_p.h>
#include <QtCore/private/qwaitcondition_p.h>
#include <QtCore/qtestsupport_core.h>
#include <QtTest/private/qtestlog_p.h>
#include <QtTest/private/qtesttable_p.h>
#include <QtTest/qtestdata.h>
#include <QtTest/private/qtestresult_p.h>
#include <QtTest/private/qsignaldumper_p.h>
#include <QtTest/private/qbenchmark_p.h>
#if QT_CONFIG(batch_test_support)
#include <QtTest/private/qtestregistry_p.h>
#endif // QT_CONFIG(batch_test_support)
#include <QtTest/private/cycle_p.h>
#include <QtTest/private/qtestblacklist_p.h>
#if defined(HAVE_XCTEST)
#include <QtTest/private/qxctestlogger_p.h>
#endif
#if defined Q_OS_MACOS
#include <QtTest/private/qtestutil_macos_p.h>
#endif
#if defined(Q_OS_DARWIN)
#include <QtTest/private/qappletestlogger_p.h>
#endif
#if !defined(Q_OS_INTEGRITY) || __GHS_VERSION_NUMBER > 202014
# include <charconv>
#else
// Broken implementation, causes link failures just by #include'ing!
# undef __cpp_lib_to_chars // in case <version> was included
#endif
#include <stdio.h>
#include <stdlib.h>
#if defined(Q_OS_LINUX)
#include <sys/prctl.h>
#include <sys/types.h>
#include <fcntl.h>
#endif
#ifdef Q_OS_UNIX
#include <QtCore/private/qcore_unix_p.h>
#include <errno.h>
#if __has_include(<paths.h>)
# include <paths.h>
#endif
#include <signal.h>
#include <time.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <unistd.h>
# if !defined(Q_OS_INTEGRITY)
# include <sys/resource.h>
# endif
# ifndef _PATH_DEFPATH
# define _PATH_DEFPATH "/usr/bin:/bin"
# endif
# ifndef SIGSTKSZ
# define SIGSTKSZ 0 /* we have code to set the minimum */
# endif
# ifndef SA_RESETHAND
# define SA_RESETHAND 0
# endif
#endif
#if defined(Q_OS_MACOS)
#include <IOKit/pwr_mgt/IOPMLib.h>
#include <mach/task.h>
#include <mach/mach_init.h>
#include <CoreFoundation/CFPreferences.h>
#endif
#if defined(Q_OS_WASM)
#include <emscripten.h>
#endif
QT_BEGIN_NAMESPACE
using namespace Qt::StringLiterals;
namespace QTest {
namespace CrashHandler {
#if defined(Q_OS_UNIX) && (!defined(Q_OS_WASM) || QT_CONFIG(thread))
struct iovec IoVec(struct iovec vec)
{
return vec;
}
struct iovec IoVec(const char *str)
{
struct iovec r = {};
r.iov_base = const_cast<char *>(str);
r.iov_len = strlen(str);
return r;
}
struct iovec asyncSafeToString(int n, AsyncSafeIntBuffer &&result)
{
char *ptr = result.array.data();
if (false) {
#ifdef __cpp_lib_to_chars
} else if (auto r = std::to_chars(ptr, ptr + result.array.size(), n, 10); r.ec == std::errc{}) {
ptr = r.ptr;
#endif
} else {
// handle the sign
if (n < 0) {
*ptr++ = '-';
n = -n;
}
// find the highest power of the base that is less than this number
static constexpr int StartingDivider = ([]() {
int divider = 1;
for (int i = 0; i < std::numeric_limits<int>::digits10; ++i)
divider *= 10;
return divider;
}());
int divider = StartingDivider;
while (divider && n < divider)
divider /= 10;
// now convert to string
while (divider > 1) {
int quot = n / divider;
n = n % divider;
divider /= 10;
*ptr++ = quot + '0';
}
*ptr++ = n + '0';
}
#ifndef QT_NO_DEBUG
// this isn't necessary, it just helps in the debugger
*ptr = '\0';
#endif
struct iovec r;
r.iov_base = result.array.data();
r.iov_len = ptr - result.array.data();
return r;
};
#endif // defined(Q_OS_UNIX) && (!defined(Q_OS_WASM) || QT_CONFIG(thread))
bool alreadyDebugging()
{
#if defined(Q_OS_LINUX)
int fd = open("/proc/self/status", O_RDONLY);
if (fd == -1)
return false;
char buffer[2048];
ssize_t size = read(fd, buffer, sizeof(buffer) - 1);
if (size == -1) {
close(fd);
return false;
}
buffer[size] = 0;
const char tracerPidToken[] = "\nTracerPid:";
char *tracerPid = strstr(buffer, tracerPidToken);
if (!tracerPid) {
close(fd);
return false;
}
tracerPid += sizeof(tracerPidToken);
long int pid = strtol(tracerPid, &tracerPid, 10);
close(fd);
return pid != 0;
#elif defined(Q_OS_WIN)
return IsDebuggerPresent();
#elif defined(Q_OS_MACOS)
// Check if there is an exception handler for the process:
mach_msg_type_number_t portCount = 0;
exception_mask_t masks[EXC_TYPES_COUNT];
mach_port_t ports[EXC_TYPES_COUNT];
exception_behavior_t behaviors[EXC_TYPES_COUNT];
thread_state_flavor_t flavors[EXC_TYPES_COUNT];
exception_mask_t mask = EXC_MASK_ALL & ~(EXC_MASK_RESOURCE | EXC_MASK_GUARD);
kern_return_t result = task_get_exception_ports(mach_task_self(), mask, masks, &portCount,
ports, behaviors, flavors);
if (result == KERN_SUCCESS) {
for (mach_msg_type_number_t portIndex = 0; portIndex < portCount; ++portIndex) {
if (MACH_PORT_VALID(ports[portIndex])) {
return true;
}
}
}
return false;
#else
// TODO
return false;
#endif
}
namespace {
enum DebuggerProgram { None, Gdb, Lldb };
static bool hasSystemCrashReporter()
{
#if defined(Q_OS_MACOS)
return QTestPrivate::macCrashReporterWillShowDialog();
#else
return false;
#endif
}
} // unnamed namespaced
void maybeDisableCoreDump()
{
#ifdef RLIMIT_CORE
bool ok = false;
const int disableCoreDump = qEnvironmentVariableIntValue("QTEST_DISABLE_CORE_DUMP", &ok);
if (ok && disableCoreDump) {
struct rlimit limit;
limit.rlim_cur = 0;
limit.rlim_max = 0;
if (setrlimit(RLIMIT_CORE, &limit) != 0)
qWarning("Failed to disable core dumps: %d", errno);
}
#endif
}
static DebuggerProgram debugger = None;
void prepareStackTrace()
{
bool ok = false;
const int disableStackDump = qEnvironmentVariableIntValue("QTEST_DISABLE_STACK_DUMP", &ok);
if (ok && disableStackDump)
return;
if (hasSystemCrashReporter())
return;
#if defined(Q_OS_MACOS)
#define CSR_ALLOW_UNRESTRICTED_FS (1 << 1)
std::optional<uint32_t> sipConfiguration = qt_mac_sipConfiguration();
if (!sipConfiguration || !(*sipConfiguration & CSR_ALLOW_UNRESTRICTED_FS))
return; // LLDB will fail to provide a valid stack trace
#endif
#ifdef Q_OS_UNIX
// like QStandardPaths::findExecutable(), but simpler
auto hasExecutable = [](const char *execname) {
std::string candidate;
std::string path;
if (const char *p = getenv("PATH"); p && *p)
path = p;
else
path = _PATH_DEFPATH;
for (const char *p = std::strtok(&path[0], ":'"); p; p = std::strtok(nullptr, ":")) {
candidate = p;
candidate += '/';
candidate += execname;
if (QT_ACCESS(candidate.data(), X_OK) == 0)
return true;
}
return false;
};
static constexpr DebuggerProgram debuggerSearchOrder[] = {
# if defined(Q_OS_QNX) || (defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID))
Gdb, Lldb
# else
Lldb, Gdb
# endif
};
for (DebuggerProgram candidate : debuggerSearchOrder) {
switch (candidate) {
case None:
Q_UNREACHABLE();
break;
case Gdb:
if (hasExecutable("gdb")) {
debugger = Gdb;
return;
}
break;
case Lldb:
if (hasExecutable("lldb")) {
debugger = Lldb;
return;
}
break;
}
}
#endif // Q_OS_UNIX
}
#if !defined(Q_OS_WASM) || QT_CONFIG(thread)
void printTestRunTime()
{
const int msecsFunctionTime = qRound(QTestLog::msecsFunctionTime());
const int msecsTotalTime = qRound(QTestLog::msecsTotalTime());
const char *const name = QTest::currentTestFunction();
writeToStderr("\n ", name ? name : "[Non-test]",
" function time: ", asyncSafeToString(msecsFunctionTime),
"ms, total time: ", asyncSafeToString(msecsTotalTime), "ms\n");
}
void generateStackTrace()
{
if (debugger == None || alreadyDebugging())
return;
# if defined(Q_OS_LINUX) && defined(PR_SET_PTRACER)
// allow ourselves to be debugged
(void) prctl(PR_SET_PTRACER, PR_SET_PTRACER_ANY);
# endif
# if defined(Q_OS_UNIX) && !defined(Q_OS_WASM) && !defined(Q_OS_INTEGRITY) && !defined(Q_OS_VXWORKS)
writeToStderr("\n=== Stack trace ===\n");
// execlp() requires null-termination, so call the default constructor
AsyncSafeIntBuffer pidbuffer;
asyncSafeToString(getpid(), std::move(pidbuffer));
// Note: POSIX.1-2001 still has fork() in the list of async-safe functions,
// but in a future edition, it might be removed. It would be safer to wake
// up a babysitter thread to launch the debugger.
pid_t pid = fork();
if (pid == 0) {
// child process
(void) dup2(STDERR_FILENO, STDOUT_FILENO); // redirect stdout to stderr
switch (debugger) {
case None:
Q_UNREACHABLE();
break;
case Gdb:
execlp("gdb", "gdb", "--nx", "--batch", "-ex", "thread apply all bt",
"--pid", pidbuffer.array.data(), nullptr);
break;
case Lldb:
execlp("lldb", "lldb", "--no-lldbinit", "--batch", "-o", "bt all",
"--attach-pid", pidbuffer.array.data(), nullptr);
break;
}
_exit(1);
} else if (pid < 0) {
writeToStderr("Failed to start debugger.\n");
} else {
int ret;
QT_EINTR_LOOP(ret, waitpid(pid, nullptr, 0));
}
writeToStderr("=== End of stack trace ===\n");
# endif // Q_OS_UNIX && !Q_OS_WASM && !Q_OS_INTEGRITY && !Q_OS_VXWORKS
}
#endif // !defined(Q_OS_WASM) || QT_CONFIG(thread)
#if defined(Q_OS_WIN)
void blockUnixSignals()
{
// Windows does have C signals, but doesn't use them for the purposes we're
// talking about here
}
#elif defined(Q_OS_UNIX) && !defined(Q_OS_WASM)
void blockUnixSignals()
{
// Block most Unix signals so the WatchDog thread won't be called when
// external signals are delivered, thus avoiding interfering with the test
sigset_t set;
sigfillset(&set);
// we allow the crashing signals, in case we have bugs
for (int signo : FatalSignalHandler::fatalSignals)
sigdelset(&set, signo);
pthread_sigmask(SIG_BLOCK, &set, nullptr);
}
#endif // Q_OS_* choice
#if defined(Q_OS_WIN)
void DebugSymbolResolver::cleanup()
{
if (m_dbgHelpLib)
FreeLibrary(m_dbgHelpLib);
m_dbgHelpLib = 0;
m_symFromAddr = nullptr;
}
DebugSymbolResolver::DebugSymbolResolver(HANDLE process)
: m_process(process), m_dbgHelpLib(0), m_symFromAddr(nullptr)
{
bool success = false;
m_dbgHelpLib = LoadLibraryW(L"dbghelp.dll");
if (m_dbgHelpLib) {
SymInitializeType symInitialize = reinterpret_cast<SymInitializeType>(
reinterpret_cast<QFunctionPointer>(GetProcAddress(m_dbgHelpLib, "SymInitialize")));
m_symFromAddr = reinterpret_cast<SymFromAddrType>(
reinterpret_cast<QFunctionPointer>(GetProcAddress(m_dbgHelpLib, "SymFromAddr")));
success = symInitialize && m_symFromAddr && symInitialize(process, NULL, TRUE);
}
if (!success)
cleanup();
}
DebugSymbolResolver::Symbol DebugSymbolResolver::resolveSymbol(DWORD64 address) const
{
// reserve additional buffer where SymFromAddr() will store the name
struct NamedSymbolInfo : public DBGHELP_SYMBOL_INFO {
enum { symbolNameLength = 255 };
char name[symbolNameLength + 1];
};
Symbol result;
if (!isValid())
return result;
NamedSymbolInfo symbolBuffer;
memset(&symbolBuffer, 0, sizeof(NamedSymbolInfo));
symbolBuffer.MaxNameLen = NamedSymbolInfo::symbolNameLength;
symbolBuffer.SizeOfStruct = sizeof(DBGHELP_SYMBOL_INFO);
if (!m_symFromAddr(m_process, address, 0, &symbolBuffer))
return result;
result.name = qstrdup(symbolBuffer.Name);
result.address = symbolBuffer.Address;
return result;
}
WindowsFaultHandler::WindowsFaultHandler()
{
# if !defined(Q_CC_MINGW)
_CrtSetReportMode(_CRT_ERROR, _CRTDBG_MODE_DEBUG);
# endif
SetErrorMode(SetErrorMode(0) | SEM_NOGPFAULTERRORBOX);
SetUnhandledExceptionFilter(windowsFaultHandler);
}
LONG WINAPI WindowsFaultHandler::windowsFaultHandler(struct _EXCEPTION_POINTERS *exInfo)
{
enum { maxStackFrames = 100 };
char appName[MAX_PATH];
if (!GetModuleFileNameA(NULL, appName, MAX_PATH))
appName[0] = 0;
const int msecsFunctionTime = qRound(QTestLog::msecsFunctionTime());
const int msecsTotalTime = qRound(QTestLog::msecsTotalTime());
const void *exceptionAddress = exInfo->ExceptionRecord->ExceptionAddress;
fprintf(stderr, "A crash occurred in %s.\n", appName);
if (const char *name = QTest::currentTestFunction())
fprintf(stderr, "While testing %s\n", name);
fprintf(stderr, "Function time: %dms Total time: %dms\n\n"
"Exception address: 0x%p\n"
"Exception code : 0x%lx\n",
msecsFunctionTime, msecsTotalTime, exceptionAddress,
exInfo->ExceptionRecord->ExceptionCode);
DebugSymbolResolver resolver(GetCurrentProcess());
if (resolver.isValid()) {
DebugSymbolResolver::Symbol exceptionSymbol = resolver.resolveSymbol(DWORD64(exceptionAddress));
if (exceptionSymbol.name) {
fprintf(stderr, "Nearby symbol : %s\n", exceptionSymbol.name);
delete [] exceptionSymbol.name;
}
void *stack[maxStackFrames];
fputs("\nStack:\n", stderr);
const unsigned frameCount = CaptureStackBackTrace(0, DWORD(maxStackFrames), stack, NULL);
for (unsigned f = 0; f < frameCount; ++f) {
DebugSymbolResolver::Symbol symbol = resolver.resolveSymbol(DWORD64(stack[f]));
if (symbol.name) {
fprintf(stderr, "#%3u: %s() - 0x%p\n", f + 1, symbol.name, (const void *)symbol.address);
delete [] symbol.name;
} else {
fprintf(stderr, "#%3u: Unable to obtain symbol\n", f + 1);
}
}
}
fputc('\n', stderr);
return EXCEPTION_EXECUTE_HANDLER;
}
#elif defined(Q_OS_UNIX) && !defined(Q_OS_WASM)
bool FatalSignalHandler::pauseOnCrash = false;
FatalSignalHandler::FatalSignalHandler()
{
pauseOnCrash = qEnvironmentVariableIsSet("QTEST_PAUSE_ON_CRASH");
struct sigaction act;
memset(&act, 0, sizeof(act));
act.sa_handler = SIG_DFL;
oldActions().fill(act);
// Remove the handler after it is invoked.
act.sa_flags = SA_RESETHAND | setupAlternateStack();
# ifdef SA_SIGINFO
act.sa_flags |= SA_SIGINFO;
act.sa_sigaction = FatalSignalHandler::actionHandler;
# else
act.sa_handler = FatalSignalHandler::regularHandler;
# endif
// Block all fatal signals in our signal handler so we don't try to close
// the testlog twice.
sigemptyset(&act.sa_mask);
for (int signal : fatalSignals)
sigaddset(&act.sa_mask, signal);
for (size_t i = 0; i < fatalSignals.size(); ++i)
sigaction(fatalSignals[i], &act, &oldActions()[i]);
}
FatalSignalHandler::~FatalSignalHandler()
{
// Restore the default signal handlers in place of ours.
// If ours has been replaced, leave the replacement alone.
auto isOurs = [](const struct sigaction &old) {
# ifdef SA_SIGINFO
return (old.sa_flags & SA_SIGINFO) && old.sa_sigaction == FatalSignalHandler::actionHandler;
# else
return old.sa_handler == FatalSignalHandler::regularHandler;
# endif
};
struct sigaction action;
for (size_t i = 0; i < fatalSignals.size(); ++i) {
struct sigaction &act = oldActions()[i];
if (act.sa_flags == 0 && act.sa_handler == SIG_DFL)
continue; // Already the default
if (sigaction(fatalSignals[i], nullptr, &action))
continue; // Failed to query present handler
if (isOurs(action))
sigaction(fatalSignals[i], &act, nullptr);
}
freeAlternateStack();
}
FatalSignalHandler::OldActionsArray &FatalSignalHandler::oldActions()
{
Q_CONSTINIT static OldActionsArray oldActions {};
return oldActions;
}
auto FatalSignalHandler::alternateStackSize()
{
struct R { size_t size, pageSize; };
static constexpr size_t MinStackSize = 32 * 1024;
size_t pageSize = sysconf(_SC_PAGESIZE);
size_t size = SIGSTKSZ;
if (size < MinStackSize) {
size = MinStackSize;
} else {
// round up to a page
size = (size + pageSize - 1) & -pageSize;
}
return R{ size + pageSize, pageSize };
}
int FatalSignalHandler::setupAlternateStack()
{
// tvOS/watchOS both define SA_ONSTACK (in sys/signal.h) but mark sigaltstack() as
// unavailable (__WATCHOS_PROHIBITED __TVOS_PROHIBITED in signal.h)
# if defined(SA_ONSTACK) && !defined(Q_OS_TVOS) && !defined(Q_OS_WATCHOS)
// Let the signal handlers use an alternate stack
// This is necessary if SIGSEGV is to catch a stack overflow
auto r = alternateStackSize();
int flags = MAP_PRIVATE | MAP_ANONYMOUS;
# ifdef MAP_STACK
flags |= MAP_STACK;
# endif
alternateStackBase = mmap(nullptr, r.size, PROT_READ | PROT_WRITE, flags, -1, 0);
if (alternateStackBase == MAP_FAILED)
return 0;
// mark the bottom page inaccessible, to catch a handler stack overflow
(void) mprotect(alternateStackBase, r.pageSize, PROT_NONE);
stack_t stack;
stack.ss_flags = 0;
stack.ss_size = r.size - r.pageSize;
stack.ss_sp = static_cast<char *>(alternateStackBase) + r.pageSize;
sigaltstack(&stack, nullptr);
return SA_ONSTACK;
# else
return 0;
# endif
}
void FatalSignalHandler::freeAlternateStack()
{
# if defined(SA_ONSTACK) && !defined(Q_OS_TVOS) && !defined(Q_OS_WATCHOS)
if (alternateStackBase != MAP_FAILED) {
stack_t stack = {};
stack.ss_flags = SS_DISABLE;
sigaltstack(&stack, nullptr);
munmap(alternateStackBase, alternateStackSize().size);
}
# endif
}
void FatalSignalHandler::actionHandler(int signum, siginfo_t *info, void *)
{
writeToStderr("Received signal ", asyncSafeToString(signum),
" (SIG", signalName(signum), ")");
bool isCrashingSignal =
std::find(crashingSignals.begin(), crashingSignals.end(), signum) != crashingSignals.end();
if (isCrashingSignal && (!info || info->si_code <= 0))
isCrashingSignal = false; // wasn't sent by the kernel, so it's not really a crash
if (isCrashingSignal)
printCrashingSignalInfo(info);
else if (info && (info->si_code == SI_USER || info->si_code == SI_QUEUE))
printSentSignalInfo(info);
printTestRunTime();
if (signum != SIGINT) {
generateStackTrace();
if (pauseOnCrash) {
writeToStderr("Pausing process ", asyncSafeToString(getpid()),
" for debugging\n");
raise(SIGSTOP);
}
}
// chain back to the previous handler, if any
for (size_t i = 0; i < fatalSignals.size(); ++i) {
struct sigaction &act = oldActions()[i];
if (signum != fatalSignals[i])
continue;
// restore the handler (if SA_RESETHAND hasn't done the job for us)
if (SA_RESETHAND == 0 || act.sa_handler != SIG_DFL || act.sa_flags)
(void) sigaction(signum, &act, nullptr);
if (!isCrashingSignal)
raise(signum);
// signal is blocked, so it'll be delivered when we return
return;
}
// we shouldn't reach here!
std::abort();
}
#endif // defined(Q_OS_UNIX) && !defined(Q_OS_WASM)
} // namespace CrashHandler
} // namespace QTest
QT_END_NAMESPACE