diff options
author | Henning Baldersheim <balder@yahoo-inc.com> | 2023-01-11 16:20:49 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-01-11 16:20:49 +0100 |
commit | 6c25f7df57f19e260730aa6dda35863fcb07e9b2 (patch) | |
tree | 342c83347e82ae9a2e9776903f52d4e3fb759d82 /vespalib | |
parent | 26ec76efc8e5d9d4d6b52d7f7cb6de059effa012 (diff) | |
parent | c293e7a78a92d72d44f1426a9a0a4de306c9b914 (diff) |
Merge pull request #25507 from vespa-engine/vekterli/add-memory-trap-util
Add utility functionality for trapping memory accesses
Diffstat (limited to 'vespalib')
-rw-r--r-- | vespalib/CMakeLists.txt | 1 | ||||
-rw-r--r-- | vespalib/src/tests/util/memory_trap/CMakeLists.txt | 9 | ||||
-rw-r--r-- | vespalib/src/tests/util/memory_trap/memory_trap_test.cpp | 61 | ||||
-rw-r--r-- | vespalib/src/vespa/vespalib/util/CMakeLists.txt | 1 | ||||
-rw-r--r-- | vespalib/src/vespa/vespalib/util/memory_trap.cpp | 166 | ||||
-rw-r--r-- | vespalib/src/vespa/vespalib/util/memory_trap.h | 101 |
6 files changed, 339 insertions, 0 deletions
diff --git a/vespalib/CMakeLists.txt b/vespalib/CMakeLists.txt index 2ceb56bf226..8509d5fc382 100644 --- a/vespalib/CMakeLists.txt +++ b/vespalib/CMakeLists.txt @@ -194,6 +194,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..d3b666d9a6e --- /dev/null +++ b/vespalib/src/vespa/vespalib/util/memory_trap.cpp @@ -0,0 +1,166 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#include "memory_trap.h" +#include <string_view> +#include <cassert> +#include <cerrno> +#include <cstdint> +#include <cstdlib> +#include <cstring> +#include <malloc.h> +#include <unistd.h> +#include <sys/mman.h> + +#include <vespa/log/log.h> +LOG_SETUP(".vespalib.util.memory_trap"); + +using namespace std::string_view_literals; + +namespace vespalib { + +namespace { + +// 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::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::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(); + } + } + } +} + +#ifdef __linux__ + +namespace { + +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; +} + +} // anon ns + +void MemoryRangeTrapper::rw_protect_buffer_if_possible() { + 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; + } + } +} + +bool MemoryRangeTrapper::hw_trapping_enabled() noexcept { + return mprotect_trapping_is_enabled(); +} + +void MemoryRangeTrapper::unprotect_buffer_to_read_only() { + 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"); + } +} + +void MemoryRangeTrapper::unprotect_buffer_to_read_and_write() { + 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"); + } +} + +#else // Not on Linux, fall back to no-ops + +void MemoryRangeTrapper::rw_protect_buffer_if_possible() { /* no-op */ } +bool MemoryRangeTrapper::hw_trapping_enabled() noexcept { return false; } +void MemoryRangeTrapper::unprotect_buffer_to_read_only() { /* no-op */ } +void MemoryRangeTrapper::unprotect_buffer_to_read_and_write() { /* no-op */ } + +#endif + +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; } +}; + +} |