From 2be9e91d485757602e7dd74dfd4116921561d033 Mon Sep 17 00:00:00 2001 From: Jaroslav Bachorik Date: Tue, 28 Jul 2026 09:45:01 +0200 Subject: [PATCH 1/6] sphinx: fix PROF-15075 --- ddprof-lib/src/main/cpp/flightRecorder.cpp | 27 +++++- ddprof-lib/src/main/cpp/flightRecorder.h | 2 +- ddprof-lib/src/main/cpp/jfrMetadata.cpp | 20 ++++ ddprof-lib/src/main/cpp/jfrMetadata.h | 13 ++- ddprof-lib/src/main/cpp/safeAccess.cpp | 4 +- ddprof-lib/src/test/cpp/jfrMetadata_ut.cpp | 93 ++++++++++++++++++ .../metadata/MetadataRestartCycleTest.java | 95 +++++++++++++++++++ 7 files changed, 247 insertions(+), 7 deletions(-) create mode 100644 ddprof-lib/src/test/cpp/jfrMetadata_ut.cpp create mode 100644 ddprof-test/src/test/java/com/datadoghq/profiler/metadata/MetadataRestartCycleTest.java diff --git a/ddprof-lib/src/main/cpp/flightRecorder.cpp b/ddprof-lib/src/main/cpp/flightRecorder.cpp index 6064e594cc..94e625a7e9 100644 --- a/ddprof-lib/src/main/cpp/flightRecorder.cpp +++ b/ddprof-lib/src/main/cpp/flightRecorder.cpp @@ -1145,7 +1145,16 @@ void Recording::writeHeader(Buffer *buf) { flushIfNeeded(buf); } -void Recording::writeElement(Buffer *buf, const Element *e) { +void Recording::writeElement(Buffer *buf, const Element *e, int depth) { + if (depth > 10) { + fprintf(stderr, "[ddprof] [ERROR] writeElement depth limit exceeded, truncating output\n"); + return; + } + + if (e == nullptr) { + return; + } + buf->putVar64(e->_name); buf->putVar64(e->_attributes.size()); @@ -1155,10 +1164,22 @@ void Recording::writeElement(Buffer *buf, const Element *e) { buf->putVar64(e->_attributes[i]._value); } - buf->putVar64(e->_children.size()); + size_t child_count = 0; for (size_t i = 0; i < e->_children.size(); i++) { + if (e->_children[i] != nullptr) { + child_count++; + } else { + fprintf(stderr, "[ddprof] [WARN] writeElement skipping null child at index %zu\n", i); + } + } + + buf->putVar64(child_count); + for (size_t i = 0; i < e->_children.size(); i++) { + if (e->_children[i] == nullptr) { + continue; + } flushIfNeeded(buf); - writeElement(buf, e->_children[i]); + writeElement(buf, e->_children[i], depth + 1); } flushIfNeeded(buf); } diff --git a/ddprof-lib/src/main/cpp/flightRecorder.h b/ddprof-lib/src/main/cpp/flightRecorder.h index ee6929b4f1..9c31c7cda6 100644 --- a/ddprof-lib/src/main/cpp/flightRecorder.h +++ b/ddprof-lib/src/main/cpp/flightRecorder.h @@ -236,7 +236,7 @@ class Recording { void writeMetadata(Buffer *buf); - void writeElement(Buffer *buf, const Element *e); + void writeElement(Buffer *buf, const Element *e, int depth = 0); void writeEventSizePrefix(Buffer *buf, int start); diff --git a/ddprof-lib/src/main/cpp/jfrMetadata.cpp b/ddprof-lib/src/main/cpp/jfrMetadata.cpp index 33496bb1bc..4064ba241d 100644 --- a/ddprof-lib/src/main/cpp/jfrMetadata.cpp +++ b/ddprof-lib/src/main/cpp/jfrMetadata.cpp @@ -24,12 +24,32 @@ std::vector Element::_strings; JfrMetadata JfrMetadata::_root; bool JfrMetadata::_initialized = false; +std::vector JfrMetadata::_nofields; JfrMetadata::JfrMetadata() : Element("root") {} +// Private helper: recursively delete all heap-allocated Elements in the subtree. +// Safe to call only when no profiling engines are running (called from reset()). +static void deleteElementTree(Element *e) { + if (e == nullptr) return; + for (const Element *child : e->_children) { + deleteElementTree(const_cast(child)); + } + delete e; +} + // Must only be called after all profiler engines are stopped and no signal // handlers can fire. std::vector/std::map are not async-signal-safe. void JfrMetadata::reset() { + // Recursively delete all heap-allocated Elements in the tree before clearing vectors. + for (const Element *child : _root._children) { + deleteElementTree(const_cast(child)); + } + // Delete all tracked NoField instances that were allocated during initialize() + for (NoField *nf : _nofields) { + delete nf; + } + _nofields.clear(); _root._children.clear(); _root._attributes.clear(); _strings.clear(); diff --git a/ddprof-lib/src/main/cpp/jfrMetadata.h b/ddprof-lib/src/main/cpp/jfrMetadata.h index ac241a7a84..7af59c2205 100644 --- a/ddprof-lib/src/main/cpp/jfrMetadata.h +++ b/ddprof-lib/src/main/cpp/jfrMetadata.h @@ -165,6 +165,8 @@ class JfrMetadata : Element { private: static JfrMetadata _root; static bool _initialized; + // Track NoField instances allocated during initialize() for cleanup in reset() + static std::vector _nofields; enum FieldFlags { F_CPOOL = 0x1, @@ -204,7 +206,9 @@ class JfrMetadata : Element { const char *label = NULL, int flags = 0, bool condition = true) { if (!condition) { - return *new NoField(name); + NoField *nf = new NoField(name); + _nofields.push_back(nf); // Track for cleanup in reset() + return *nf; } Element &e = element("field"); e.attribute("name", name); @@ -261,7 +265,14 @@ class JfrMetadata : Element { public: JfrMetadata(); + // Initialize the JFR metadata tree with standard types and optional context attributes. + // PRECONDITION: Must be called with Profiler::_state_lock held. + // reset() must be called before each initialize() to clean up the prior tree. static void initialize(const std::vector &contextAttributes); + + // Reset and deallocate the JFR metadata tree. + // PRECONDITION: Must be called with Profiler::_state_lock held. + // Must be called before any signal handlers can fire (all profiling engines stopped). static void reset(); static Element *root() { return &_root; } diff --git a/ddprof-lib/src/main/cpp/safeAccess.cpp b/ddprof-lib/src/main/cpp/safeAccess.cpp index bd637d051a..4bcf596b18 100644 --- a/ddprof-lib/src/main/cpp/safeAccess.cpp +++ b/ddprof-lib/src/main/cpp/safeAccess.cpp @@ -60,10 +60,10 @@ static void verify_safecopy_range() { #endif // DEBUG #ifdef __APPLE__ + #define DU3_PREFIX(s, m) __ ## s.__ ## m #if defined(__x86_64__) - #define current_pc context_rip + #define current_pc uc_mcontext->DU3_PREFIX(ss,rip) #elif defined(__aarch64__) - #define DU3_PREFIX(s, m) __ ## s.__ ## m #define current_pc uc_mcontext->DU3_PREFIX(ss,pc) #endif #else diff --git a/ddprof-lib/src/test/cpp/jfrMetadata_ut.cpp b/ddprof-lib/src/test/cpp/jfrMetadata_ut.cpp new file mode 100644 index 0000000000..5f039fb0dd --- /dev/null +++ b/ddprof-lib/src/test/cpp/jfrMetadata_ut.cpp @@ -0,0 +1,93 @@ +/* + * 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. + +#include "jfrMetadata.h" + +#include +#include +#include + +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()); + EXPECT_TRUE(JfrMetadata::strings().empty()); +} + +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()); + EXPECT_TRUE(JfrMetadata::strings().empty()); +} + +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 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(); +} diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/metadata/MetadataRestartCycleTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/metadata/MetadataRestartCycleTest.java new file mode 100644 index 0000000000..1d87e5c938 --- /dev/null +++ b/ddprof-test/src/test/java/com/datadoghq/profiler/metadata/MetadataRestartCycleTest.java @@ -0,0 +1,95 @@ +/* + * Copyright 2026 Datadog, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datadoghq.profiler.metadata; + +import com.datadoghq.profiler.AbstractProfilerTest; +import org.junit.jupiter.api.Test; +import org.openjdk.jmc.common.item.IItemCollection; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Regression test for PROF-15075 (SIGSEGV in {@code Recording::writeElement}). + * + *

Before the fix, {@code JfrMetadata::reset()} cleared the metadata tree's + * child vector without deleting the heap-allocated {@code Element} and + * {@code NoField} objects that {@code JfrMetadata::initialize()} had + * allocated. On the next profiler restart, {@code initialize()} rebuilt the + * tree with freshly allocated {@code Element}s; once the allocator reused a + * freed address, {@code Recording::writeElement()} could dereference stale + * memory during the following dump and crash with a SIGSEGV. + * + *

This test drives several full stop/start restart cycles with CPU + * sampling, allocation sampling, and dynamic context attributes enabled, so + * that both {@code Element::operator||()} (dynamic attribute fields) and the + * JDK-version-conditional {@code NoField} path in {@code JfrMetadata::field()} + * are exercised on every {@code reset()}/{@code initialize()} pair. Each + * cycle's dump must produce a valid, parseable JFR recording; a pre-fix + * build crashes before completing all cycles. + */ +public class MetadataRestartCycleTest extends AbstractProfilerTest { + + private static final int RESTART_CYCLES = 5; + private static volatile Object sink; + + @Test + public void repeatedRestartCyclesProduceValidRecordings() throws Exception { + waitForProfilerReady(2000); + runWorkload(); + stopProfiler(); + + Files.createDirectories(Paths.get("/tmp/recordings")); + for (int cycle = 0; cycle < RESTART_CYCLES; cycle++) { + Path recording = Files.createTempFile(Paths.get("/tmp/recordings"), + "MetadataRestartCycleTest_" + cycle + "_", ".jfr"); + try { + // Triggers JfrMetadata::reset() (of the previous cycle's tree) followed + // by JfrMetadata::initialize() for the new session. + profiler.execute("start," + getProfilerCommand() + ",jfr,file=" + recording.toAbsolutePath()); + waitForProfilerReady(2000); + + runWorkload(); + + // Triggers Profiler::dump() -> Recording::switchChunk() -> + // Recording::writeMetadata() -> Recording::writeElement(). + profiler.stop(); + + IItemCollection samples = verifyEvents(recording, "datadog.ExecutionSample", true); + assertTrue(samples.hasItems(), "cycle " + cycle + " produced no ExecutionSample events"); + + IItemCollection allocations = verifyEvents(recording, "datadog.ObjectSample", true); + assertTrue(allocations.hasItems(), "cycle " + cycle + " produced no ObjectSample events"); + } finally { + Files.deleteIfExists(recording); + } + } + } + + private static void runWorkload() { + for (int i = 0; i < 500_000; i++) { + sink = new Object[4]; + } + } + + @Override + protected String getProfilerCommand() { + return "cpu=1ms,alloc=512k,attributes=tag1;tag2;tag3"; + } +} From 3f6e8538a3fcfb9196af77c1163fb20ececabb0d Mon Sep 17 00:00:00 2001 From: Jaroslav Bachorik Date: Tue, 28 Jul 2026 13:39:50 +0200 Subject: [PATCH 2/6] Scope PROF-15075 fix to writeElement guard only Drop the JfrMetadata::reset() leak fix and its tests: that leak is only reachable via a Profiler restart (reset() -> initialize() twice in the same process), which doesn't happen in production. It's real but unrelated to this crash and is fixed separately in DataDog/java-profiler#. The PROF-15075 crash trace (dump -> switchChunk -> writeMetadata -> writeElement, no restart involved) is addressed by the null-check and recursion-depth guard in writeElement alone; true trigger still unconfirmed, see updated spec. --- ddprof-lib/src/main/cpp/jfrMetadata.cpp | 20 ---- ddprof-lib/src/main/cpp/jfrMetadata.h | 13 +-- ddprof-lib/src/test/cpp/jfrMetadata_ut.cpp | 93 ------------------ .../metadata/MetadataRestartCycleTest.java | 95 ------------------- 4 files changed, 1 insertion(+), 220 deletions(-) delete mode 100644 ddprof-lib/src/test/cpp/jfrMetadata_ut.cpp delete mode 100644 ddprof-test/src/test/java/com/datadoghq/profiler/metadata/MetadataRestartCycleTest.java diff --git a/ddprof-lib/src/main/cpp/jfrMetadata.cpp b/ddprof-lib/src/main/cpp/jfrMetadata.cpp index 4064ba241d..33496bb1bc 100644 --- a/ddprof-lib/src/main/cpp/jfrMetadata.cpp +++ b/ddprof-lib/src/main/cpp/jfrMetadata.cpp @@ -24,32 +24,12 @@ std::vector Element::_strings; JfrMetadata JfrMetadata::_root; bool JfrMetadata::_initialized = false; -std::vector JfrMetadata::_nofields; JfrMetadata::JfrMetadata() : Element("root") {} -// Private helper: recursively delete all heap-allocated Elements in the subtree. -// Safe to call only when no profiling engines are running (called from reset()). -static void deleteElementTree(Element *e) { - if (e == nullptr) return; - for (const Element *child : e->_children) { - deleteElementTree(const_cast(child)); - } - delete e; -} - // Must only be called after all profiler engines are stopped and no signal // handlers can fire. std::vector/std::map are not async-signal-safe. void JfrMetadata::reset() { - // Recursively delete all heap-allocated Elements in the tree before clearing vectors. - for (const Element *child : _root._children) { - deleteElementTree(const_cast(child)); - } - // Delete all tracked NoField instances that were allocated during initialize() - for (NoField *nf : _nofields) { - delete nf; - } - _nofields.clear(); _root._children.clear(); _root._attributes.clear(); _strings.clear(); diff --git a/ddprof-lib/src/main/cpp/jfrMetadata.h b/ddprof-lib/src/main/cpp/jfrMetadata.h index 7af59c2205..ac241a7a84 100644 --- a/ddprof-lib/src/main/cpp/jfrMetadata.h +++ b/ddprof-lib/src/main/cpp/jfrMetadata.h @@ -165,8 +165,6 @@ class JfrMetadata : Element { private: static JfrMetadata _root; static bool _initialized; - // Track NoField instances allocated during initialize() for cleanup in reset() - static std::vector _nofields; enum FieldFlags { F_CPOOL = 0x1, @@ -206,9 +204,7 @@ class JfrMetadata : Element { const char *label = NULL, int flags = 0, bool condition = true) { if (!condition) { - NoField *nf = new NoField(name); - _nofields.push_back(nf); // Track for cleanup in reset() - return *nf; + return *new NoField(name); } Element &e = element("field"); e.attribute("name", name); @@ -265,14 +261,7 @@ class JfrMetadata : Element { public: JfrMetadata(); - // Initialize the JFR metadata tree with standard types and optional context attributes. - // PRECONDITION: Must be called with Profiler::_state_lock held. - // reset() must be called before each initialize() to clean up the prior tree. static void initialize(const std::vector &contextAttributes); - - // Reset and deallocate the JFR metadata tree. - // PRECONDITION: Must be called with Profiler::_state_lock held. - // Must be called before any signal handlers can fire (all profiling engines stopped). static void reset(); static Element *root() { return &_root; } diff --git a/ddprof-lib/src/test/cpp/jfrMetadata_ut.cpp b/ddprof-lib/src/test/cpp/jfrMetadata_ut.cpp deleted file mode 100644 index 5f039fb0dd..0000000000 --- a/ddprof-lib/src/test/cpp/jfrMetadata_ut.cpp +++ /dev/null @@ -1,93 +0,0 @@ -/* - * 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. - -#include "jfrMetadata.h" - -#include -#include -#include - -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()); - EXPECT_TRUE(JfrMetadata::strings().empty()); -} - -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()); - EXPECT_TRUE(JfrMetadata::strings().empty()); -} - -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 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(); -} diff --git a/ddprof-test/src/test/java/com/datadoghq/profiler/metadata/MetadataRestartCycleTest.java b/ddprof-test/src/test/java/com/datadoghq/profiler/metadata/MetadataRestartCycleTest.java deleted file mode 100644 index 1d87e5c938..0000000000 --- a/ddprof-test/src/test/java/com/datadoghq/profiler/metadata/MetadataRestartCycleTest.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright 2026 Datadog, Inc - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datadoghq.profiler.metadata; - -import com.datadoghq.profiler.AbstractProfilerTest; -import org.junit.jupiter.api.Test; -import org.openjdk.jmc.common.item.IItemCollection; - -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; - -import static org.junit.jupiter.api.Assertions.assertTrue; - -/** - * Regression test for PROF-15075 (SIGSEGV in {@code Recording::writeElement}). - * - *

Before the fix, {@code JfrMetadata::reset()} cleared the metadata tree's - * child vector without deleting the heap-allocated {@code Element} and - * {@code NoField} objects that {@code JfrMetadata::initialize()} had - * allocated. On the next profiler restart, {@code initialize()} rebuilt the - * tree with freshly allocated {@code Element}s; once the allocator reused a - * freed address, {@code Recording::writeElement()} could dereference stale - * memory during the following dump and crash with a SIGSEGV. - * - *

This test drives several full stop/start restart cycles with CPU - * sampling, allocation sampling, and dynamic context attributes enabled, so - * that both {@code Element::operator||()} (dynamic attribute fields) and the - * JDK-version-conditional {@code NoField} path in {@code JfrMetadata::field()} - * are exercised on every {@code reset()}/{@code initialize()} pair. Each - * cycle's dump must produce a valid, parseable JFR recording; a pre-fix - * build crashes before completing all cycles. - */ -public class MetadataRestartCycleTest extends AbstractProfilerTest { - - private static final int RESTART_CYCLES = 5; - private static volatile Object sink; - - @Test - public void repeatedRestartCyclesProduceValidRecordings() throws Exception { - waitForProfilerReady(2000); - runWorkload(); - stopProfiler(); - - Files.createDirectories(Paths.get("/tmp/recordings")); - for (int cycle = 0; cycle < RESTART_CYCLES; cycle++) { - Path recording = Files.createTempFile(Paths.get("/tmp/recordings"), - "MetadataRestartCycleTest_" + cycle + "_", ".jfr"); - try { - // Triggers JfrMetadata::reset() (of the previous cycle's tree) followed - // by JfrMetadata::initialize() for the new session. - profiler.execute("start," + getProfilerCommand() + ",jfr,file=" + recording.toAbsolutePath()); - waitForProfilerReady(2000); - - runWorkload(); - - // Triggers Profiler::dump() -> Recording::switchChunk() -> - // Recording::writeMetadata() -> Recording::writeElement(). - profiler.stop(); - - IItemCollection samples = verifyEvents(recording, "datadog.ExecutionSample", true); - assertTrue(samples.hasItems(), "cycle " + cycle + " produced no ExecutionSample events"); - - IItemCollection allocations = verifyEvents(recording, "datadog.ObjectSample", true); - assertTrue(allocations.hasItems(), "cycle " + cycle + " produced no ObjectSample events"); - } finally { - Files.deleteIfExists(recording); - } - } - } - - private static void runWorkload() { - for (int i = 0; i < 500_000; i++) { - sink = new Object[4]; - } - } - - @Override - protected String getProfilerCommand() { - return "cpu=1ms,alloc=512k,attributes=tag1;tag2;tag3"; - } -} From 1b4c67ad1b900b7cc6251967fd5771a94e9dbfea Mon Sep 17 00:00:00 2001 From: Jaroslav Bachorik Date: Tue, 28 Jul 2026 14:03:04 +0200 Subject: [PATCH 3/6] Surface writeElement's metadata-tree guard via Counters stderr from an embedded native lib is rarely captured/monitored, and this guard exists precisely because the tree-corruption trigger is unconfirmed (PROF-15075) -- an unmonitored log line would let it recur invisibly. Increment metadata_tree_null_child / metadata_tree_depth_exceeded so a recurrence shows up as a datadog.ProfilerCounter JFR event instead of only stderr. --- ddprof-lib/src/main/cpp/counters.h | 6 ++++++ ddprof-lib/src/main/cpp/flightRecorder.cpp | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/ddprof-lib/src/main/cpp/counters.h b/ddprof-lib/src/main/cpp/counters.h index deb6e40d9a..d14bf2e08b 100644 --- a/ddprof-lib/src/main/cpp/counters.h +++ b/ddprof-lib/src/main/cpp/counters.h @@ -134,6 +134,12 @@ X(SAFECOPY_FAILED, "safecopy_failed") \ X(SAFEFETCH_FAILED, "safefetch_failed") \ X(STACKWALK_LONGJMP_RECOVERED, "stackwalk_longjmp_recovered") \ + /* writeElement() guards against a corrupted/dangling JfrMetadata tree \ + * (PROF-15075); root cause unconfirmed as of this counter's addition, see \ + * docs/sphinx/specs/2026-07-27-sigsegv-in-recording-writeelement.md. These \ + * counters are the durable signal for spotting a recurrence. */ \ + X(METADATA_TREE_NULL_CHILD, "metadata_tree_null_child") \ + X(METADATA_TREE_DEPTH_EXCEEDED, "metadata_tree_depth_exceeded") \ DD_COUNTER_TABLE_FAULT_INJECTION(X) \ DD_COUNTER_TABLE_FI_DEBUG(X) \ DD_COUNTER_TABLE_DEBUG(X) diff --git a/ddprof-lib/src/main/cpp/flightRecorder.cpp b/ddprof-lib/src/main/cpp/flightRecorder.cpp index 94e625a7e9..ceb27b01aa 100644 --- a/ddprof-lib/src/main/cpp/flightRecorder.cpp +++ b/ddprof-lib/src/main/cpp/flightRecorder.cpp @@ -1147,6 +1147,11 @@ void Recording::writeHeader(Buffer *buf) { void Recording::writeElement(Buffer *buf, const Element *e, int depth) { if (depth > 10) { + // Counter is the durable signal here: stderr from an embedded native lib + // is rarely captured/monitored, and this guard exists precisely because + // we don't yet know what corrupts the tree (PROF-15075) — an unmonitored + // log line would let that recur invisibly forever. + Counters::increment(METADATA_TREE_DEPTH_EXCEEDED); fprintf(stderr, "[ddprof] [ERROR] writeElement depth limit exceeded, truncating output\n"); return; } @@ -1169,6 +1174,7 @@ void Recording::writeElement(Buffer *buf, const Element *e, int depth) { if (e->_children[i] != nullptr) { child_count++; } else { + Counters::increment(METADATA_TREE_NULL_CHILD); fprintf(stderr, "[ddprof] [WARN] writeElement skipping null child at index %zu\n", i); } } From 1a63378f218cde5293b5630dbc84a43fc49a1a56 Mon Sep 17 00:00:00 2001 From: Jaroslav Bachorik Date: Wed, 29 Jul 2026 14:35:22 +0200 Subject: [PATCH 4/6] Fix child_count mismatch and add metadata-guard regression tests Exclude depth-truncated children from the encoded child_count so the JFR metadata stream stays structurally valid, and add gtest coverage for the null-child and depth-boundary serialization guards. --- ddprof-lib/src/main/cpp/flightRecorder.cpp | 54 +++++--- ddprof-lib/src/main/cpp/flightRecorder.h | 14 ++ .../test/cpp/flightRecorder_metadata_ut.cpp | 127 ++++++++++++++++++ 3 files changed, 177 insertions(+), 18 deletions(-) create mode 100644 ddprof-lib/src/test/cpp/flightRecorder_metadata_ut.cpp diff --git a/ddprof-lib/src/main/cpp/flightRecorder.cpp b/ddprof-lib/src/main/cpp/flightRecorder.cpp index ceb27b01aa..042cf338f4 100644 --- a/ddprof-lib/src/main/cpp/flightRecorder.cpp +++ b/ddprof-lib/src/main/cpp/flightRecorder.cpp @@ -1145,7 +1145,34 @@ void Recording::writeHeader(Buffer *buf) { flushIfNeeded(buf); } +size_t Recording::countSerializableChildren( + const std::vector &children, int depth) { + // Children one level deeper than `depth` are what writeElement() would + // truncate on its own depth check, so exclude them here too, before being + // counted, so child_count always matches the number of children actually + // serialized below (an inflated count would make the metadata stream + // itself malformed). + bool truncate_children = depth + 1 > 10; + + size_t child_count = 0; + for (size_t i = 0; i < children.size(); i++) { + if (children[i] == nullptr) { + Counters::increment(METADATA_TREE_NULL_CHILD); + fprintf(stderr, "[ddprof] [WARN] writeElement skipping null child at index %zu\n", i); + } else if (truncate_children) { + Counters::increment(METADATA_TREE_DEPTH_EXCEEDED); + } else { + child_count++; + } + } + return child_count; +} + void Recording::writeElement(Buffer *buf, const Element *e, int depth) { + if (e == nullptr) { + return; + } + if (depth > 10) { // Counter is the durable signal here: stderr from an embedded native lib // is rarely captured/monitored, and this guard exists precisely because @@ -1156,10 +1183,6 @@ void Recording::writeElement(Buffer *buf, const Element *e, int depth) { return; } - if (e == nullptr) { - return; - } - buf->putVar64(e->_name); buf->putVar64(e->_attributes.size()); @@ -1169,23 +1192,18 @@ void Recording::writeElement(Buffer *buf, const Element *e, int depth) { buf->putVar64(e->_attributes[i]._value); } - size_t child_count = 0; - for (size_t i = 0; i < e->_children.size(); i++) { - if (e->_children[i] != nullptr) { - child_count++; - } else { - Counters::increment(METADATA_TREE_NULL_CHILD); - fprintf(stderr, "[ddprof] [WARN] writeElement skipping null child at index %zu\n", i); - } - } + bool truncate_children = depth + 1 > 10; + size_t child_count = countSerializableChildren(e->_children, depth); buf->putVar64(child_count); - for (size_t i = 0; i < e->_children.size(); i++) { - if (e->_children[i] == nullptr) { - continue; + if (!truncate_children) { + for (size_t i = 0; i < e->_children.size(); i++) { + if (e->_children[i] == nullptr) { + continue; + } + flushIfNeeded(buf); + writeElement(buf, e->_children[i], depth + 1); } - flushIfNeeded(buf); - writeElement(buf, e->_children[i], depth + 1); } flushIfNeeded(buf); } diff --git a/ddprof-lib/src/main/cpp/flightRecorder.h b/ddprof-lib/src/main/cpp/flightRecorder.h index 9c31c7cda6..7ec422e08d 100644 --- a/ddprof-lib/src/main/cpp/flightRecorder.h +++ b/ddprof-lib/src/main/cpp/flightRecorder.h @@ -174,6 +174,11 @@ class Recording { friend ObjectSampler; friend Profiler; friend Lookup; + // Grants gtest access to the private countSerializableChildren() helper below, + // since Recording itself can't be constructed in a plain gtest binary (its + // constructor needs a live JVMTI environment). Same pattern as + // VMTestAccessor/ProfilerTestAccessor in the test sources. + friend class RecordingTestAccessor; private: static char *_agent_properties; @@ -181,6 +186,15 @@ class Recording { static char *_jvm_flags; static char *_java_command; + // Determines how many of `children` writeElement() will actually serialize + // at the given depth, applying the same null-child and depth-limit skip + // rules the recursive writer uses. Both the child_count written to the + // buffer and the recursion in writeElement() call this single function, so + // the encoded count can never diverge from what actually gets serialized + // (PROF-15075). + static size_t countSerializableChildren( + const std::vector &children, int depth); + RecordingBuffer _buf[CONCURRENCY_LEVEL]; // we have several tables to avoid lock contention // we have a second dimension to allow a switch in the active table diff --git a/ddprof-lib/src/test/cpp/flightRecorder_metadata_ut.cpp b/ddprof-lib/src/test/cpp/flightRecorder_metadata_ut.cpp new file mode 100644 index 0000000000..da06294cdf --- /dev/null +++ b/ddprof-lib/src/test/cpp/flightRecorder_metadata_ut.cpp @@ -0,0 +1,127 @@ +/* + * Copyright 2026, Datadog, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +// Regression tests for Recording::countSerializableChildren(), the helper +// writeElement() uses (PROF-15075) to decide which children of a +// JfrMetadata::Element tree get serialized when the tree is corrupted +// (null children) or unexpectedly deep (cycles / excessive recursion). +// +// Recording itself can't be constructed in a plain gtest binary -- its +// constructor unconditionally calls VM::jvmti()->GetAvailableProcessors(), +// and writeSettings()/writeOsCpuInfo()/writeJvmInfo() reach into +// VM::libjvm() and Profiler::instance(), none of which are set up without a +// live JVM attached. countSerializableChildren() is extracted specifically +// so the counting/truncation logic writeElement() depends on can be tested +// directly, without needing any of that. +// +// The bug this guards against: originally, child_count was computed by +// counting every non-null child, while the recursive write skipped children +// once `depth > 10`. A non-null child at the depth boundary was included in +// child_count but never serialized, so the encoded count didn't match the +// number of children actually written -- a structurally invalid JFR +// metadata event. countSerializableChildren() now applies the exact same +// depth-truncation rule used to decide whether to recurse, so the count it +// returns can never diverge from what gets serialized. + +#include + +#include "counters.h" +#include "flightRecorder.h" +#include "jfrMetadata.h" + +// Friend of Recording (see flightRecorder.h), giving this test access to the +// private countSerializableChildren() helper. Same pattern as +// VMTestAccessor/ProfilerTestAccessor used elsewhere in this test suite. +class RecordingTestAccessor { +public: + static size_t countSerializableChildren( + const std::vector &children, int depth) { + return Recording::countSerializableChildren(children, depth); + } +}; + +namespace { + +// Builds a chain of `depth` nested "field" elements: root -> child_1 -> ... . +// Only used to hold real Element children; JfrMetadata's own string interning +// keeps this allocation-free after warmup, and none of these elements are +// ever freed (mirrors JfrMetadata::root(), whose tree lives for the process +// lifetime). +const Element *makeChild() { return new Element("field"); } + +} // namespace + +TEST(WriteElementMetadataGuardTest, AllValidChildrenAreCountedAtShallowDepth) { + std::vector children = {makeChild(), makeChild(), + makeChild()}; + + size_t count = RecordingTestAccessor::countSerializableChildren(children, 0); + + EXPECT_EQ(3u, count); +} + +TEST(WriteElementMetadataGuardTest, NullChildrenAreExcludedAndCounted) { + std::vector children = {makeChild(), nullptr, makeChild(), + nullptr}; + + long long null_before = Counters::getCounter(METADATA_TREE_NULL_CHILD); + + size_t count = RecordingTestAccessor::countSerializableChildren(children, 0); + + long long null_after = Counters::getCounter(METADATA_TREE_NULL_CHILD); + + // Only the two non-null children are counted -- a naive + // children.size()-based count would report 4, which would make the + // encoded metadata event advertise two children that are never written. + EXPECT_EQ(2u, count); + EXPECT_EQ(2, null_after - null_before); +} + +// This is the exact structural-validity bug the depth guard must not +// reintroduce: a non-null child sitting exactly at the depth boundary +// (depth 10, so its would-be recursive call is at depth 11) must be +// excluded from child_count, not just skipped by the recursive writer -- +// otherwise the encoded count and the actually-serialized children diverge. +TEST(WriteElementMetadataGuardTest, ChildAtDepthBoundaryIsExcludedFromCount) { + std::vector children = {makeChild()}; + + long long depth_before = Counters::getCounter(METADATA_TREE_DEPTH_EXCEEDED); + + size_t count = RecordingTestAccessor::countSerializableChildren(children, 10); + + long long depth_after = Counters::getCounter(METADATA_TREE_DEPTH_EXCEEDED); + + EXPECT_EQ(0u, count); + EXPECT_EQ(1, depth_after - depth_before); +} + +// Sanity check: a child one level shallower than the boundary (depth 9, so +// its recursive call lands at depth 10, still within the depth > 10 limit) +// must still be counted normally -- the truncation must not kick in early. +TEST(WriteElementMetadataGuardTest, ChildJustBeforeDepthBoundaryIsStillCounted) { + std::vector children = {makeChild(), makeChild()}; + + size_t count = RecordingTestAccessor::countSerializableChildren(children, 9); + + EXPECT_EQ(2u, count); +} + +// A null child at the depth boundary must be reported as a null child, not +// double-counted against the depth-exceeded counter as well. +TEST(WriteElementMetadataGuardTest, NullChildAtDepthBoundaryIsReportedAsNull) { + std::vector children = {nullptr}; + + long long null_before = Counters::getCounter(METADATA_TREE_NULL_CHILD); + long long depth_before = Counters::getCounter(METADATA_TREE_DEPTH_EXCEEDED); + + size_t count = RecordingTestAccessor::countSerializableChildren(children, 10); + + long long null_after = Counters::getCounter(METADATA_TREE_NULL_CHILD); + long long depth_after = Counters::getCounter(METADATA_TREE_DEPTH_EXCEEDED); + + EXPECT_EQ(0u, count); + EXPECT_EQ(1, null_after - null_before); + EXPECT_EQ(0, depth_after - depth_before); +} From ef66af9789131c8459ab2e502a5611b141ec70f9 Mon Sep 17 00:00:00 2001 From: Jaroslav Bachorik Date: Wed, 29 Jul 2026 15:57:20 +0200 Subject: [PATCH 5/6] Fix ASan leak in flightRecorder_metadata_ut by owning test Elements Each TEST now uses an ElementOwner that frees its Elements via unique_ptr on scope exit, instead of leaking raw new-allocated Elements per test run. Co-Authored-By: Claude Opus 5 --- .../test/cpp/flightRecorder_metadata_ut.cpp | 37 +++++++++++++------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/ddprof-lib/src/test/cpp/flightRecorder_metadata_ut.cpp b/ddprof-lib/src/test/cpp/flightRecorder_metadata_ut.cpp index da06294cdf..67a7b1bafb 100644 --- a/ddprof-lib/src/test/cpp/flightRecorder_metadata_ut.cpp +++ b/ddprof-lib/src/test/cpp/flightRecorder_metadata_ut.cpp @@ -26,6 +26,7 @@ // returns can never diverge from what gets serialized. #include +#include #include "counters.h" #include "flightRecorder.h" @@ -44,18 +45,27 @@ class RecordingTestAccessor { namespace { -// Builds a chain of `depth` nested "field" elements: root -> child_1 -> ... . -// Only used to hold real Element children; JfrMetadata's own string interning -// keeps this allocation-free after warmup, and none of these elements are -// ever freed (mirrors JfrMetadata::root(), whose tree lives for the process -// lifetime). -const Element *makeChild() { return new Element("field"); } +// Owns the "field" Elements handed out by makeChild() below, so each test's +// children are freed when its owner goes out of scope instead of leaking +// (unlike JfrMetadata::root(), whose tree intentionally lives for the +// process lifetime, these are throwaway test fixtures). +class ElementOwner { +public: + const Element *makeChild() { + _owned.push_back(std::make_unique("field")); + return _owned.back().get(); + } + +private: + std::vector> _owned; +}; } // namespace TEST(WriteElementMetadataGuardTest, AllValidChildrenAreCountedAtShallowDepth) { - std::vector children = {makeChild(), makeChild(), - makeChild()}; + ElementOwner owner; + std::vector children = {owner.makeChild(), owner.makeChild(), + owner.makeChild()}; size_t count = RecordingTestAccessor::countSerializableChildren(children, 0); @@ -63,8 +73,9 @@ TEST(WriteElementMetadataGuardTest, AllValidChildrenAreCountedAtShallowDepth) { } TEST(WriteElementMetadataGuardTest, NullChildrenAreExcludedAndCounted) { - std::vector children = {makeChild(), nullptr, makeChild(), - nullptr}; + ElementOwner owner; + std::vector children = {owner.makeChild(), nullptr, + owner.makeChild(), nullptr}; long long null_before = Counters::getCounter(METADATA_TREE_NULL_CHILD); @@ -85,7 +96,8 @@ TEST(WriteElementMetadataGuardTest, NullChildrenAreExcludedAndCounted) { // excluded from child_count, not just skipped by the recursive writer -- // otherwise the encoded count and the actually-serialized children diverge. TEST(WriteElementMetadataGuardTest, ChildAtDepthBoundaryIsExcludedFromCount) { - std::vector children = {makeChild()}; + ElementOwner owner; + std::vector children = {owner.makeChild()}; long long depth_before = Counters::getCounter(METADATA_TREE_DEPTH_EXCEEDED); @@ -101,7 +113,8 @@ TEST(WriteElementMetadataGuardTest, ChildAtDepthBoundaryIsExcludedFromCount) { // its recursive call lands at depth 10, still within the depth > 10 limit) // must still be counted normally -- the truncation must not kick in early. TEST(WriteElementMetadataGuardTest, ChildJustBeforeDepthBoundaryIsStillCounted) { - std::vector children = {makeChild(), makeChild()}; + ElementOwner owner; + std::vector children = {owner.makeChild(), owner.makeChild()}; size_t count = RecordingTestAccessor::countSerializableChildren(children, 9); From 655da91c4a91c60ba941e5d60221bcb1f4e681f3 Mon Sep 17 00:00:00 2001 From: Jaroslav Bachorik Date: Tue, 4 Aug 2026 13:01:50 +0200 Subject: [PATCH 6/6] Drop inline JIRA refs from PROF-15075 comments Co-Authored-By: Claude Opus 5 --- ddprof-lib/src/main/cpp/counters.h | 7 +++---- ddprof-lib/src/main/cpp/flightRecorder.cpp | 7 +++---- ddprof-lib/src/main/cpp/flightRecorder.h | 3 +-- ddprof-lib/src/test/cpp/flightRecorder_metadata_ut.cpp | 6 +++--- 4 files changed, 10 insertions(+), 13 deletions(-) diff --git a/ddprof-lib/src/main/cpp/counters.h b/ddprof-lib/src/main/cpp/counters.h index d14bf2e08b..a3b3ea34f7 100644 --- a/ddprof-lib/src/main/cpp/counters.h +++ b/ddprof-lib/src/main/cpp/counters.h @@ -134,10 +134,9 @@ X(SAFECOPY_FAILED, "safecopy_failed") \ X(SAFEFETCH_FAILED, "safefetch_failed") \ X(STACKWALK_LONGJMP_RECOVERED, "stackwalk_longjmp_recovered") \ - /* writeElement() guards against a corrupted/dangling JfrMetadata tree \ - * (PROF-15075); root cause unconfirmed as of this counter's addition, see \ - * docs/sphinx/specs/2026-07-27-sigsegv-in-recording-writeelement.md. These \ - * counters are the durable signal for spotting a recurrence. */ \ + /* writeElement() guards against a corrupted/dangling JfrMetadata tree. \ + * Root cause is still unconfirmed, so these counters are the durable \ + * signal for spotting a recurrence. */ \ X(METADATA_TREE_NULL_CHILD, "metadata_tree_null_child") \ X(METADATA_TREE_DEPTH_EXCEEDED, "metadata_tree_depth_exceeded") \ DD_COUNTER_TABLE_FAULT_INJECTION(X) \ diff --git a/ddprof-lib/src/main/cpp/flightRecorder.cpp b/ddprof-lib/src/main/cpp/flightRecorder.cpp index 042cf338f4..d910105b2e 100644 --- a/ddprof-lib/src/main/cpp/flightRecorder.cpp +++ b/ddprof-lib/src/main/cpp/flightRecorder.cpp @@ -1174,10 +1174,9 @@ void Recording::writeElement(Buffer *buf, const Element *e, int depth) { } if (depth > 10) { - // Counter is the durable signal here: stderr from an embedded native lib - // is rarely captured/monitored, and this guard exists precisely because - // we don't yet know what corrupts the tree (PROF-15075) — an unmonitored - // log line would let that recur invisibly forever. + // stderr from an embedded native lib is rarely captured or monitored, and + // we don't yet know what corrupts the tree, so the counter is the durable + // signal here — an unmonitored log line would let it recur invisibly. Counters::increment(METADATA_TREE_DEPTH_EXCEEDED); fprintf(stderr, "[ddprof] [ERROR] writeElement depth limit exceeded, truncating output\n"); return; diff --git a/ddprof-lib/src/main/cpp/flightRecorder.h b/ddprof-lib/src/main/cpp/flightRecorder.h index 7ec422e08d..f7319f55af 100644 --- a/ddprof-lib/src/main/cpp/flightRecorder.h +++ b/ddprof-lib/src/main/cpp/flightRecorder.h @@ -190,8 +190,7 @@ class Recording { // at the given depth, applying the same null-child and depth-limit skip // rules the recursive writer uses. Both the child_count written to the // buffer and the recursion in writeElement() call this single function, so - // the encoded count can never diverge from what actually gets serialized - // (PROF-15075). + // the encoded count can never diverge from what actually gets serialized. static size_t countSerializableChildren( const std::vector &children, int depth); diff --git a/ddprof-lib/src/test/cpp/flightRecorder_metadata_ut.cpp b/ddprof-lib/src/test/cpp/flightRecorder_metadata_ut.cpp index 67a7b1bafb..c1c6258b2b 100644 --- a/ddprof-lib/src/test/cpp/flightRecorder_metadata_ut.cpp +++ b/ddprof-lib/src/test/cpp/flightRecorder_metadata_ut.cpp @@ -4,9 +4,9 @@ */ // Regression tests for Recording::countSerializableChildren(), the helper -// writeElement() uses (PROF-15075) to decide which children of a -// JfrMetadata::Element tree get serialized when the tree is corrupted -// (null children) or unexpectedly deep (cycles / excessive recursion). +// writeElement() uses to decide which children of a JfrMetadata::Element +// tree get serialized when the tree is corrupted (null children) or +// unexpectedly deep (cycles / excessive recursion). // // Recording itself can't be constructed in a plain gtest binary -- its // constructor unconditionally calls VM::jvmti()->GetAvailableProcessors(),