diff options
author | Mitch Curtis <[email protected]> | 2024-03-01 15:02:15 +0800 |
---|---|---|
committer | Mitch Curtis <[email protected]> | 2024-04-04 14:08:57 +0800 |
commit | c0014becca2cd376eadd5c8a0265e5cf47c9aa01 (patch) | |
tree | ec24059fc3c037d8c34da41f3e66009de151ec1a | |
parent | baa44b9ddfa0fe92275ff4f27d7f2b1a9be10486 (diff) |
QTest: move crash-handling code out into qtestcrashhandler_p.h
In preparation for reusing it in Qt Quick, which currently doesn't
print backtraces upon crashes.
Task-number: QTQAINFRA-6146
Change-Id: Ib0384f514b348a398f53529ff3bcc7d4ac2daba7
Reviewed-by: Qt CI Bot <[email protected]>
Reviewed-by: Ahmad Samir <[email protected]>
-rw-r--r-- | src/testlib/CMakeLists.txt | 1 | ||||
-rw-r--r-- | src/testlib/qtestcase.cpp | 743 | ||||
-rw-r--r-- | src/testlib/qtestcrashhandler.cpp | 663 | ||||
-rw-r--r-- | src/testlib/qtestcrashhandler_p.h | 251 |
4 files changed, 923 insertions, 735 deletions
diff --git a/src/testlib/CMakeLists.txt b/src/testlib/CMakeLists.txt index 014ee2c6ab0..732d45aa3ed 100644 --- a/src/testlib/CMakeLists.txt +++ b/src/testlib/CMakeLists.txt @@ -40,6 +40,7 @@ qt_internal_add_module(Test qtestblacklist.cpp qtestblacklist_p.h qtestcase.cpp qtestcase.h qtestcase_p.h qtestcoreelement_p.h + qtestcrashhandler.cpp qtestcrashhandler_p.h qtestdata.cpp qtestdata.h qtestelement.cpp qtestelement_p.h qtestelementattribute.cpp qtestelementattribute_p.h diff --git a/src/testlib/qtestcase.cpp b/src/testlib/qtestcase.cpp index b9a4c9aaf10..afa96a7032a 100644 --- a/src/testlib/qtestcase.cpp +++ b/src/testlib/qtestcase.cpp @@ -39,6 +39,7 @@ #endif // QT_CONFIG(batch_test_support) #include <QtTest/private/cycle_p.h> #include <QtTest/private/qtestblacklist_p.h> +#include <QtTest/private/qtestcrashhandler_p.h> #if defined(HAVE_XCTEST) #include <QtTest/private/qxctestlogger_p.h> #endif @@ -129,298 +130,6 @@ using namespace Qt::StringLiterals; using QtMiscUtils::toHexUpper; using QtMiscUtils::fromHex; -namespace { -enum DebuggerProgram { None, Gdb, Lldb }; - -#if defined(Q_OS_UNIX) && (!defined(Q_OS_WASM) || QT_CONFIG(thread)) -static struct iovec IoVec(struct iovec vec) -{ - return vec; -} -static struct iovec IoVec(const char *str) -{ - struct iovec r = {}; - r.iov_base = const_cast<char *>(str); - r.iov_len = strlen(str); - return r; -} - -template <typename... Args> static ssize_t writeToStderr(Args &&... args) -{ - struct iovec vec[] = { IoVec(std::forward<Args>(args))... }; - return ::writev(STDERR_FILENO, vec, std::size(vec)); -} - -// async-signal-safe conversion from int to string -struct AsyncSafeIntBuffer -{ - // digits10 + 1 for all possible digits - // +1 for the sign - // +1 for the terminating null - static constexpr int Digits10 = std::numeric_limits<int>::digits10 + 3; - std::array<char, Digits10> array; - constexpr AsyncSafeIntBuffer() : array{} {} // initializes array - AsyncSafeIntBuffer(Qt::Initialization) {} // leaves array uninitialized -}; - -static struct iovec asyncSafeToString(int n, AsyncSafeIntBuffer &&result = Qt::Uninitialized) -{ - 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; -}; -#elif defined(Q_OS_WIN) -// Windows doesn't need to be async-safe -template <typename... Args> static void writeToStderr(Args &&... args) -{ - (std::cerr << ... << args); -} - -static std::string asyncSafeToString(int n) -{ - return std::to_string(n); -} -#endif // defined(Q_OS_UNIX) -} // unnamed namespace - -[[maybe_unused]] static void blockUnixSignals(); - -static 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 -} - -static bool hasSystemCrashReporter() -{ -#if defined(Q_OS_MACOS) - return QTestPrivate::macCrashReporterWillShowDialog(); -#else - return false; -#endif -} - -static 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; -static 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) -static 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"); -} - -static 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) - static bool installCoverageTool(const char * appname, const char * testname) { #if defined(__COVERAGESCANNER__) && !QT_CONFIG(testlib_selfcover) @@ -1526,7 +1235,7 @@ public: void run() override { - blockUnixSignals(); + CrashHandler::blockUnixSignals(); auto locker = qt_unique_lock(mutex); expecting.store(TestFunctionStart, std::memory_order_release); waitCondition.notify_all(); @@ -1541,8 +1250,8 @@ public: case TestFunctionEnd: if (Q_UNLIKELY(!waitFor(locker, e))) { fflush(stderr); - printTestRunTime(); - generateStackTrace(); + CrashHandler::printTestRunTime(); + CrashHandler::generateStackTrace(); qFatal("Test function timed out"); } } @@ -1986,7 +1695,7 @@ void TestMethods::invokeTests(QObject *testObject) const invokeTestMethodIfValid(m_initTestCaseDataMethod, testObject); std::optional<WatchDog> watchDog = std::nullopt; - if (!alreadyDebugging() + if (!CrashHandler::alreadyDebugging() #if QT_CONFIG(valgrind) && QBenchmarkGlobalData::current->mode() != QBenchmarkGlobalData::CallgrindChildProcess #endif @@ -2043,442 +1752,6 @@ bool reportResult(bool success, qxp::function_ref<const char *()> lhs, } // namespace QTest -#if defined(Q_OS_WIN) -namespace { -// Helper class for resolving symbol names by dynamically loading "dbghelp.dll". -class DebugSymbolResolver -{ - Q_DISABLE_COPY_MOVE(DebugSymbolResolver) -public: - struct Symbol { - Symbol() : name(nullptr), address(0) {} - - const char *name; // Must be freed by caller. - DWORD64 address; - }; - - explicit DebugSymbolResolver(HANDLE process); - ~DebugSymbolResolver() { cleanup(); } - - bool isValid() const { return m_symFromAddr; } - - Symbol resolveSymbol(DWORD64 address) const; - -private: - // typedefs from DbgHelp.h/.dll - struct DBGHELP_SYMBOL_INFO { // SYMBOL_INFO - ULONG SizeOfStruct; - ULONG TypeIndex; // Type Index of symbol - ULONG64 Reserved[2]; - ULONG Index; - ULONG Size; - ULONG64 ModBase; // Base Address of module comtaining this symbol - ULONG Flags; - ULONG64 Value; // Value of symbol, ValuePresent should be 1 - ULONG64 Address; // Address of symbol including base address of module - ULONG Register; // register holding value or pointer to value - ULONG Scope; // scope of the symbol - ULONG Tag; // pdb classification - ULONG NameLen; // Actual length of name - ULONG MaxNameLen; - CHAR Name[1]; // Name of symbol - }; - - typedef BOOL (__stdcall *SymInitializeType)(HANDLE, PCSTR, BOOL); - typedef BOOL (__stdcall *SymFromAddrType)(HANDLE, DWORD64, PDWORD64, DBGHELP_SYMBOL_INFO *); - - void cleanup(); - - const HANDLE m_process; - HMODULE m_dbgHelpLib; - SymFromAddrType m_symFromAddr; -}; - -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; -} - -class WindowsFaultHandler -{ -public: - WindowsFaultHandler() - { -# if !defined(Q_CC_MINGW) - _CrtSetReportMode(_CRT_ERROR, _CRTDBG_MODE_DEBUG); -# endif - SetErrorMode(SetErrorMode(0) | SEM_NOGPFAULTERRORBOX); - SetUnhandledExceptionFilter(windowsFaultHandler); - } - -private: - static LONG WINAPI 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; - } -}; -} // unnamed namespace -using FatalSignalHandler = WindowsFaultHandler; - -inline 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) -namespace { -class FatalSignalHandler -{ -public: -# define OUR_SIGNALS(F) \ - F(HUP) \ - F(INT) \ - F(QUIT) \ - F(ABRT) \ - F(ILL) \ - F(BUS) \ - F(FPE) \ - F(SEGV) \ - F(PIPE) \ - F(TERM) \ - /**/ -# define CASE_LABEL(S) case SIG ## S: return QT_STRINGIFY(S); -# define ENUMERATE_SIGNALS(S) SIG ## S, - static const char *signalName(int signum) noexcept - { - switch (signum) { - OUR_SIGNALS(CASE_LABEL) - } - -# if defined(__GLIBC_MINOR__) && (__GLIBC_MINOR__ >= 32 || __GLIBC__ > 2) - // get the other signal names from glibc 2.32 - // (accessing the sys_sigabbrev variable causes linker warnings) - if (const char *p = sigabbrev_np(signum)) - return p; -# endif - return "???"; - } - static constexpr std::array fatalSignals = { - OUR_SIGNALS(ENUMERATE_SIGNALS) - }; -# undef CASE_LABEL -# undef ENUMERATE_SIGNALS - - static constexpr std::array crashingSignals = { - // Crash signals are special, because if we return from the handler - // without adjusting the machine state, the same instruction that - // originally caused the crash will get re-executed and will thus cause - // the same crash again. This is useful if our parent process logs the - // exit result or if core dumps are enabled: the core file will point - // to the actual instruction that crashed. - SIGILL, SIGBUS, SIGFPE, SIGSEGV - }; - using OldActionsArray = std::array<struct sigaction, fatalSignals.size()>; - - 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() - { - // 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(); - } - -private: - Q_DISABLE_COPY_MOVE(FatalSignalHandler) - - static OldActionsArray &oldActions() - { - Q_CONSTINIT static OldActionsArray oldActions {}; - return oldActions; - } - - auto 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 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 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 - } - - template <typename T> static - std::enable_if_t<sizeof(std::declval<T>().si_pid) + sizeof(std::declval<T>().si_uid) >= 1> - printSentSignalInfo(T *info) - { - writeToStderr(" sent by PID ", asyncSafeToString(info->si_pid), - " UID ", asyncSafeToString(info->si_uid)); - } - static void printSentSignalInfo(...) {} - - template <typename T> static - std::enable_if_t<sizeof(std::declval<T>().si_addr) >= 1> printCrashingSignalInfo(T *info) - { - using HexString = std::array<char, sizeof(quintptr) * 2>; - auto toHexString = [](quintptr u, HexString &&r = {}) { - int shift = sizeof(quintptr) * 8 - 4; - for (size_t i = 0; i < sizeof(quintptr) * 2; ++i, shift -= 4) - r[i] = QtMiscUtils::toHexLower(u >> shift); - struct iovec vec; - vec.iov_base = r.data(); - vec.iov_len = r.size(); - return vec; - }; - writeToStderr(", code ", asyncSafeToString(info->si_code), - ", for address 0x", toHexString(quintptr(info->si_addr))); - } - static void printCrashingSignalInfo(...) {} - - static void actionHandler(int signum, siginfo_t *info, void * /* ucontext */) - { - 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(); - } - - [[maybe_unused]] static void regularHandler(int signum) - { - actionHandler(signum, nullptr, nullptr); - } - - void *alternateStackBase = MAP_FAILED; - static bool pauseOnCrash; -}; -bool FatalSignalHandler::pauseOnCrash = false; -} // unnamed namespace - -inline 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); -} -#else // Q_OS_WASM or weird systems -class FatalSignalHandler {}; -inline void blockUnixSignals() {} -#endif // Q_OS_* choice - static void initEnvironment() { qputenv("QT_QTESTLIB_RUNNING", "1"); @@ -2547,7 +1820,7 @@ int QTest::qExec(QObject *testObject, int argc, char **argv) void QTest::qInit(QObject *testObject, int argc, char **argv) { initEnvironment(); - maybeDisableCoreDump(); + CrashHandler::maybeDisableCoreDump(); QBenchmarkGlobalData::current = new QBenchmarkGlobalData; #if defined(Q_OS_MACOS) @@ -2614,8 +1887,8 @@ int QTest::qRun() } else #endif { - std::optional<FatalSignalHandler> handler; - prepareStackTrace(); + std::optional<CrashHandler::FatalSignalHandler> handler; + CrashHandler::prepareStackTrace(); if (!noCrashHandler) handler.emplace(); diff --git a/src/testlib/qtestcrashhandler.cpp b/src/testlib/qtestcrashhandler.cpp new file mode 100644 index 00000000000..aabac1c4663 --- /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 diff --git a/src/testlib/qtestcrashhandler_p.h b/src/testlib/qtestcrashhandler_p.h new file mode 100644 index 00000000000..2f1a1798937 --- /dev/null +++ b/src/testlib/qtestcrashhandler_p.h @@ -0,0 +1,251 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#ifndef QTESTCRASHHANDLER_H +#define QTESTCRASHHANDLER_H + +#include <QtTest/qttestglobal.h> + +#include <QtCore/private/qtools_p.h> + +#ifdef Q_OS_UNIX +#include <signal.h> +#include <sys/mman.h> +#include <sys/uio.h> +#include <unistd.h> +#endif + +#ifdef Q_OS_WIN +#include <iostream> +# if !defined(Q_CC_MINGW) || (defined(Q_CC_MINGW) && defined(__MINGW64_VERSION_MAJOR)) +# include <crtdbg.h> +# endif +#include <qt_windows.h> // for Sleep +#endif + +QT_BEGIN_NAMESPACE +namespace QTest { +namespace CrashHandler { +#if defined(Q_OS_UNIX) && (!defined(Q_OS_WASM) || QT_CONFIG(thread)) + struct iovec IoVec(struct iovec vec); + struct iovec IoVec(const char *str); + + template <typename... Args> static ssize_t writeToStderr(Args &&... args) + { + struct iovec vec[] = { IoVec(std::forward<Args>(args))... }; + return ::writev(STDERR_FILENO, vec, std::size(vec)); + } + + // async-signal-safe conversion from int to string + struct AsyncSafeIntBuffer + { + // digits10 + 1 for all possible digits + // +1 for the sign + // +1 for the terminating null + static constexpr int Digits10 = std::numeric_limits<int>::digits10 + 3; + std::array<char, Digits10> array; + constexpr AsyncSafeIntBuffer() : array{} {} // initializes array + AsyncSafeIntBuffer(Qt::Initialization) {} // leaves array uninitialized + }; + + struct iovec asyncSafeToString(int n, AsyncSafeIntBuffer &&result = Qt::Uninitialized); +#elif defined(Q_OS_WIN) + // Windows doesn't need to be async-safe + template <typename... Args> static void writeToStderr(Args &&... args) + { + (std::cerr << ... << args); + } + + inline std::string asyncSafeToString(int n) + { + return std::to_string(n); + } +#endif // defined(Q_OS_UNIX) && (!defined(Q_OS_WASM) || QT_CONFIG(thread)) + + bool alreadyDebugging(); + void blockUnixSignals(); + +#if !defined(Q_OS_WASM) || QT_CONFIG(thread) + void printTestRunTime(); + void generateStackTrace(); +#endif + + void maybeDisableCoreDump(); + void prepareStackTrace(); + +#if defined(Q_OS_WIN) + // Helper class for resolving symbol names by dynamically loading "dbghelp.dll". + class DebugSymbolResolver + { + Q_DISABLE_COPY_MOVE(DebugSymbolResolver) + public: + struct Symbol + { + Symbol() : name(nullptr), address(0) {} + + const char *name; // Must be freed by caller. + DWORD64 address; + }; + + explicit DebugSymbolResolver(HANDLE process); + ~DebugSymbolResolver() { cleanup(); } + + bool isValid() const { return m_symFromAddr; } + + Symbol resolveSymbol(DWORD64 address) const; + + private: + // typedefs from DbgHelp.h/.dll + struct DBGHELP_SYMBOL_INFO { // SYMBOL_INFO + ULONG SizeOfStruct; + ULONG TypeIndex; // Type Index of symbol + ULONG64 Reserved[2]; + ULONG Index; + ULONG Size; + ULONG64 ModBase; // Base Address of module comtaining this symbol + ULONG Flags; + ULONG64 Value; // Value of symbol, ValuePresent should be 1 + ULONG64 Address; // Address of symbol including base address of module + ULONG Register; // register holding value or pointer to value + ULONG Scope; // scope of the symbol + ULONG Tag; // pdb classification + ULONG NameLen; // Actual length of name + ULONG MaxNameLen; + CHAR Name[1]; // Name of symbol + }; + + typedef BOOL (__stdcall *SymInitializeType)(HANDLE, PCSTR, BOOL); + typedef BOOL (__stdcall *SymFromAddrType)(HANDLE, DWORD64, PDWORD64, DBGHELP_SYMBOL_INFO *); + + void cleanup(); + + const HANDLE m_process; + HMODULE m_dbgHelpLib; + SymFromAddrType m_symFromAddr; + }; + + class WindowsFaultHandler + { + public: + WindowsFaultHandler(); + + private: + static LONG WINAPI windowsFaultHandler(struct _EXCEPTION_POINTERS *exInfo); + }; + using FatalSignalHandler = WindowsFaultHandler; +#elif defined(Q_OS_UNIX) && !defined(Q_OS_WASM) + class FatalSignalHandler + { + public: + # define OUR_SIGNALS(F) \ + F(HUP) \ + F(INT) \ + F(QUIT) \ + F(ABRT) \ + F(ILL) \ + F(BUS) \ + F(FPE) \ + F(SEGV) \ + F(PIPE) \ + F(TERM) \ + /**/ + # define CASE_LABEL(S) case SIG ## S: return QT_STRINGIFY(S); + # define ENUMERATE_SIGNALS(S) SIG ## S, + static const char *signalName(int signum) noexcept + { + switch (signum) { + OUR_SIGNALS(CASE_LABEL) + } + + # if defined(__GLIBC_MINOR__) && (__GLIBC_MINOR__ >= 32 || __GLIBC__ > 2) + // get the other signal names from glibc 2.32 + // (accessing the sys_sigabbrev variable causes linker warnings) + if (const char *p = sigabbrev_np(signum)) + return p; + # endif + return "???"; + } + static constexpr std::array fatalSignals = { + OUR_SIGNALS(ENUMERATE_SIGNALS) + }; + # undef CASE_LABEL + # undef ENUMERATE_SIGNALS + + static constexpr std::array crashingSignals = { + // Crash signals are special, because if we return from the handler + // without adjusting the machine state, the same instruction that + // originally caused the crash will get re-executed and will thus cause + // the same crash again. This is useful if our parent process logs the + // exit result or if core dumps are enabled: the core file will point + // to the actual instruction that crashed. + SIGILL, SIGBUS, SIGFPE, SIGSEGV + }; + using OldActionsArray = std::array<struct sigaction, fatalSignals.size()>; + + FatalSignalHandler(); + ~FatalSignalHandler(); + + private: + Q_DISABLE_COPY_MOVE(FatalSignalHandler) + + static OldActionsArray &oldActions(); + auto alternateStackSize(); + int setupAlternateStack(); + void freeAlternateStack(); + + template <typename T> static + std::enable_if_t<sizeof(std::declval<T>().si_pid) + sizeof(std::declval<T>().si_uid) >= 1> + printSentSignalInfo(T *info) + { + writeToStderr(" sent by PID ", asyncSafeToString(info->si_pid), + " UID ", asyncSafeToString(info->si_uid)); + } + static void printSentSignalInfo(...) {} + + template <typename T> static + std::enable_if_t<sizeof(std::declval<T>().si_addr) >= 1> printCrashingSignalInfo(T *info) + { + using HexString = std::array<char, sizeof(quintptr) * 2>; + auto toHexString = [](quintptr u, HexString &&r = {}) { + int shift = sizeof(quintptr) * 8 - 4; + for (size_t i = 0; i < sizeof(quintptr) * 2; ++i, shift -= 4) + r[i] = QtMiscUtils::toHexLower(u >> shift); + struct iovec vec; + vec.iov_base = r.data(); + vec.iov_len = r.size(); + return vec; + }; + writeToStderr(", code ", asyncSafeToString(info->si_code), + ", for address 0x", toHexString(quintptr(info->si_addr))); + } + static void printCrashingSignalInfo(...) {} + static void actionHandler(int signum, siginfo_t *info, void * /* ucontext */); + + [[maybe_unused]] static void regularHandler(int signum) + { + actionHandler(signum, nullptr, nullptr); + } + + void *alternateStackBase = MAP_FAILED; + static bool pauseOnCrash; + }; +#else // Q_OS_WASM or weird systems +class FatalSignalHandler {}; +inline void blockUnixSignals() {} +#endif // Q_OS_* choice +} // namespace CrashHandler +} // namespace QTest +QT_END_NAMESPACE + +#endif // QTESTCRASHHANDLER_H |