aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--vespalib/CMakeLists.txt1
-rw-r--r--vespalib/src/tests/util/memory_trap/CMakeLists.txt9
-rw-r--r--vespalib/src/tests/util/memory_trap/memory_trap_test.cpp61
-rw-r--r--vespalib/src/vespa/vespalib/util/CMakeLists.txt1
-rw-r--r--vespalib/src/vespa/vespalib/util/memory_trap.cpp171
-rw-r--r--vespalib/src/vespa/vespalib/util/memory_trap.h101
6 files changed, 344 insertions, 0 deletions
diff --git a/vespalib/CMakeLists.txt b/vespalib/CMakeLists.txt
index a3d5054973f..87d9cb559c8 100644
--- a/vespalib/CMakeLists.txt
+++ b/vespalib/CMakeLists.txt
@@ -193,6 +193,7 @@ vespa_define_module(
src/tests/util/generationhandler_stress
src/tests/util/hamming
src/tests/util/md5
+ src/tests/util/memory_trap
src/tests/util/mmap_file_allocator
src/tests/util/mmap_file_allocator_factory
src/tests/util/rcuvector
diff --git a/vespalib/src/tests/util/memory_trap/CMakeLists.txt b/vespalib/src/tests/util/memory_trap/CMakeLists.txt
new file mode 100644
index 00000000000..c3241b0ad93
--- /dev/null
+++ b/vespalib/src/tests/util/memory_trap/CMakeLists.txt
@@ -0,0 +1,9 @@
+# Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+vespa_add_executable(vespalib_util_memory_trap_test_app TEST
+ SOURCES
+ memory_trap_test.cpp
+ DEPENDS
+ vespalib
+ GTest::GTest
+)
+vespa_add_test(NAME vespalib_util_memory_trap_test_app COMMAND vespalib_util_memory_trap_test_app)
diff --git a/vespalib/src/tests/util/memory_trap/memory_trap_test.cpp b/vespalib/src/tests/util/memory_trap/memory_trap_test.cpp
new file mode 100644
index 00000000000..ee26231c546
--- /dev/null
+++ b/vespalib/src/tests/util/memory_trap/memory_trap_test.cpp
@@ -0,0 +1,61 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+#include <vespa/vespalib/util/memory_trap.h>
+#include <vespa/vespalib/gtest/gtest.h>
+#include <cstdlib>
+
+using namespace vespalib;
+using namespace ::testing;
+
+template <typename T>
+void do_not_optimize_away(T&& t) noexcept {
+ asm volatile("" : : "m"(t) : "memory"); // Clobber the value to avoid losing it to compiler optimizations
+}
+
+struct MemoryTrapTest : Test {
+ static void SetUpTestSuite() {
+ // Don't overwrite env var if already set; we'll assume it's done for a good reason.
+ setenv("VESPA_USE_MPROTECT_TRAP", "yes", 0);
+ }
+};
+
+TEST_F(MemoryTrapTest, untouched_memory_traps_do_not_trigger) {
+ InlineMemoryTrap<2> stack_trap;
+ HeapMemoryTrap heap_trap(4);
+ // No touching == no crashing. Good times.
+}
+
+TEST_F(MemoryTrapTest, write_to_stack_trap_eventually_discovered) {
+ // We don't explicitly test death messages since the way the process dies depends on
+ // whether mprotect is enabled, whether ASAN instrumentation is enabled etc.
+ ASSERT_DEATH({
+ InlineMemoryTrap<2> stack_trap;
+ // This may trigger immediately or on destruction. Either way it eventually kills the process.
+ stack_trap.trapper().buffer()[0] = 0x01;
+ },"");
+}
+
+TEST_F(MemoryTrapTest, write_to_heap_trap_eventually_discovered) {
+ ASSERT_DEATH({
+ HeapMemoryTrap heap_trap(4);
+ // This may trigger immediately or on destruction. Either way it eventually kills the process.
+ heap_trap.trapper().buffer()[heap_trap.trapper().size() - 1] = 0x01;
+ },"");
+}
+
+TEST_F(MemoryTrapTest, read_from_hw_backed_trap_crashes_process) {
+ if (!MemoryRangeTrapper::hw_trapping_enabled()) {
+ return;
+ }
+ ASSERT_DEATH({
+ HeapMemoryTrap heap_trap(4); // Entire buffer should always be covered
+ // Clobber trap just in case the compiler is clever enough to look into the trap implementation
+ // and see that we memset everything to zero and `dummy` can thus be constant-promoted to 0
+ // (probably won't dare to do this anyway due to opaque mprotect() that touches buffer pointer).
+ do_not_optimize_away(heap_trap);
+ char dummy = heap_trap.trapper().buffer()[0];
+ do_not_optimize_away(dummy); // never reached
+ },"");
+}
+
+GTEST_MAIN_RUN_ALL_TESTS()
diff --git a/vespalib/src/vespa/vespalib/util/CMakeLists.txt b/vespalib/src/vespa/vespalib/util/CMakeLists.txt
index 3812fda4bdf..ad2db89288c 100644
--- a/vespalib/src/vespa/vespalib/util/CMakeLists.txt
+++ b/vespalib/src/vespa/vespalib/util/CMakeLists.txt
@@ -54,6 +54,7 @@ vespa_add_library(vespalib_vespalib_util OBJECT
lz4compressor.cpp
malloc_mmap_guard.cpp
md5.c
+ memory_trap.cpp
memoryusage.cpp
mmap_file_allocator.cpp
mmap_file_allocator_factory.cpp
diff --git a/vespalib/src/vespa/vespalib/util/memory_trap.cpp b/vespalib/src/vespa/vespalib/util/memory_trap.cpp
new file mode 100644
index 00000000000..113af0de35a
--- /dev/null
+++ b/vespalib/src/vespa/vespalib/util/memory_trap.cpp
@@ -0,0 +1,171 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+#ifdef __linux__
+# define VESPA_HAS_MPROTECT
+#endif
+
+#include "memory_trap.h"
+#include <string_view>
+#include <cassert>
+#include <cerrno>
+#include <cstdint>
+#include <cstdlib>
+#include <cstring>
+#include <malloc.h>
+#ifdef VESPA_HAS_MPROTECT
+# include <unistd.h>
+# include <sys/mman.h>
+#endif
+
+#include <vespa/log/log.h>
+LOG_SETUP(".vespalib.util.memory_trap");
+
+using namespace std::string_view_literals;
+
+namespace vespalib {
+
+namespace {
+
+#ifdef VESPA_HAS_MPROTECT
+
+bool has_4k_pages() noexcept {
+ return (sysconf(_SC_PAGESIZE) == 4096);
+}
+
+constexpr bool is_4k_aligned(size_t v) noexcept {
+ return (v % 4096) == 0;
+}
+
+constexpr size_t align_up_4k(size_t v) noexcept {
+ return (v + 4095) & ~4095ULL;
+}
+
+constexpr size_t align_down_4k(size_t v) noexcept {
+ return v & ~4095ULL;
+}
+
+bool env_var_is_yes(const char* env_var) noexcept {
+ const char* ev = getenv(env_var);
+ return ((ev != nullptr) && ("yes"sv == ev));
+}
+
+bool mprotect_trapping_is_enabled() noexcept {
+ static const bool enabled = (has_4k_pages() && env_var_is_yes("VESPA_USE_MPROTECT_TRAP"));
+ return enabled;
+}
+
+#endif
+
+// Have some symbols that provide immediate context in a crash backtrace
+[[noreturn]] void abort_due_to_guard_bits_tampered_with() __attribute__((noinline));
+[[noreturn]] void abort_due_to_guard_bits_tampered_with() {
+ abort();
+}
+
+[[noreturn]] void abort_due_to_PROTECTED_guard_bits_tampered_with() __attribute__((noinline));
+[[noreturn]] void abort_due_to_PROTECTED_guard_bits_tampered_with() {
+ abort();
+}
+
+} // anon ns
+
+MemoryRangeTrapper::MemoryRangeTrapper(char* trap_buf, size_t buf_len) noexcept
+ : _trap_buf(trap_buf),
+ _buf_len(buf_len),
+ _trap_offset(0),
+ _trap_len(0)
+{
+ if (_buf_len > 0) {
+ memset(trap_buf, 0, _buf_len);
+ }
+ rw_protect_buffer_if_possible();
+}
+
+MemoryRangeTrapper::~MemoryRangeTrapper() {
+ check_and_release();
+}
+
+void MemoryRangeTrapper::rw_protect_buffer_if_possible() {
+#ifdef VESPA_HAS_MPROTECT
+ static_assert(std::is_same_v<size_t, uintptr_t>);
+ const auto aligned_start = align_up_4k(reinterpret_cast<uintptr_t>(_trap_buf));
+ const auto aligned_end = align_down_4k(reinterpret_cast<uintptr_t>(_trap_buf + _buf_len));
+ if ((aligned_end > aligned_start) && mprotect_trapping_is_enabled()) {
+ _trap_offset = aligned_start - reinterpret_cast<uintptr_t>(_trap_buf);
+ _trap_len = aligned_end - aligned_start;
+ assert(is_4k_aligned(_trap_len));
+
+ LOG(info, "attempting mprotect(%p + %zu = %p, %zu, PROT_NONE)",
+ _trap_buf, _trap_offset, _trap_buf + _trap_offset, _trap_len);
+ int ret = mprotect(_trap_buf + _trap_offset, _trap_len, PROT_NONE);
+ if (ret != 0) {
+ LOG(warning, "Failed to mprotect(%p + %zu, %zu, PROT_NONE). errno = %d. "
+ "Falling back to unprotected mode.",
+ _trap_buf, _trap_offset, _trap_len, errno);
+ _trap_offset = _trap_len = 0;
+ }
+ }
+#endif
+}
+
+bool MemoryRangeTrapper::hw_trapping_enabled() noexcept {
+#ifdef VESPA_HAS_MPROTECT
+ return mprotect_trapping_is_enabled();
+#else
+ return false;
+#endif
+}
+
+void MemoryRangeTrapper::check_and_release() noexcept {
+ unprotect_buffer_to_read_only(); // Make sure sanity check can't race with writes
+ verify_buffer_is_all_zeros();
+ unprotect_buffer_to_read_and_write();
+ _trap_len = _buf_len = 0;
+}
+
+void MemoryRangeTrapper::unprotect_buffer_to_read_only() {
+#ifdef VESPA_HAS_MPROTECT
+ if (_trap_len > 0) {
+ int ret = mprotect(_trap_buf + _trap_offset, _trap_len, PROT_READ);
+ assert(ret == 0 && "failed to un-protect memory region to PROT_READ");
+ }
+#endif
+}
+
+void MemoryRangeTrapper::unprotect_buffer_to_read_and_write() {
+#ifdef VESPA_HAS_MPROTECT
+ if (_trap_len > 0) {
+ int ret = mprotect(_trap_buf + _trap_offset, _trap_len, PROT_READ | PROT_WRITE);
+ assert(ret == 0 && "failed to un-protect memory region to PROT_READ | PROT_WRITE");
+ }
+#endif
+}
+
+void MemoryRangeTrapper::verify_buffer_is_all_zeros() {
+ for (size_t i = 0; i < _buf_len; ++i) {
+ if (_trap_buf[i] != 0) {
+ const bool in_protected_area = ((i >= _trap_offset) && (i < _trap_offset + _trap_len));
+ LOG(error, "Memory corruption detected! Offset %zu into buffer %p: 0x%.2x != 0x00%s",
+ i, _trap_buf, static_cast<unsigned int>(_trap_buf[i]),
+ in_protected_area ? ". CORRUPTION IN R/W PROTECTED MEMORY!" : "");
+ if (in_protected_area) {
+ abort_due_to_PROTECTED_guard_bits_tampered_with();
+ } else {
+ abort_due_to_guard_bits_tampered_with();
+ }
+ }
+ }
+}
+
+HeapMemoryTrap::HeapMemoryTrap(size_t trap_4k_pages)
+ : _trap_buf(static_cast<char*>(memalign(4096, trap_4k_pages * 4096))),
+ _trapper(_trap_buf, _trap_buf ? trap_4k_pages * 4096 : 0)
+{
+}
+
+HeapMemoryTrap::~HeapMemoryTrap() {
+ _trapper.check_and_release();
+ free(_trap_buf);
+}
+
+}
diff --git a/vespalib/src/vespa/vespalib/util/memory_trap.h b/vespalib/src/vespa/vespalib/util/memory_trap.h
new file mode 100644
index 00000000000..f9c8e8458fd
--- /dev/null
+++ b/vespalib/src/vespa/vespalib/util/memory_trap.h
@@ -0,0 +1,101 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+#pragma once
+
+#include <cstddef>
+
+namespace vespalib {
+
+/**
+ * Guard for attempting to detect spurious writes (and if possible; reads) to a memory region.
+ *
+ * If supported by the OS+HW, as much as possible of the buffer will be mapped
+ * as non-readable and non-writable. This immediately triggers a SIGSEGV for any
+ * spurious read or write to the mapped buffer sub-range.
+ *
+ * For memory map-backed trapping to be used, all of the following must hold:
+ * - The process must be running on Linux and on hardware with a page size of 4 KiB
+ * - The environment variable VESPA_USE_MPROTECT_TRAP must be set and have the value 'yes'
+ * - The trap buffer must be long enough to fit at least one whole 4 KiB-aligned page
+ * - The buffer passed to the trapper must originally have been allocated via mmap().
+ * This should hold for any reasonable implementation of malloc().
+ *
+ * Regardless of whether memory map-backed trapping is used, the buffer will always be
+ * filled with all zeroes upon construction. If any buffer byte is non-zero upon
+ * destruction, the process will be terminated with a corruption error in the logs.
+ *
+ * If buffer mapping fails during construction, the trapper falls back to just checking
+ * buffer contents. This may happen if the kernel has exhausted the bookkeeping-structures
+ * for keeping track of separate virtual memory ranges.
+ *
+ * Note that due to possible interference with things like hugepages etc, VESPA_USE_MPROTECT_TRAP
+ * should only be selectively enabled.
+ */
+class MemoryRangeTrapper {
+ char* _trap_buf;
+ size_t _buf_len;
+ size_t _trap_offset;
+ size_t _trap_len;
+public:
+ MemoryRangeTrapper(char* trap_buf, size_t buf_len) noexcept;
+ ~MemoryRangeTrapper();
+
+ MemoryRangeTrapper(const MemoryRangeTrapper&) = delete;
+ MemoryRangeTrapper(MemoryRangeTrapper&&) noexcept = delete;
+
+ // Exposed for testing only
+ char* buffer() const noexcept { return _trap_buf; }
+ size_t size() const noexcept { return _buf_len; }
+
+ void check_and_release() noexcept;
+
+ [[nodiscard]] static bool hw_trapping_enabled() noexcept;
+private:
+ void rw_protect_buffer_if_possible();
+ void unprotect_buffer_to_read_only();
+ void unprotect_buffer_to_read_and_write();
+ void verify_buffer_is_all_zeros();
+};
+
+/**
+ * Places a memory trap "inline" with other variables in an object. I.e. the trap will
+ * be in a memory range that is a sub-range of that taken up by the owning object.
+ *
+ * Always takes up at least 8 KiB of space.
+ */
+template <size_t Guard4KPages>
+class InlineMemoryTrap {
+ static_assert(Guard4KPages > 0);
+ constexpr static size_t BufSize = 4096 * (Guard4KPages + 1);
+ char _trap_buf[BufSize];
+ MemoryRangeTrapper _trapper;
+public:
+ InlineMemoryTrap() noexcept : _trap_buf(), _trapper(_trap_buf, BufSize) {}
+ ~InlineMemoryTrap() = default;
+
+ InlineMemoryTrap(const InlineMemoryTrap&) = delete;
+ InlineMemoryTrap(InlineMemoryTrap&&) noexcept = delete;
+
+ // Exposed for testing only
+ const MemoryRangeTrapper& trapper() const noexcept { return _trapper; }
+};
+
+/**
+ * Allocates a 4 KiB-aligned heap buffer and watches it for spurious access.
+ * Useful for distributing traps across various allocation size-classes.
+ */
+class HeapMemoryTrap {
+ char* _trap_buf;
+ MemoryRangeTrapper _trapper;
+public:
+ explicit HeapMemoryTrap(size_t trap_4k_pages);
+ ~HeapMemoryTrap();
+
+ HeapMemoryTrap(const HeapMemoryTrap&) = delete;
+ HeapMemoryTrap(HeapMemoryTrap&&) noexcept = delete;
+
+ // Exposed for testing only
+ const MemoryRangeTrapper& trapper() const noexcept { return _trapper; }
+};
+
+}