diff options
author | Billy Donahue <billy.donahue@mongodb.com> | 2019-10-25 17:59:49 +0000 |
---|---|---|
committer | Evergreen Agent <no-reply@evergreen.mongodb.com> | 2020-10-09 19:04:50 +0000 |
commit | 98ea7262eecc5e42845d65d087e54decf6523bfe (patch) | |
tree | f9a1410f0f5ad0d4fab8509ae76d91fd6f2ebb97 | |
parent | ece51101e58dfaf7e455c8c96df6ade42b99515c (diff) | |
download | mongo-98ea7262eecc5e42845d65d087e54decf6523bfe.tar.gz |
SERVER-15902 sigaltstack for workers and stdx::thread.
(cherry picked from commit 0d84ec739b3e831de70775a5cae20ac1c26c28b1)
- simulate std::apply with ref(f)(...)
- remove libfmt dep
- downgrade some c++17 idioms to c++14.
- clang-format to v4.0 style
-rw-r--r-- | src/mongo/SConscript | 1 | ||||
-rw-r--r-- | src/mongo/stdx/SConscript | 17 | ||||
-rw-r--r-- | src/mongo/stdx/sigaltstack_location_test.cpp | 293 | ||||
-rw-r--r-- | src/mongo/stdx/thread.h | 110 | ||||
-rw-r--r-- | src/mongo/transport/service_entry_point_utils.cpp | 9 | ||||
-rw-r--r-- | src/mongo/util/signal_handlers_synchronous.cpp | 7 |
6 files changed, 433 insertions, 4 deletions
diff --git a/src/mongo/SConscript b/src/mongo/SConscript index 8b76f063d1c..2676befa029 100644 --- a/src/mongo/SConscript +++ b/src/mongo/SConscript @@ -36,6 +36,7 @@ env.SConscript( 's', 'scripting', 'shell', + 'stdx', 'tools', 'transport', 'unittest', diff --git a/src/mongo/stdx/SConscript b/src/mongo/stdx/SConscript new file mode 100644 index 00000000000..2284ae3f93c --- /dev/null +++ b/src/mongo/stdx/SConscript @@ -0,0 +1,17 @@ +# -*- mode: python -*- + +Import("env") + +env = env.Clone(); + +# Not a CppUnitTest because it needs low-level control of thread creation and signals, +# so it shouldn't use unittest_main and typical mongo startup routines. +env.RegisterUnitTest(env.Program( + target='sigaltstack_location_test', + source=[ + 'sigaltstack_location_test.cpp', + ], + LIBDEPS=[ + '$BUILD_DIR/mongo/base', + ] +)[0]) diff --git a/src/mongo/stdx/sigaltstack_location_test.cpp b/src/mongo/stdx/sigaltstack_location_test.cpp new file mode 100644 index 00000000000..7d8c829c11a --- /dev/null +++ b/src/mongo/stdx/sigaltstack_location_test.cpp @@ -0,0 +1,293 @@ +/** + * Copyright (C) 2019-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#include "mongo/stdx/thread.h" + +#include <condition_variable> +#include <exception> +#include <iostream> +#include <mutex> +#include <setjmp.h> +#include <stdexcept> + +#ifndef _WIN32 +#include <sys/types.h> +#include <sys/wait.h> +#include <unistd.h> +#endif + +#if !MONGO_HAS_SIGALTSTACK + +int main() { + std::cout << "`sigaltstack` testing skipped on this platform." << std::endl; + return EXIT_SUCCESS; +} + +#else // MONGO_HAS_SIGALTSTACK + +#if !defined(__has_feature) +#define __has_feature(x) 0 +#endif + +namespace mongo { +namespace stdx { +namespace { + +/** Make sure sig is unblocked. */ +void unblockSignal(int sig) { + sigset_t sigset; + sigemptyset(&sigset); + sigaddset(&sigset, sig); + if (sigprocmask(SIG_UNBLOCK, &sigset, nullptr)) { + perror("sigprocmask"); + exit(EXIT_FAILURE); + } +} + +/** Install action for signal sig. Be careful to specify SA_ONSTACK. */ +void installAction(int sig, void (*action)(int, siginfo_t*, void*)) { + struct sigaction sa; + sa.sa_sigaction = action; + sa.sa_flags = SA_SIGINFO | SA_ONSTACK; + sigemptyset(&sa.sa_mask); + if (sigaction(sig, &sa, nullptr)) { + perror("sigaction"); + exit(EXIT_FAILURE); + } +} + +void uninstallSigAltStack() { + // Disable sigaltstack to see what happens. Process should die. + stack_t ss{}; + ss.ss_flags = SS_DISABLE; + if (sigaltstack(&ss, nullptr)) { + perror("uninstall sigaltstack"); + abort(); + } +} + +template <typename T> +struct H { + explicit H(const T& t) : _t(t) {} + friend std::ostream& operator<<(std::ostream& os, const H& h) { + return os << std::hex << std::showbase << h._t << std::noshowbase << std::dec; + } + const T& _t; +}; + +template <typename T> +auto Hex(const T& x) { + return H<T>{x}; +} + +int stackLocationTest() { + struct ChildThreadInfo { + stack_t ss; + const char* handlerLocal; + }; + static ChildThreadInfo childInfo{}; + + stdx::thread childThread([&] { + static const int kSignal = SIGUSR1; + // Use sigaltstack's `old_ss` parameter to query the installed sigaltstack. + if (sigaltstack(nullptr, &childInfo.ss)) { + perror("sigaltstack"); + abort(); + } + unblockSignal(kSignal); + installAction(kSignal, [](int, siginfo_t*, void*) { + char n; + childInfo.handlerLocal = &n; + }); + // `raise` waits for signal handler to complete. + // https://pubs.opengroup.org/onlinepubs/009695399/functions/raise.html + raise(kSignal); + }); + childThread.join(); + + if (childInfo.ss.ss_flags & SS_DISABLE) { + std::cerr << "Child thread unexpectedly had sigaltstack disabled." << std::endl; + exit(EXIT_FAILURE); + } + + uintptr_t altStackBegin = reinterpret_cast<uintptr_t>(childInfo.ss.ss_sp); + uintptr_t altStackEnd = altStackBegin + childInfo.ss.ss_size; + uintptr_t handlerLocal = reinterpret_cast<uintptr_t>(childInfo.handlerLocal); + + std::cerr << "child sigaltstack[" << Hex(altStackEnd - altStackBegin) << "] = [" + << Hex(altStackBegin) << ", " << Hex(altStackEnd) << ")\n" + << "handlerLocal = " << Hex(handlerLocal) << "(sigaltstack + " + << Hex(handlerLocal - altStackBegin) << ")" << std::endl; + if (handlerLocal < altStackBegin || handlerLocal >= altStackEnd) { + std::cerr << "Handler local address " << Hex(handlerLocal) << " was outside of: [" + << Hex(altStackBegin) << ", " << Hex(altStackEnd) << ")" << std::endl; + exit(EXIT_FAILURE); + } + return EXIT_SUCCESS; +} + +/** + * Start a child thread which overflows its stack, causing it to receive a SIGSEGV. If + * !useSigAltStack, disable that child thread's sigaltstack. + * + * We install a signal handler for SIGSEGV that gives the child thread a way out of the + * SIGSEGV: it can siglongjmp to a sigsetjmp point before the recursion started. This + * allows the child thread to recover and exit normally. + * + * This can only happen if the signal handler can be activated safely while the thread + * is in the stack overflow condition. The sigaltstack is what makes it possible to do + * so. Without sigaltstack, there's no stack space for the signal handler to run, so the + * SIGSEGV is process-fatal. + */ +int recursionTestImpl(bool useSigAltStack) { + static sigjmp_buf sigjmp; + + unblockSignal(SIGSEGV); + installAction(SIGSEGV, [](int, siginfo_t*, void*) { siglongjmp(sigjmp, 1); }); + + stdx::thread childThread([=] { + if (!useSigAltStack) { + uninstallSigAltStack(); + std::cout << "child thread uninstalled its sigaltstack" << std::endl; + } + + struct MostlyInfiniteRecursion { + // Recurse to run out of stack on purpose. There can be no destructors or + // AS-unsafe code here, as this function terminates via `siglongjmp`. + void run() { + if (++depth == std::numeric_limits<size_t>::max()) + return; // Avoid the undefined behavior of truly infinite recursion. + char localVar; + deepestAddress = &localVar; + run(); + } + size_t depth; + void* deepestAddress; + }; + MostlyInfiniteRecursion recursion = {0, &recursion}; + + // When the signal handler fires, it will return to this sigsetjmp call, causing + // it to return a nonzero value. This makes the child thread viable again, and + // it prints a few diagnostics before exiting gracefully. + // There are special rules about the kinds of expressions in which `setjmp` can appear. + if (sigsetjmp(sigjmp, 1)) { + // Nonzero: a fake return from the signal handler's `siglongjmp`. + ptrdiff_t stackSpan = (const char*)&recursion - (const char*)recursion.deepestAddress; + std::cout << "Recovered from SIGSEGV after stack depth=" << recursion.depth + << ", stack spans approximately " << (1. * stackSpan / (1 << 20)) + << " MiB.\n"; + std::cout << "That is " << (1. * stackSpan / recursion.depth) << " bytes per frame" + << std::endl; + } else { + // Does not return, but recovers via signal handler's `siglongjmp`. + recursion.run(); + } + }); + childThread.join(); + return EXIT_SUCCESS; +} + +/** + * Cause an infinite recursion to check that the sigaltstack recovery mechanism + * built into `stdx::thread` works. + */ +int recursionTest() { + return recursionTestImpl(true); +} + +/** + * Check that stack overflow will crash the process and signal delivery can't happen. + * Verifies that the sigaltstack is necessary. + */ +int recursionDeathTest() { + pid_t kidPid = fork(); + if (kidPid == -1) { + perror("fork"); + return EXIT_FAILURE; + } else if (kidPid == 0) { + // Child process: run the recursion test with no sigaltstack protection. + return recursionTestImpl(false); + } else { + // Parent process: expect child to die from a SIGSEGV. + int wstatus; + pid_t waited = waitpid(kidPid, &wstatus, 0); + if (waited == -1) { + perror("waitpid"); + return EXIT_FAILURE; + } + if (WIFEXITED(wstatus)) { + std::cerr << "child unexpectedly exited with: " << WEXITSTATUS(wstatus) << std::endl; + return EXIT_FAILURE; + } + if (!WIFSIGNALED(wstatus)) { + std::cerr << "child did not die from a signal" << std::endl; + return EXIT_FAILURE; + } + int kidSignal = WTERMSIG(wstatus); + if (kidSignal != SIGSEGV) { + std::cerr << "child died from the wrong signal: " << kidSignal << std::endl; + return EXIT_FAILURE; + } + return EXIT_SUCCESS; + } +} + +int runTests() { + struct Test { + const char* name; + int (*func)(); + } static constexpr kTests[] = { + {"stackLocationTest", &stackLocationTest}, +// These tests violate the memory space deliberately, so they generate false positives from ASAN. +#if !__has_feature(address_sanitizer) + {"recursionTest", &recursionTest}, + {"recursionDeathTest", &recursionDeathTest}, +#endif + }; + for (auto& test : kTests) { + std::cout << "\n===== " << test.name << " begin:" << std::endl; + int r = test.func(); + if (r != EXIT_SUCCESS) { + std::cout << test.name << " FAIL" << std::endl; + return r; + } + std::cout << "===== " << test.name << " PASS" << std::endl; + } + return EXIT_SUCCESS; +} + +} // namespace +} // namespace stdx +} // namespace mongo + +int main() { + return mongo::stdx::runTests(); +} + +#endif // MONGO_HAS_SIGALTSTACK diff --git a/src/mongo/stdx/thread.h b/src/mongo/stdx/thread.h index 7fd244ee238..74e6f3c7883 100644 --- a/src/mongo/stdx/thread.h +++ b/src/mongo/stdx/thread.h @@ -31,13 +31,109 @@ #pragma once #include <chrono> +#include <csignal> +#include <cstddef> +#include <cstdint> #include <ctime> #include <exception> #include <thread> #include <type_traits> +#if defined(__linux__) || defined(__FreeBSD__) +#define MONGO_HAS_SIGALTSTACK 1 +#else +#define MONGO_HAS_SIGALTSTACK 0 +#endif + namespace mongo { namespace stdx { +namespace support { + +/** + * Manages an alternate stack for signal handlers. + * A dummy implementation is provided on platforms which do not support `sigaltstack`. + */ +class SigAltStackController { +public: +#if MONGO_HAS_SIGALTSTACK + /** Return an object that installs and uninstalls our `_stackStorage` as `sigaltstack`. */ + auto makeInstallGuard() const { + struct Guard { + explicit Guard(const SigAltStackController& controller) : _controller(controller) { + _controller._install(); + } + + ~Guard() { + _controller._uninstall(); + } + + const SigAltStackController& _controller; + }; + return Guard{*this}; + } + +private: + void _install() const { + stack_t ss; + ss.ss_sp = _stackStorage.get(); + ss.ss_flags = 0; + ss.ss_size = kStackSize; + if (sigaltstack(&ss, nullptr)) { + abort(); + } + } + + void _uninstall() const { + stack_t ss; + ss.ss_flags = SS_DISABLE; + if (sigaltstack(&ss, nullptr)) { + abort(); + } + } + + // Signal stack consumption was measured in mongo/util/stacktrace_test. + // 64 kiB is 4X our worst case, so that should be enough. + // . signal handler action + // . --use-libunwind : ----\ ============================= + // . --dbg=on : -\ \ minimal | print | backtrace + // . = = ========|=========|========== + // . N N : 4,344 | 7,144 | 5,096 + // . Y N : 4,424 | 7,528 | 5,160 + // . N Y : 4,344 | 13,048 | 7,352 + // . Y Y : 4,424 | 13,672 | 8,392 + // ( https://jira.mongodb.org/secure/attachment/233569/233569_stacktrace-writeup.txt ) + static constexpr std::size_t kMongoMinSignalStackSize = std::size_t{64} << 10; + + static constexpr std::size_t kStackSize = + std::max(kMongoMinSignalStackSize, std::size_t{MINSIGSTKSZ}); + std::unique_ptr<char[]> _stackStorage = std::make_unique<char[]>(kStackSize); + +#else // !MONGO_HAS_SIGALTSTACK + auto makeInstallGuard() const { + struct Guard { + ~Guard() {} // needed to suppress 'unused variable' warnings. + }; + return Guard{}; + } +#endif // !MONGO_HAS_SIGALTSTACK +}; + + +template <typename F, typename Tup, size_t... Is> +void apply(F&& f, Tup&& args, std::index_sequence<Is...>) { + // Use `std::reference_wrapper` to indirectly call `INVOKE` from [func.require], + // as the `std::thread` constructor would. + std::ref(f)(std::move(std::get<Is>(args))...); +} + +template <typename F, typename Tup> +void apply(F&& f, Tup&& args) { + apply(std::forward<F>(f), + std::forward<Tup>(args), + std::make_index_sequence<std::tuple_size<Tup>::value>{}); +} + +} // namespace support /** * We're wrapping std::thread here, rather than aliasing it, because we'd like @@ -48,6 +144,8 @@ namespace stdx { * of the system failed thread creation (as the exception itself is caught at * the top of the stack). * + * We also want to allocate and install a `sigaltstack` to diagnose stack overflows. + * * We're putting this in stdx, rather than having it as some kind of * mongo::Thread, because the signature and use of the type is otherwise * completely identical. Rather than migrate all callers, it was deemed @@ -79,8 +177,16 @@ public: class... Args, typename std::enable_if<!std::is_same<thread, typename std::decay<Function>::type>::value, int>::type = 0> - explicit thread(Function&& f, Args&&... args) try: - ::std::thread::thread(std::forward<Function>(f), std::forward<Args>(args)...) {} // NOLINT + explicit thread(Function f, Args&&... args) try: + ::std::thread::thread( // NOLINT + [ + sigAltStackController = support::SigAltStackController(), + f = std::move(f), + pack = std::make_tuple(std::forward<Args>(args)...) + ]() mutable noexcept { + auto sigAltStackGuard = sigAltStackController.makeInstallGuard(); + return support::apply(std::move(f), std::move(pack)); + }) {} catch (...) { std::terminate(); } diff --git a/src/mongo/transport/service_entry_point_utils.cpp b/src/mongo/transport/service_entry_point_utils.cpp index a2543fbd665..ff46cbfe5b6 100644 --- a/src/mongo/transport/service_entry_point_utils.cpp +++ b/src/mongo/transport/service_entry_point_utils.cpp @@ -90,6 +90,15 @@ Status launchServiceWorkerThread(stdx::function<void()> task) { warning() << "Stack size set to " << (limits.rlim_cur / 1024) << "KB. We suggest 1MB"; } + // Wrap the user-specified `task` so it runs with an installed `sigaltstack`. + task = [ + sigAltStackController = std::make_shared<stdx::support::SigAltStackController>(), + f = std::move(task) + ] { + auto sigAltStackGuard = sigAltStackController->makeInstallGuard(); + f(); + }; + pthread_t thread; auto ctx = stdx::make_unique<stdx::function<void()>>(std::move(task)); int failed = pthread_create(&thread, &attrs, runFunc, ctx.get()); diff --git a/src/mongo/util/signal_handlers_synchronous.cpp b/src/mongo/util/signal_handlers_synchronous.cpp index 1c526521d6a..b3e8202fafd 100644 --- a/src/mongo/util/signal_handlers_synchronous.cpp +++ b/src/mongo/util/signal_handlers_synchronous.cpp @@ -49,6 +49,7 @@ #include "mongo/logger/logger.h" #include "mongo/platform/compiler.h" #include "mongo/stdx/thread.h" +#include "mongo/util/assert_util.h" #include "mongo/util/concurrency/thread_name.h" #include "mongo/util/debug_util.h" #include "mongo/util/debugger.h" @@ -264,7 +265,9 @@ void myPureCallHandler() { #else -void abruptQuitWithAddrSignal(int signalNum, siginfo_t* siginfo, void*) { +void abruptQuitWithAddrSignal(int signalNum, siginfo_t* siginfo, void* ucontext_erased) { + // For convenient debugger access. + MONGO_COMPILER_VARIABLE_UNUSED auto ucontext = static_cast<const ucontext_t*>(ucontext_erased); MallocFreeOStreamGuard lk{}; const char* action = (signalNum == SIGSEGV || signalNum == SIGBUS) ? "access" : "operation"; @@ -344,7 +347,7 @@ void setupSynchronousSignalHandlers() { memset(&addrSignals, 0, sizeof(addrSignals)); addrSignals.sa_sigaction = abruptQuitWithAddrSignal; sigemptyset(&addrSignals.sa_mask); - addrSignals.sa_flags = SA_SIGINFO; + addrSignals.sa_flags = SA_SIGINFO | SA_ONSTACK; invariant(sigaction(SIGSEGV, &addrSignals, nullptr) == 0); invariant(sigaction(SIGBUS, &addrSignals, nullptr) == 0); |