-
Notifications
You must be signed in to change notification settings - Fork 13
Fix memory leak in JfrMetadata::reset() #694
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
Merged
+132
−1
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
008d688
Fix memory leak in JfrMetadata::reset()
jbachorik 64c7b44
Merge branch 'main' into fix/jfrmetadata-reset-leak
jbachorik 3ac838f
Add virtual destructor to Element to avoid UB on polymorphic delete
jbachorik fc8b762
Merge branch 'main' into fix/jfrmetadata-reset-leak
jbachorik File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| /* | ||
| * Copyright 2026, Datadog, Inc. | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| // Regression tests for PROF-15075 (SIGSEGV in Recording::writeElement). | ||
| // | ||
| // JfrMetadata::reset() used to clear _root._children (and the tracked | ||
| // NoField instances) without deleting the underlying heap-allocated Element | ||
| // objects that JfrMetadata::initialize() had allocated via element(), | ||
| // operator||(), and the conditional NoField path in field(). Once the | ||
| // allocator reused a freed address on the next initialize() call, any | ||
| // dangling pointer left over from before would point at unrelated memory. | ||
| // | ||
| // These tests exercise JfrMetadata::initialize()/reset() directly (no JVM | ||
| // attach required -- initialize() only touches VM::isHotspot()/ | ||
| // VM::hotspot_version(), which default to false/-1 in this test binary, | ||
| // so the conditional NoField path in field() is deterministically taken). | ||
| // Running this test under an ASan/LeakSanitizer build (testAsan) is what | ||
| // actually proves the fix: a pre-fix build leaks every Element and NoField | ||
| // allocated by initialize() on every reset(), and LeakSanitizer reports it | ||
| // at process exit. | ||
|
jbachorik marked this conversation as resolved.
|
||
|
|
||
| #include "jfrMetadata.h" | ||
|
|
||
| #include <gtest/gtest.h> | ||
| #include <vector> | ||
| #include <string> | ||
|
|
||
| TEST(JfrMetadataResetTest, ResetIsSafeBeforeAnyInitialize) { | ||
| // reset() must be safe to call even if initialize() was never called | ||
| // (e.g. Profiler::stop() racing a failed Profiler::start()). | ||
| JfrMetadata::reset(); | ||
| EXPECT_TRUE(JfrMetadata::root()->_children.empty()); | ||
| // reset() re-registers "root" at string id 0 so _root._name stays valid, | ||
| // so strings() always contains exactly that one entry, never empty. | ||
| EXPECT_EQ(JfrMetadata::strings().size(), 1u); | ||
| EXPECT_EQ(JfrMetadata::strings()[0], "root"); | ||
| } | ||
|
|
||
| TEST(JfrMetadataResetTest, InitializeThenResetClearsTree) { | ||
| JfrMetadata::reset(); | ||
| JfrMetadata::initialize({}); | ||
|
|
||
| EXPECT_FALSE(JfrMetadata::root()->_children.empty()); | ||
| EXPECT_FALSE(JfrMetadata::strings().empty()); | ||
|
|
||
| JfrMetadata::reset(); | ||
|
|
||
| EXPECT_TRUE(JfrMetadata::root()->_children.empty()); | ||
| // See ResetIsSafeBeforeAnyInitialize: "root" is always re-registered. | ||
| EXPECT_EQ(JfrMetadata::strings().size(), 1u); | ||
| EXPECT_EQ(JfrMetadata::strings()[0], "root"); | ||
| } | ||
|
|
||
| TEST(JfrMetadataResetTest, MultipleInitializeResetCyclesDoNotCrash) { | ||
| // Simulates repeated Profiler::start()/stop() restart cycles. Each | ||
| // initialize() allocates a fresh Element/NoField tree; each reset() must | ||
| // fully delete the previous cycle's tree before the next initialize() | ||
| // reuses the freed heap addresses. | ||
| for (int i = 0; i < 5; i++) { | ||
| JfrMetadata::reset(); | ||
| JfrMetadata::initialize({}); | ||
| EXPECT_FALSE(JfrMetadata::root()->_children.empty()) | ||
| << "cycle " << i << " did not populate the metadata tree"; | ||
| } | ||
| JfrMetadata::reset(); | ||
| EXPECT_TRUE(JfrMetadata::root()->_children.empty()); | ||
| } | ||
|
|
||
| TEST(JfrMetadataResetTest, RestartCyclesWithContextAttributesDoNotCrash) { | ||
| // Non-empty contextAttributes exercise Element::operator||(), which | ||
| // allocates one "field" Element per attribute name; those instances must | ||
| // also be reachable (and deleted) via reset()'s recursive tree cleanup. | ||
| std::vector<std::string> contextAttributes = {"tag1", "tag2", "tag3"}; | ||
| for (int i = 0; i < 5; i++) { | ||
| JfrMetadata::reset(); | ||
| JfrMetadata::initialize(contextAttributes); | ||
| EXPECT_FALSE(JfrMetadata::root()->_children.empty()) | ||
| << "cycle " << i << " did not populate the metadata tree"; | ||
| } | ||
| JfrMetadata::reset(); | ||
| } | ||
|
|
||
| TEST(JfrMetadataResetTest, InitializeIsIdempotentWithoutReset) { | ||
| // JfrMetadata::initialize() guards against double-initialization; calling | ||
| // it twice without an intervening reset() must not double-allocate or | ||
| // crash. This documents/protects the existing `if (_initialized) return;` | ||
| // safeguard called out in the PROF-15075 spec. | ||
| JfrMetadata::reset(); | ||
| JfrMetadata::initialize({}); | ||
| size_t childrenAfterFirst = JfrMetadata::root()->_children.size(); | ||
|
|
||
| JfrMetadata::initialize({}); // no-op: _initialized guard short-circuits | ||
| EXPECT_EQ(childrenAfterFirst, JfrMetadata::root()->_children.size()); | ||
|
|
||
| JfrMetadata::reset(); | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.