summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBilly Donahue <billy.donahue@mongodb.com>2019-10-25 17:59:49 +0000
committerEvergreen Agent <no-reply@evergreen.mongodb.com>2020-10-09 19:04:50 +0000
commit98ea7262eecc5e42845d65d087e54decf6523bfe (patch)
treef9a1410f0f5ad0d4fab8509ae76d91fd6f2ebb97
parentece51101e58dfaf7e455c8c96df6ade42b99515c (diff)
downloadmongo-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/SConscript1
-rw-r--r--src/mongo/stdx/SConscript17
-rw-r--r--src/mongo/stdx/sigaltstack_location_test.cpp293
-rw-r--r--src/mongo/stdx/thread.h110
-rw-r--r--src/mongo/transport/service_entry_point_utils.cpp9
-rw-r--r--src/mongo/util/signal_handlers_synchronous.cpp7
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);