diff --git a/src/hotspot/share/runtime/os.cpp b/src/hotspot/share/runtime/os.cpp index d3e9d44565f..2abee47c5e1 100644 --- a/src/hotspot/share/runtime/os.cpp +++ b/src/hotspot/share/runtime/os.cpp @@ -1202,6 +1202,11 @@ void os::print_location(outputStream* st, intptr_t x, bool verbose) { } #endif + // Still nothing? If NMT is enabled, we can ask what it thinks... + if (MemTracker::print_containing_region(addr, st)) { + return; + } + // Try an OS specific find if (os::find(addr, st)) { return; diff --git a/src/hotspot/share/services/mallocHeader.hpp b/src/hotspot/share/services/mallocHeader.hpp index 1257e4380b5..c376c130e9d 100644 --- a/src/hotspot/share/services/mallocHeader.hpp +++ b/src/hotspot/share/services/mallocHeader.hpp @@ -96,11 +96,11 @@ class MallocHeader { const uint8_t _unused; uint16_t _canary; - static const uint16_t _header_canary_life_mark = 0xE99E; + static const uint16_t _header_canary_live_mark = 0xE99E; static const uint16_t _header_canary_dead_mark = 0xD99D; - static const uint16_t _footer_canary_life_mark = 0xE88E; + static const uint16_t _footer_canary_live_mark = 0xE88E; static const uint16_t _footer_canary_dead_mark = 0xD88D; - NOT_LP64(static const uint32_t _header_alt_canary_life_mark = 0xE99EE99E;) + NOT_LP64(static const uint32_t _header_alt_canary_live_mark = 0xE99EE99E;) NOT_LP64(static const uint32_t _header_alt_canary_dead_mark = 0xD88DD88D;) // We discount sizes larger than these @@ -139,6 +139,13 @@ public: inline void mark_block_as_dead(); inline void revive(); + + bool is_dead() const { return _canary == _header_canary_dead_mark; } + bool is_live() const { return _canary == _header_canary_live_mark; } + + // Used for debugging purposes only. Check header if it could constitute a valid (live or dead) header. + inline bool looks_valid() const; + // If block is broken, fill in a short descriptive text in out, // an option pointer to the corruption in p_corruption, and return false. // Return true if block is fine. diff --git a/src/hotspot/share/services/mallocHeader.inline.hpp b/src/hotspot/share/services/mallocHeader.inline.hpp index 26d94b03699..669438c85fa 100644 --- a/src/hotspot/share/services/mallocHeader.inline.hpp +++ b/src/hotspot/share/services/mallocHeader.inline.hpp @@ -36,22 +36,22 @@ inline MallocHeader::MallocHeader(size_t size, MEMFLAGS flags, uint32_t mst_marker) : _size(size), _mst_marker(mst_marker), _flags(flags), - _unused(0), _canary(_header_canary_life_mark) + _unused(0), _canary(_header_canary_live_mark) { assert(size < max_reasonable_malloc_size, "Too large allocation size?"); // On 32-bit we have some bits more, use them for a second canary // guarding the start of the header. - NOT_LP64(_alt_canary = _header_alt_canary_life_mark;) - set_footer(_footer_canary_life_mark); // set after initializing _size + NOT_LP64(_alt_canary = _header_alt_canary_live_mark;) + set_footer(_footer_canary_live_mark); // set after initializing _size } inline void MallocHeader::revive() { assert(_canary == _header_canary_dead_mark, "must be dead"); assert(get_footer() == _footer_canary_dead_mark, "must be dead"); NOT_LP64(assert(_alt_canary == _header_alt_canary_dead_mark, "must be dead")); - _canary = _header_canary_life_mark; - NOT_LP64(_alt_canary = _header_alt_canary_life_mark); - set_footer(_footer_canary_life_mark); + _canary = _header_canary_live_mark; + NOT_LP64(_alt_canary = _header_alt_canary_live_mark); + set_footer(_footer_canary_live_mark); } // The effects of this method must be reversible with MallocHeader::revive() @@ -116,12 +116,22 @@ inline const MallocHeader* MallocHeader::resolve_checked(const void* memblock) { return MallocHeader::resolve_checked_impl(memblock); } + +// Used for debugging purposes only. Check header if it could constitute a valid (live or dead) header. +inline bool MallocHeader::looks_valid() const { + // Note: we define these restrictions loose enough to also catch moderately corrupted blocks. + // E.g. we don't check footer canary. + return ( (_canary == _header_canary_live_mark NOT_LP64(&& _alt_canary == _header_alt_canary_live_mark)) || + (_canary == _header_canary_dead_mark NOT_LP64(&& _alt_canary == _header_alt_canary_dead_mark)) ) && + _size > 0 && _size < max_reasonable_malloc_size; +} + inline bool MallocHeader::check_block_integrity(char* msg, size_t msglen, address* p_corruption) const { // Note: if you modify the error messages here, make sure you // adapt the associated gtests too. // Check header canary - if (_canary != _header_canary_life_mark) { + if (_canary != _header_canary_live_mark) { *p_corruption = (address)this; jio_snprintf(msg, msglen, "header canary broken"); return false; @@ -129,7 +139,7 @@ inline bool MallocHeader::check_block_integrity(char* msg, size_t msglen, addres #ifndef _LP64 // On 32-bit we have a second canary, check that one too. - if (_alt_canary != _header_alt_canary_life_mark) { + if (_alt_canary != _header_alt_canary_live_mark) { *p_corruption = (address)this; jio_snprintf(msg, msglen, "header canary broken"); return false; @@ -144,7 +154,7 @@ inline bool MallocHeader::check_block_integrity(char* msg, size_t msglen, addres } // Check footer canary - if (get_footer() != _footer_canary_life_mark) { + if (get_footer() != _footer_canary_live_mark) { *p_corruption = footer_address(); jio_snprintf(msg, msglen, "footer canary broken at " PTR_FORMAT " (buffer overflow?)", p2i(footer_address())); diff --git a/src/hotspot/share/services/mallocTracker.cpp b/src/hotspot/share/services/mallocTracker.cpp index 7277fba133c..cd3e51b9f0a 100644 --- a/src/hotspot/share/services/mallocTracker.cpp +++ b/src/hotspot/share/services/mallocTracker.cpp @@ -1,6 +1,8 @@ /* * Copyright (c) 2014, 2023, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2021, 2023 SAP SE. All rights reserved. + * Copyright (c) 2023, Red Hat, Inc. and/or its affiliates. + * * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -190,31 +192,89 @@ void MallocTracker::deaccount(MallocHeader::FreeInfo free_info) { } } -// Given a pointer, if it seems to point to the start of a valid malloced block, -// print the block. Note that since there is very low risk of memory looking +// Given a pointer, look for the containing malloc block. +// Print the block. Note that since there is very low risk of memory looking // accidentally like a valid malloc block header (canaries and all) this is not // totally failproof. Only use this during debugging or when you can afford // signals popping up, e.g. when writing an hs_err file. bool MallocTracker::print_pointer_information(const void* p, outputStream* st) { - assert(MemTracker::enabled(), "NMT must be enabled"); - if (os::is_readable_pointer(p)) { - const NMT_TrackingLevel tracking_level = MemTracker::tracking_level(); - const MallocHeader* mhdr = malloc_header(p); - char msg[256]; - address p_corrupted; - if (os::is_readable_pointer(mhdr) && - mhdr->check_block_integrity(msg, sizeof(msg), &p_corrupted)) { - st->print_cr(PTR_FORMAT " malloc'd " SIZE_FORMAT " bytes by %s", - p2i(p), mhdr->size(), NMTUtil::flag_to_name(mhdr->flags())); - if (tracking_level == NMT_detail) { - NativeCallStack ncs; - if (mhdr->get_stack(ncs)) { - ncs.print_on(st); - st->cr(); + assert(MemTracker::enabled(), "NMT not enabled"); + + address addr = (address)p; + + // Carefully feel your way upwards and try to find a malloc header. Then check if + // we are within the block. + // We give preference to found live blocks; but if no live block had been found, + // but the pointer points into remnants of a dead block, print that instead. + const MallocHeader* likely_dead_block = nullptr; + const MallocHeader* likely_live_block = nullptr; + { + const size_t smallest_possible_alignment = sizeof(void*); + const uint8_t* here = align_down(addr, smallest_possible_alignment); + const uint8_t* const end = here - (0x1000 + sizeof(MallocHeader)); // stop searching after 4k + for (; here >= end; here -= smallest_possible_alignment) { + if (!os::is_readable_pointer(here)) { + // Probably OOB, give up + return false; + } + const MallocHeader* const candidate = (const MallocHeader*)here; + if (!candidate->looks_valid()) { + // This is definitely not a header, go on to the next candidate. + continue; + } + + // fudge factor: + // We don't report blocks for which p is clearly outside of. That would cause us to return true and possibly prevent + // subsequent tests of p, see os::print_location(). But if p is just outside of the found block, this may be a + // narrow oob error and we'd like to know that. + const int fudge = 8; + const address start_block = (address)candidate; + const address start_payload = (address)(candidate + 1); + const address end_payload = start_payload + candidate->size(); + const address end_payload_plus_fudge = end_payload + fudge; + if (addr >= start_block && addr < end_payload_plus_fudge) { + // We found a block the pointer is pointing into, or almost into. + // If its a live block, we have our info. If its a dead block, we still + // may be within the borders of a larger live block we have not found yet - + // continue search. + if (candidate->is_live()) { + likely_live_block = candidate; + break; + } else { + likely_dead_block = candidate; + continue; } } - return true; } } + + // If we've found a reasonable candidate. Print the info. + const MallocHeader* block = likely_live_block != nullptr ? likely_live_block : likely_dead_block; + if (block != nullptr) { + const char* where = nullptr; + const address start_block = (address)block; + const address start_payload = (address)(block + 1); + const address end_payload = start_payload + block->size(); + if (addr < start_payload) { + where = "into header of"; + } else if (addr < end_payload) { + where = "into"; + } else { + where = "just outside of"; + } + st->print_cr(PTR_FORMAT " %s %s malloced block starting at " PTR_FORMAT ", size " SIZE_FORMAT ", tag %s", + p2i(p), where, + (block->is_dead() ? "dead" : "live"), + p2i(block + 1), // lets print the payload start, not the header + block->size(), NMTUtil::flag_to_enum_name(block->flags())); + if (MemTracker::tracking_level() == NMT_detail) { + NativeCallStack ncs; + if (block->get_stack(ncs)) { + ncs.print_on(st); + st->cr(); + } + } + return true; + } return false; } diff --git a/src/hotspot/share/services/mallocTracker.hpp b/src/hotspot/share/services/mallocTracker.hpp index 80acc7e1854..f4f824bb07c 100644 --- a/src/hotspot/share/services/mallocTracker.hpp +++ b/src/hotspot/share/services/mallocTracker.hpp @@ -308,8 +308,8 @@ class MallocTracker : AllStatic { // under category f would hit either the global limit or the limit for category f. static inline bool check_exceeds_limit(size_t s, MEMFLAGS f); - // Given a pointer, if it seems to point to the start of a valid malloced block, - // print the block. Note that since there is very low risk of memory looking + // Given a pointer, look for the containing malloc block. + // Print the block. Note that since there is very low risk of memory looking // accidentally like a valid malloc block header (canaries and all) this is not // totally failproof. Only use this during debugging or when you can afford // signals popping up, e.g. when writing an hs_err file. diff --git a/src/hotspot/share/services/memTracker.cpp b/src/hotspot/share/services/memTracker.cpp index 2ea8cb8b2e5..fa2f78abc43 100644 --- a/src/hotspot/share/services/memTracker.cpp +++ b/src/hotspot/share/services/memTracker.cpp @@ -132,6 +132,14 @@ void MemTracker::final_report(outputStream* output) { } } +// Given an unknown pointer, check if it points into a known region; print region if found +// and return true; false if not found. +bool MemTracker::print_containing_region(const void* p, outputStream* out) { + return enabled() && + (MallocTracker::print_pointer_information(p, out) || + VirtualMemoryTracker::print_containing_region(p, out)); +} + void MemTracker::report(bool summary_only, outputStream* output, size_t scale) { assert(output != nullptr, "No output stream"); MemBaseline baseline; diff --git a/src/hotspot/share/services/memTracker.hpp b/src/hotspot/share/services/memTracker.hpp index e9f2ea0bfbd..fb687c202b0 100644 --- a/src/hotspot/share/services/memTracker.hpp +++ b/src/hotspot/share/services/memTracker.hpp @@ -234,6 +234,10 @@ class MemTracker : AllStatic { // under category f would hit either the global limit or the limit for category f. static inline bool check_exceeds_limit(size_t s, MEMFLAGS f); + // Given an unknown pointer, check if it points into a known region; print region if found + // and return true; false if not found. + static bool print_containing_region(const void* p, outputStream* out); + private: static void report(bool summary_only, outputStream* output, size_t scale); diff --git a/src/hotspot/share/services/virtualMemoryTracker.cpp b/src/hotspot/share/services/virtualMemoryTracker.cpp index 7138b916fba..66fdb236256 100644 --- a/src/hotspot/share/services/virtualMemoryTracker.cpp +++ b/src/hotspot/share/services/virtualMemoryTracker.cpp @@ -679,8 +679,8 @@ public: bool do_allocation_site(const ReservedMemoryRegion* rgn) { if (rgn->contain_address(_p)) { - _st->print_cr(PTR_FORMAT " in mmap'd memory region [" PTR_FORMAT " - " PTR_FORMAT "] by %s", - p2i(_p), p2i(rgn->base()), p2i(rgn->base() + rgn->size()), rgn->flag_name()); + _st->print_cr(PTR_FORMAT " in mmap'd memory region [" PTR_FORMAT " - " PTR_FORMAT "], tag %s", + p2i(_p), p2i(rgn->base()), p2i(rgn->base() + rgn->size()), NMTUtil::flag_to_enum_name(rgn->flag())); if (MemTracker::tracking_level() == NMT_detail) { rgn->call_stack()->print_on(_st); _st->cr(); diff --git a/src/hotspot/share/utilities/debug.cpp b/src/hotspot/share/utilities/debug.cpp index e1fbe5fe672..4fd3b48456e 100644 --- a/src/hotspot/share/utilities/debug.cpp +++ b/src/hotspot/share/utilities/debug.cpp @@ -398,15 +398,8 @@ extern "C" JNIEXPORT void pp(void* p) { // catch the signal and disable the pp() command for further use. // In order to avoid that, switch off SIGSEGV handling with "handle SIGSEGV nostop" before // invoking pp() - if (MemTracker::enabled()) { - // Does it point into a known mmapped region? - if (VirtualMemoryTracker::print_containing_region(p, tty)) { - return; - } - // Does it look like the start of a malloced block? - if (MallocTracker::print_pointer_information(p, tty)) { - return; - } + if (MemTracker::print_containing_region(p, tty)) { + return; } tty->print_cr(PTR_FORMAT, p2i(p)); } diff --git a/src/hotspot/share/utilities/vmError.cpp b/src/hotspot/share/utilities/vmError.cpp index d3c2f542b7a..e5c9f4c66a6 100644 --- a/src/hotspot/share/utilities/vmError.cpp +++ b/src/hotspot/share/utilities/vmError.cpp @@ -33,6 +33,7 @@ #include "gc/shared/gcLogPrecious.hpp" #include "jvm.h" #include "logging/logConfiguration.hpp" +#include "memory/allocation.hpp" #include "memory/metaspace.hpp" #include "memory/metaspaceUtils.hpp" #include "memory/resourceArea.inline.hpp" diff --git a/test/hotspot/gtest/nmt/test_nmt_locationprinting.cpp b/test/hotspot/gtest/nmt/test_nmt_locationprinting.cpp new file mode 100644 index 00000000000..281110674ac --- /dev/null +++ b/test/hotspot/gtest/nmt/test_nmt_locationprinting.cpp @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2023, Red Hat, Inc. and/or its affiliates. + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code 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 GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#include "precompiled.hpp" +#include "memory/allocation.hpp" +#include "runtime/os.hpp" +#include "services/mallocHeader.inline.hpp" +#include "services/memTracker.hpp" +#include "unittest.hpp" + +// Uncomment to get test output +//#define LOG_PLEASE +#include "testutils.hpp" + +using ::testing::HasSubstr; + +static void test_pointer(const void* p, bool expected_return_code, const char* expected_message) { + stringStream ss; + const bool b = MemTracker::print_containing_region(p, &ss); + LOG_HERE("MemTracker::print_containing_region(" PTR_FORMAT ") yielded: %d \"%s\"", p2i(p), b, ss.base()); + EXPECT_EQ(b, expected_return_code); + if (b) { + EXPECT_THAT(ss.base(), HasSubstr(expected_message)); + } +} + +static void test_for_live_c_heap_block(size_t sz, ssize_t offset) { + char* c = NEW_C_HEAP_ARRAY(char, sz, mtTest); + LOG_HERE("C-block starts " PTR_FORMAT ", size " SIZE_FORMAT ".", p2i(c), offset); + memset(c, 0, sz); + if (MemTracker::enabled()) { + const char* expected_string = "into live malloced block"; + if (offset < 0) { + expected_string = "into header of live malloced block"; + } else if ((size_t)offset >= sz) { + expected_string = "just outside of live malloced block"; + } + test_pointer(c + offset, true, expected_string); + } else { + // NMT disabled: we should see nothing. + test_pointer(c + offset, false, ""); + } + FREE_C_HEAP_ARRAY(char, c); +} + +static void test_for_dead_c_heap_block(size_t sz, ssize_t offset) { + if (!MemTracker::enabled()) { + return; + } + char* c = NEW_C_HEAP_ARRAY(char, sz, mtTest); + LOG_HERE("C-block starts " PTR_FORMAT ", size " SIZE_FORMAT ".", p2i(c), offset); + memset(c, 0, sz); + // We cannot just free the allocation to try dead block printing, since the memory + // may be immediately reused by concurrent code. Instead, we mark the block as dead + // manually, and revert that before freeing it. + MallocHeader* const hdr = MallocHeader::resolve_checked(c); + hdr->mark_block_as_dead(); + + const char* expected_string = "into dead malloced block"; + if (offset < 0) { + expected_string = "into header of dead malloced block"; + } else if ((size_t)offset >= sz) { + expected_string = "just outside of dead malloced block"; + } + + test_pointer(c + offset, true, expected_string); + + hdr->revive(); + FREE_C_HEAP_ARRAY(char, c); +} + +TEST_VM(NMT, location_printing_cheap_live_1) { test_for_live_c_heap_block(2 * K, 0); } // start of payload +TEST_VM(NMT, location_printing_cheap_live_2) { test_for_live_c_heap_block(2 * K, -7); } // into header +TEST_VM(NMT, location_printing_cheap_live_3) { test_for_live_c_heap_block(2 * K, K + 1); } // into payload +TEST_VM(NMT, location_printing_cheap_live_4) { test_for_live_c_heap_block(2 * K, K + 2); } // into payload (check for even/odd errors) +TEST_VM(NMT, location_printing_cheap_live_5) { test_for_live_c_heap_block(2 * K + 1, 2 * K + 2); } // just outside payload +TEST_VM(NMT, location_printing_cheap_live_6) { test_for_live_c_heap_block(4, 0); } // into a very small block +TEST_VM(NMT, location_printing_cheap_live_7) { test_for_live_c_heap_block(4, 4); } // just outside a very small block + +#ifdef LINUX +TEST_VM(NMT, location_printing_cheap_dead_1) { test_for_dead_c_heap_block(2 * K, 0); } // start of payload +TEST_VM(NMT, location_printing_cheap_dead_2) { test_for_dead_c_heap_block(2 * K, -7); } // into header +TEST_VM(NMT, location_printing_cheap_dead_3) { test_for_dead_c_heap_block(2 * K, K + 1); } // into payload +TEST_VM(NMT, location_printing_cheap_dead_4) { test_for_dead_c_heap_block(2 * K, K + 2); } // into payload (check for even/odd errors) +TEST_VM(NMT, location_printing_cheap_dead_5) { test_for_dead_c_heap_block(2 * K + 1, 2 * K + 2); } // just outside payload +TEST_VM(NMT, location_printing_cheap_dead_6) { test_for_dead_c_heap_block(4, 0); } // into a very small block +TEST_VM(NMT, location_printing_cheap_dead_7) { test_for_dead_c_heap_block(4, 4); } // just outside a very small block +#endif + +static void test_for_mmap(size_t sz, ssize_t offset) { + char* addr = os::reserve_memory(sz, false, mtTest); + if (MemTracker::enabled()) { + test_pointer(addr + offset, true, "in mmap'd memory region"); + } else { + // NMT disabled: we should see nothing. + test_pointer(addr + offset, false, ""); + } + os::release_memory(addr, os::vm_page_size()); +} + +TEST_VM(NMT, location_printing_mmap_1) { test_for_mmap(os::vm_page_size(), 0); } +TEST_VM(NMT, location_printing_mmap_2) { test_for_mmap(os::vm_page_size(), os::vm_page_size() - 1); } diff --git a/test/hotspot/gtest/nmt/test_nmt_totals.cpp b/test/hotspot/gtest/nmt/test_nmt_totals.cpp index c087e4f5cf7..9f1319f4f04 100644 --- a/test/hotspot/gtest/nmt/test_nmt_totals.cpp +++ b/test/hotspot/gtest/nmt/test_nmt_totals.cpp @@ -30,8 +30,8 @@ #include "unittest.hpp" // convenience log. switch on if debugging tests. Don't use tty, plain stdio only. -#define LOG(...) { printf(__VA_ARGS__); printf("\n"); fflush(stdout); } -//#define LOG(...) +//#define LOG(...) { printf(__VA_ARGS__); printf("\n"); fflush(stdout); } +#define LOG(...) static size_t get_total_malloc_invocs() { return MallocMemorySummary::as_snapshot()->total_count(); diff --git a/test/hotspot/gtest/testutils.hpp b/test/hotspot/gtest/testutils.hpp index 23428a6526b..6b3d42f1127 100644 --- a/test/hotspot/gtest/testutils.hpp +++ b/test/hotspot/gtest/testutils.hpp @@ -62,6 +62,10 @@ public: #define ASSERT_ALIGN(p, n) ASSERT_TRUE(is_aligned(p, n)) +#ifdef LOG_PLEASE #define LOG_HERE(s, ...) { printf(s, __VA_ARGS__); printf("\n"); fflush(stdout); } +#else +#define LOG_HERE(s, ...) +#endif #endif // TESTUTILS_HPP