-
Notifications
You must be signed in to change notification settings - Fork 5.5k
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 20 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,17 @@ | ||
| #pragma once | ||
|
|
||
| #include <cassert> | ||
| #include <cstdint> | ||
|
|
||
| namespace Envoy { | ||
| namespace Memory { | ||
|
|
||
| inline uint64_t align(uint64_t size, uint64_t alignment) { | ||
| // Check that alignment is a power of 2: | ||
| // http://www.graphics.stanford.edu/~seander/bithacks.html#DetermineIfPowerOf2 | ||
| assert((alignment > 0) && ((alignment & (alignment - 1)) == 0)); | ||
| return (size + alignment - 1) & ~(alignment - 1); | ||
| } | ||
|
|
||
| } // namespace Memory | ||
| } // namespace Envoy | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,118 @@ | ||
| // 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 DeadMarker1). 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 "common/memory/align.h" | ||
|
|
||
| static std::atomic<int64_t> bytes_allocated(0); | ||
|
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. Why signed?
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 was thinking that a negative value was possible if a block of memory was subtly corrupted before it was freed. However it's better to assert when that happens than let this go negative. I'll fix.
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 we also track
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. Who would consume that? The main reason I'm tracking this at all is so that the memory-constraining tests can run in debug mode. |
||
|
|
||
| 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 MEMORY_DEBUG_ENABLED | ||
|
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. ENVOY_MEMORY_DEBUG_ENABLED?
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. done |
||
|
|
||
| namespace { | ||
|
|
||
| constexpr uint32_t LiveMarker1 = 0xfeedface; // first 4 bytes after alloc | ||
|
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. Those names should all be uppercase.
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. From https://github.com/envoyproxy/envoy/blob/master/STYLE.md
|
||
| constexpr uint64_t LiveMarker2 = 0xfeedfacefeedface; // first 4 bytes after alloc | ||
|
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. Comment says 4 bytes, but this is 8 bytes long
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. done |
||
| constexpr uint32_t DeadMarker1 = 0xabacabff; // first 4 bytes after free | ||
|
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. I cannot comment on the original thread, so reviving it here.
Yes, the usefulness of
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 must have been in some distant debug scenario this distinction became interesting. But I can't pull it out of history, and so I'll just make there be one dead marker. |
||
| constexpr uint64_t DeadMarker2 = 0xdeadbeefdeadbeef; // overwrites the 'size' field on 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. | ||
| inline void scribble(void* ptr, uint64_t rounded_size, uint64_t scribble_word) { | ||
|
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. I don't think
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. done; I was thinking I might be able to convince the compiler to optimize out the assert on alignment; but it's likely not worth it, and the compiler may figure out how to do that anyway. |
||
| assert((rounded_size % Overhead) == 0); | ||
| uint64_t num_uint64s = rounded_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 rounded = Envoy::Memory::align(size, Overhead); | ||
|
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. Note: this can now return
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 still need to add the overhead so we can do the inverse accounting on free. And why special-case the call to scribble, which should happily does nothing in the degenerate case?
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. Nit: please rename
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. done |
||
| bytes_allocated += rounded; | ||
| uint32_t* marker = static_cast<uint32_t*>(::malloc(rounded + Overhead)); | ||
| assert(marker != NULL); | ||
| marker[0] = LiveMarker1; | ||
| marker[1] = size; | ||
| uint32_t* ret = marker + 2; | ||
|
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 is labeled
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. Actually I made it the returned value, but also renamed to 'payload'; wdyt?
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.
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. OK though I'm not sure if that's easier to read. The '2' is the logical progression from assigning marker[0] and then marker[1]; now we are working on what begins at index 2, which is the payload. |
||
| scribble(ret, rounded, LiveMarker2); | ||
| return reinterpret_cast<char*>(marker) + Overhead; | ||
| } | ||
|
|
||
| // 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); | ||
| uint32_t size = marker[1]; | ||
| uint64_t rounded = Envoy::Memory::align(size, Overhead); | ||
| bytes_allocated -= rounded; | ||
| scribble(ptr, rounded, DeadMarker2); | ||
| 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. |
||
| marker[0] = DeadMarker1; | ||
| marker[1] = DeadMarker2 & 0xffffffff; | ||
| ::free(marker); | ||
| } | ||
| } | ||
|
|
||
| } // namespace | ||
|
|
||
| void* operator new(uint64_t size) { return debugMalloc(size); } | ||
|
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. Shouldn't this be
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. urr, yes :) |
||
| void operator delete(void* ptr) noexcept { debugFree(ptr); } | ||
| void operator delete(void* ptr, size_t) noexcept { debugFree(ptr); } | ||
|
|
||
| void* operator new[](size_t size) { return debugMalloc(size); } | ||
| void operator delete[](void* ptr) noexcept { debugFree(ptr); } | ||
| void operator delete[](void* ptr, size_t) noexcept { debugFree(ptr); } | ||
|
|
||
| #endif // MEMORY_DEBUG_ENABLED | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| #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(); | ||
| }; | ||
|
|
||
| // 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 optimizd builds to avoid impacting | ||
|
jmarantz marked this conversation as resolved.
Outdated
|
||
| // production performance. | ||
| #ifndef NDEBUG | ||
|
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. I think, I'd prefer if users had to opt-in into this via
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. This is a healthy discussion to have...my motivation was to help people find memory bugs when they aren't looking for them. If they are looking for them they can use valgrind which is superior but onerous. However, as I explore using this in different compile modes, I am also running into trouble with what looks to me to be inconsistent usage of operator new/delete in protobufs. This results in segv before main in these contexts, whereas in others it works great. At least until all the build/run modes are working, I think this suggestion makes sense and I'll have this off-by-default. I re-worked the build files around this strategy. WDYT? |
||
|
|
||
| // 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. | ||
| // | ||
| // We also must disable memory debugging for tsan/asan builds, as they also | ||
| // need to override operator new/delete. | ||
| #if !defined(TCMALLOC) && !defined(ENVOY_DISABLE_MEMDEBUG) | ||
|
|
||
| #define MEMORY_DEBUG_ENABLED 1 | ||
|
|
||
| #endif // !TCMALLOC && !ENVOY_DISABLE_MEMDEBUG | ||
| #endif // !NDEBUG | ||
|
|
||
| } // namespace Memory | ||
| } // namespace Envoy | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -45,10 +45,12 @@ uint64_t Stats::totalPageHeapUnmapped() { | |
|
|
||
| #else | ||
|
|
||
| #include "common/memory/debug.h" | ||
|
|
||
| namespace Envoy { | ||
| namespace Memory { | ||
|
|
||
| uint64_t Stats::totalCurrentlyAllocated() { return 0; } | ||
| uint64_t Stats::totalCurrentlyAllocated() { return Debug::bytesUsed(); } | ||
|
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. I'd slightly prefer to have this whole block duplicated, so that we have: instead of adding code to the build without debug memory.
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. Good point...I reorganized it a bit; ptal. |
||
| uint64_t Stats::totalThreadCacheBytes() { return 0; } | ||
| uint64_t Stats::totalCurrentlyReserved() { return 0; } | ||
| uint64_t Stats::totalPageHeapUnmapped() { return 0; } | ||
|
|
||
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.
ASSERT or RELEASE_ASSERT?
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.
I was trying to avoid assertion layers that might allocate memory in the context of a utility designed to help with memory allocation. Although actually the right thing here is to use static_assert, which I can do with a little template magic.