-
Notifications
You must be signed in to change notification settings - Fork 5.3k
memory: Add memory-debug scribbling when tcmalloc is disabled and not compiled for optimization #5450
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
memory: Add memory-debug scribbling when tcmalloc is disabled and not compiled for optimization #5450
Changes from all commits
a5d2e9a
c5ab27c
f608251
68668a6
cf1ff7e
c20d7cd
d2d196b
324d7c2
8f4dd93
a3c0b54
a37ebbb
bd0d593
32b8d0d
3fe801c
6282866
4022204
a5303ef
efdfc93
0da923e
a612677
d532232
664c08a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| #pragma once | ||
|
|
||
| #include <cstdint> | ||
| #include <type_traits> | ||
|
|
||
| namespace Envoy { | ||
| namespace Memory { | ||
|
|
||
| template <uint64_t alignment> inline uint64_t align(uint64_t size) { | ||
| // Check that alignment is a power of 2: | ||
| // http://www.graphics.stanford.edu/~seander/bithacks.html#DetermineIfPowerOf2 | ||
| static_assert((alignment > 0) && ((alignment & (alignment - 1)) == 0), | ||
| "alignment must be a power of 2"); | ||
| return (size + alignment - 1) & ~(alignment - 1); | ||
| } | ||
|
|
||
| } // namespace Memory | ||
| } // namespace Envoy |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,151 @@ | ||
| // Very simple memory debugging overrides for operator new/delete, to | ||
| // help us quickly find simple memory violations: | ||
| // 1. Double destruct | ||
| // 2. Read before write (via scribbling) | ||
| // 3. Read after delete (via scribbling) | ||
| // | ||
| // Note that valgrind does all of this much better, but is too slow to run all | ||
| // the time. asan does read-after-delete detection but not read-before-init | ||
| // detection. See | ||
| // https://clang.llvm.org/docs/AddressSanitizer.html#initialization-order-checking | ||
| // for more details. | ||
|
|
||
| // Principle of operation: add 8 bytes to every allocation. The first | ||
| // 4 bytes are a marker (LiveMarker1 or DeadMarker). The next 4 | ||
| // bytes are used to store size of the allocation, which helps us | ||
| // know how many bytes to scribble when we free. | ||
| // | ||
| // This code was adapted from mod_pagespeed, and adapted for Envoy | ||
| // style. Original source: | ||
| // https://github.com/apache/incubator-pagespeed-mod/blob/master/pagespeed/kernel/base/mem_debug.cc | ||
|
|
||
| // We keep a global count of bytes allocated so that memory-consumption tests | ||
| // work with memory debugging. Put another way, if we disable tcmalloc when | ||
| // compiling for debug, we want the memory-debugging tests to work, otherwise we | ||
| // can't debug them. | ||
| #include "common/memory/debug.h" | ||
|
|
||
| #include <atomic> | ||
| #include <cassert> // don't use Envoy ASSERT as it may allocate memory. | ||
| #include <cstdlib> | ||
| #include <new> | ||
|
|
||
| #include "common/memory/align.h" | ||
|
|
||
| static std::atomic<uint64_t> bytes_allocated(0); | ||
|
|
||
| namespace Envoy { | ||
| namespace Memory { | ||
|
|
||
| // We always provide the constructor entry-point to be called to force-load this | ||
| // module, regardless of compilation mode. If the #ifdefs line as required | ||
| // below, then it will also override operator new/delete in various flavors so | ||
| // that we can debug memory issues. | ||
| Debug::Debug() = default; | ||
|
|
||
| // We also provide the bytes-loaded counter, though this will return 0 when | ||
| // memory-debugging is not compiled in. | ||
| uint64_t Debug::bytesUsed() { return uint64_t(bytes_allocated); } | ||
|
|
||
| } // namespace Memory | ||
| } // namespace Envoy | ||
|
|
||
| #ifdef ENVOY_MEMORY_DEBUG_ENABLED | ||
|
|
||
| // Centralized ifdef logic to determine whether this compile has memory | ||
| // debugging. This is exposed in the header file for testing. | ||
|
|
||
| // We don't run memory debugging for optimized builds to avoid impacting | ||
| // production performance. | ||
| #ifdef NDEBUG | ||
| #error Memory debugging should not be enabled for production builds | ||
| #endif | ||
|
|
||
| // We can't run memory debugging with tcmalloc due to conflicts with | ||
| // overriding operator new/delete. Note tcmalloc allows installation | ||
| // of a malloc hook (MallocHook::AddNewHook(&tcmallocHook)) e.g. | ||
| // tcmallocHook(const void* ptr, size_t size). I tried const_casting ptr | ||
| // and scribbling over it, but this results in a SEGV in grpc and the | ||
| // internals of gtest. | ||
| // | ||
| // And in any case, you can't use the tcmalloc hooks to do free-scribbling | ||
| // as it does not pass in the size to the corresponding free hook. See | ||
| // gperftools/malloc_hook.h for details. | ||
| #ifdef TCMALLOC | ||
| #error Memory debugging cannot be enabled with tcmalloc. | ||
| #endif | ||
|
|
||
| #ifdef ENVOY_TSAN_BUIOD | ||
| #error Memory debugging cannot be enabled with tsan. | ||
| #endif | ||
|
|
||
| #ifdef ENVOY_ASAN_BUILD | ||
| #error Memory debugging cannot be enabled with asan. | ||
| #endif | ||
|
|
||
| namespace { | ||
|
|
||
| constexpr uint32_t LiveMarker1 = 0xfeedface; // first 4 bytes after alloc | ||
| constexpr uint64_t LiveMarker2 = 0xfeedfacefeedface; // pattern written into allocated memory | ||
| constexpr uint64_t DeadMarker = 0xdeadbeefdeadbeef; // pattern to scribble over memory before free | ||
| constexpr uint64_t Overhead = sizeof(uint64_t); // number of extra bytes to alloc | ||
|
|
||
| // Writes scribble_word over the block of memory starting at ptr and extending | ||
| // size bytes. | ||
| void scribble(void* ptr, uint64_t aligned_size, uint64_t scribble_word) { | ||
| assert((aligned_size % Overhead) == 0); | ||
| uint64_t num_uint64s = aligned_size / sizeof(uint64_t); | ||
| uint64_t* p = static_cast<uint64_t*>(ptr); | ||
| for (uint64_t i = 0; i < num_uint64s; ++i, ++p) { | ||
| *p = scribble_word; | ||
| } | ||
| } | ||
|
|
||
| // Replacement allocator, which prepends an 8-byte overhead where we write the | ||
| // size, and scribbles over the returned payload so that callers assuming | ||
| // malloced memory is 0 get data that, when interpreted as pointers, will SEGV, | ||
| // and that will be easily seen in the debugger (0xfeedface pattern). | ||
| void* debugMalloc(uint64_t size) { | ||
| assert(size <= 0xffffffff); // For now we store the original size in a uint32_t. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This makes me nervous.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It wouldn't be hard to use all 8 bytes in the marker to store the size. But I'm curious why it makes you nervous? Do you think -- especially in a testing/debugging contest, that we are likely to see 4G requests to malloc? I would imagine that would not behave well, and it might not be a bad idea to get an assert fail while debugging or testing to become aware of this. If that's OK with you I'll document the assert better. If you'd prefer to allow >=4G allocations I can change it. |
||
| uint64_t aligned_size = Envoy::Memory::align<Overhead>(size); | ||
| bytes_allocated += aligned_size; | ||
| uint32_t* marker = static_cast<uint32_t*>(::malloc(aligned_size + Overhead)); | ||
| assert(marker != NULL); | ||
| marker[0] = LiveMarker1; | ||
| marker[1] = size; | ||
| uint32_t* payload = marker + sizeof(Overhead) / sizeof(uint32_t); | ||
| scribble(payload, aligned_size, LiveMarker2); | ||
| return payload; | ||
| } | ||
|
|
||
| // free() implementation corresponding to debugMalloc(), which pulls out | ||
| // The size from the 8 bytes prior to the payload, so it can know how much | ||
| // 0xdeadbeef to scribble over the freed memory before calling actual free(). | ||
| void debugFree(void* ptr) { | ||
| if (ptr != NULL) { | ||
| char* alloced_ptr = static_cast<char*>(ptr) - Overhead; | ||
| uint32_t* marker = reinterpret_cast<uint32_t*>(alloced_ptr); | ||
| assert(LiveMarker1 == marker[0]); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you move this check right after assigning
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I moved it to after the assignment of marker. I'm not sure what you meant in the second half of your suggestion. |
||
| uint32_t size = marker[1]; | ||
| uint64_t aligned_size = Envoy::Memory::align<Overhead>(size); | ||
| assert(bytes_allocated >= aligned_size); | ||
| bytes_allocated -= aligned_size; | ||
| scribble(marker, aligned_size + sizeof(Overhead), DeadMarker); | ||
| ::free(marker); | ||
| } | ||
| } | ||
|
|
||
| } // namespace | ||
|
|
||
| void* operator new(size_t size) { return debugMalloc(size); } | ||
| void* operator new(size_t size, const std::nothrow_t&) noexcept { return debugMalloc(size); } | ||
| void operator delete(void* ptr) noexcept { debugFree(ptr); } | ||
| void operator delete(void* ptr, size_t) noexcept { debugFree(ptr); } | ||
| void operator delete(void* ptr, std::nothrow_t const&)noexcept { debugFree(ptr); } | ||
|
|
||
| void* operator new[](size_t size) { return debugMalloc(size); } | ||
| void* operator new[](size_t size, const std::nothrow_t&) noexcept { return debugMalloc(size); } | ||
| void operator delete[](void* ptr) noexcept { debugFree(ptr); } | ||
| void operator delete[](void* ptr, size_t) noexcept { debugFree(ptr); } | ||
|
|
||
| #endif // ENVOY_MEMORY_DEBUG_ENABLED | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| #pragma once | ||
|
|
||
| #include <cstdint> | ||
|
|
||
| namespace Envoy { | ||
| namespace Memory { | ||
|
|
||
| class Debug { | ||
| public: | ||
| // Instantiate to force-load the memory debugging module. This is called | ||
| // whether or not memory-debugging is enabled, which is controlled by ifdefs | ||
| // in mem_debug.cc. | ||
| Debug(); | ||
|
|
||
| // Returns the number of bytes used -- if memory debugging is enabled. | ||
| // Otherwise returns 0. | ||
| static uint64_t bytesUsed(); | ||
| }; | ||
|
|
||
| } // namespace Memory | ||
| } // namespace Envoy |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Those names should all be uppercase.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From https://github.com/envoyproxy/envoy/blob/master/STYLE.md