8304815: Use NMT for more precise hs_err location printing

Reviewed-by: jsjolen, rkennke
This commit is contained in:
Thomas Stuefe 2023-04-02 06:19:07 +00:00
parent 34e66ce1ef
commit 41a3db267d
13 changed files with 260 additions and 45 deletions

@ -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;

@ -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.

@ -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<const void*, const MallocHeader*>(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()));

@ -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;
}

@ -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.

@ -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;

@ -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);

@ -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();

@ -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));
}

@ -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"

@ -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); }

@ -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();

@ -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