diff options
Diffstat (limited to 'src/testlib/qtestcrashhandler.cpp')
-rw-r--r-- | src/testlib/qtestcrashhandler.cpp | 663 |
1 files changed, 663 insertions, 0 deletions
diff --git a/src/testlib/qtestcrashhandler.cpp b/src/testlib/qtestcrashhandler.cpp new file mode 100644 index 0000000000..aabac1c466 --- /dev/null +++ b/src/testlib/qtestcrashhandler.cpp @@ -0,0 +1,663 @@ +// 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/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_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 |