Skip to content

Commit 84d124e

Browse files
committed
Fail safe the canary chain walk and retire dead marker-tag plumbing
buildCanaryChainEvent()'s parent_tag walk now bounds at the frontier's maxCapacity() like reconstructChain() and returns false on a cyclic or corrupt chain instead of spinning the poll thread; the walk's event._root_kind now comes from the terminal root-attached entry rather than the parent-side interior entry (always 0), matching the appendStaticFieldRootType(terminal) call and suppressChainEvent()'s transient-root gate. Remove the retired canary marker-tag mechanism end to end: nothing pre-tags candidates anymore, so the decode branches in heapReferenceCallback() and pollWatchedTargets() were unreachable - and runPass()'s marker-release loop was worse than dead: with every _candidate_tags[i] left at 0 it asked GetObjectsWithTags to enumerate ALL untagged objects once per candidate slot on every search stop. Also: read the container usage file from the hierarchy that supplied the winning limit instead of probing v2/v1 filenames in order; clamp PainBudget::drain() against a backward clock step; refuse symlinked or non-owned rc-debug override files under /tmp; resolve each container interface tag independently instead of failing the whole call; reset consume_tier_fair()'s cursor on a completed-but-unproductive lap; drop the pod fixture's dead placeholder assignment and assert the representative carries its leak tag. Tests cover the bounded walk, the deep-chain reconstruction, the symlink rejection and the cycle fail-safe.
1 parent 25693f0 commit 84d124e

11 files changed

Lines changed: 289 additions & 167 deletions

‎ddprof-lib/src/main/cpp/os_linux.cpp‎

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -918,6 +918,14 @@ int OS::getCgroupCpuMillicores() {
918918
// delay the OOM projection.
919919
static char g_memory_limit_cgroup_path[PATH_MAX] = {0};
920920

921+
// Which cgroup hierarchy (v2 or v1) supplied the winning limit recorded in
922+
// g_memory_limit_cgroup_path: the usage file must be read from the SAME
923+
// hierarchy (v2 memory.current vs v1 memory.usage_in_bytes), not probed by
924+
// filename order - on a hybrid system both controller files can be visible
925+
// for the same cgroup dir, and reading the wrong one pairs the limit with an
926+
// unrelated usage number.
927+
static bool g_memory_limit_cgroup_v2 = true;
928+
921929
static long walkCgroupV2MemoryLimit(char* path, char* winner_path_out) {
922930
size_t base_len = strlen("/sys/fs/cgroup");
923931
long best = -1;
@@ -1002,6 +1010,7 @@ long OS::getContainerMemoryLimit() {
10021010
int fd = open(leaf, O_RDONLY);
10031011
if (fd != -1) {
10041012
close(fd);
1013+
g_memory_limit_cgroup_v2 = true;
10051014
return walkCgroupV2MemoryLimit(path, g_memory_limit_cgroup_path);
10061015
}
10071016
}
@@ -1022,6 +1031,7 @@ long OS::getContainerMemoryLimit() {
10221031
int fd = open(leaf, O_RDONLY);
10231032
if (fd != -1) {
10241033
close(fd);
1034+
g_memory_limit_cgroup_v2 = false;
10251035
return walkCgroupV1MemoryLimit(path, g_memory_limit_cgroup_path);
10261036
}
10271037
}
@@ -1045,26 +1055,24 @@ long OS::getContainerMemoryUsage() {
10451055

10461056
// Same cgroup the winning limit came from, if the limit walk recorded
10471057
// one - read its usage first, falling back to the process's own leaf.
1048-
// v2 names the file memory.current, v1 memory.usage_in_bytes - try both.
1058+
// The usage filename follows the hierarchy that supplied the limit (the
1059+
// recorded winner's hierarchy is authoritative, not filename order).
10491060
if (g_memory_limit_cgroup_path[0] != '\0') {
10501061
char file[PATH_MAX];
1051-
const char *usage_files[] = {"%s/memory.current", "%s/memory.usage_in_bytes"};
1052-
for (const char *fmt : usage_files) {
1053-
if ((size_t)snprintf(file, sizeof(file), fmt,
1054-
g_memory_limit_cgroup_path) >= sizeof(file)) {
1055-
continue;
1056-
}
1062+
const char *fmt = g_memory_limit_cgroup_v2 ? "%s/memory.current"
1063+
: "%s/memory.usage_in_bytes";
1064+
if ((size_t)snprintf(file, sizeof(file), fmt,
1065+
g_memory_limit_cgroup_path) < sizeof(file)) {
10571066
int fd = open(file, O_RDONLY);
1058-
if (fd == -1) {
1059-
continue;
1060-
}
1061-
char buf[32] = {0};
1062-
ssize_t r = read(fd, buf, sizeof(buf) - 1);
1063-
close(fd);
1064-
if (r > 0) {
1065-
long usage = atol(buf);
1066-
if (usage >= 0) {
1067-
return usage;
1067+
if (fd != -1) {
1068+
char buf[32] = {0};
1069+
ssize_t r = read(fd, buf, sizeof(buf) - 1);
1070+
close(fd);
1071+
if (r > 0) {
1072+
long usage = atol(buf);
1073+
if (usage >= 0) {
1074+
return usage;
1075+
}
10681076
}
10691077
}
10701078
}

‎ddprof-lib/src/main/cpp/painBudget.h‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,15 @@ class PainBudget {
4141
_last_update_ns = now_ns;
4242
return;
4343
}
44+
if (now_ns <= _last_update_ns) {
45+
// Clock stepped backward (or a test passed a literal smaller than the
46+
// OS::nanotime()-seeded baseline): the unsigned subtraction would wrap
47+
// to ~2^64 ns and drain the entire debt in one call. The failure
48+
// direction is permissive (a blocked restart becomes allowed), so clamp
49+
// to zero elapsed and re-baseline instead.
50+
_last_update_ns = now_ns;
51+
return;
52+
}
4453
u64 elapsed_ns = now_ns - _last_update_ns;
4554
double elapsed_ms = (double)elapsed_ns / 1000000.0;
4655
// _refill_rate == 0.0 (the default constructor argument) makes this a

‎ddprof-lib/src/main/cpp/referenceChainAnchors.cpp‎

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,24 @@ ReferenceChainTracker::collectStaticFieldAnchorsForRotation(int max_count) {
254254
}
255255
if (took > 0) {
256256
cursor = consumed_pos + 1 >= idx_size ? 0 : consumed_pos + 1;
257+
} else {
258+
// Took nothing AND no pick sits at or ahead of the cursor: this lap
259+
// already passed every current member of the tier (members selected in
260+
// earlier calls and since demoted out of eligibility). Without a reset
261+
// the cursor never wraps again - every later call skips them all
262+
// (p.pos < cursor) and the tier starves until a NEW anchor is appended
263+
// at a higher pos. Treat the lap as completed-but-unproductive and
264+
// restart it, matching the wrap semantics applied above.
265+
bool any_ahead = false;
266+
for (const TierPick &p : picks) {
267+
if (p.pos >= cursor) {
268+
any_ahead = true;
269+
break;
270+
}
271+
}
272+
if (!any_ahead) {
273+
cursor = 0;
274+
}
257275
}
258276
return took;
259277
};
@@ -346,10 +364,19 @@ bool ReferenceChainTracker::resolveContainerInterfaceTags(
346364
if (*iface.tag_out != 0) {
347365
continue;
348366
}
367+
// Each interface is resolved independently: a transient JVMTI error on
368+
// one of them (GetTag/SetTag/FindClass failing for exactly one) must not
369+
// abort the other's resolution - the already-resolved tag stays cached in
370+
// its slot either way, and a per-interface failure only leaves THAT tag
371+
// at 0 for this call (the caller treats a false return as "shapes
372+
// unknown this pass" and retries on the next reconcile). Failing the
373+
// whole call on the first error would keep both anchors unclassified for
374+
// as long as one interface keeps erroring, even though the other resolved
375+
// fine.
349376
jclass local = jni->FindClass(iface.name);
350377
if (jniExceptionCheck(jni) || local == nullptr) {
351378
jni->ExceptionClear();
352-
return false;
379+
continue;
353380
}
354381
jlong tag = 0;
355382
bool ok = jvmti->GetTag(local, &tag) == JVMTI_ERROR_NONE;
@@ -375,9 +402,9 @@ bool ReferenceChainTracker::resolveContainerInterfaceTags(
375402
*iface.tag_out = tag;
376403
}
377404
jni->DeleteLocalRef(local);
378-
if (!ok) {
379-
return false;
380-
}
405+
// A per-interface GetTag/SetTag failure leaves that tag at 0 - the final
406+
// check below reports false only if an interface genuinely has no tag,
407+
// never as an early abort that skips the other interface.
381408
}
382409
return _collection_iface_class_tag != 0 && _map_iface_class_tag != 0;
383410
}

‎ddprof-lib/src/main/cpp/referenceChainEvents.cpp‎

Lines changed: 46 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -171,8 +171,27 @@ bool ReferenceChainTracker::buildCanaryChainEvent(int candidate_idx,
171171
(long long)parent_tag, candidate_idx);
172172
return false;
173173
}
174-
root_kind = entry.root_kind;
175-
for (jlong tag = parent_tag; tag > 0;) {
174+
// Bounded like every sibling walk over this same parent chain:
175+
// FrontierTable::reconstructChain() bounds at maxCapacity() hops and
176+
// returns false on a "cyclic or corrupt parent chain",
177+
// requeueChainRootForRotation() bounds at _hop_cap, and improveChain()'s
178+
// cycle guard bounds at 4096. improveChain() structurally prevents cycles
179+
// (it refuses a parent whose chain routes through the entry), but the
180+
// table's contents are also written by insert() with no such validation,
181+
// so a corrupt chain must fail safe instead of spinning this poll-thread
182+
// walk forever: every tag maps to a distinct slot (tags are never
183+
// reused), so a well-formed chain can visit at most maxCapacity() entries
184+
// before reaching parent_tag == 0 or repeating a slot.
185+
const int hop_bound = _frontier->maxCapacity();
186+
int hops = 0;
187+
for (jlong tag = parent_tag; tag > 0; hops++) {
188+
if (hops > hop_bound) {
189+
TEST_LOG_SUMMARY("ReferenceChainTracker::buildCanaryChainEvent false: "
190+
"chain walk exceeded hop bound (%d) - cyclic or "
191+
"corrupt parent chain (candidate=%d)",
192+
hops, candidate_idx);
193+
return false;
194+
}
176195
if (!_frontier->lookup(tag, &entry)) {
177196
TEST_LOG_SUMMARY("ReferenceChainTracker::buildCanaryChainEvent false: "
178197
"chain walk: tag=%lld not in frontier (candidate=%d)",
@@ -182,6 +201,14 @@ bool ReferenceChainTracker::buildCanaryChainEvent(int candidate_idx,
182201
chain.push_back(entry.referrer_klass);
183202
tag = entry.parent_tag;
184203
}
204+
// The root kind describes the chain's ROOT, not the candidate-side parent:
205+
// the walk's last iteration is always the root-attached entry (parent_tag
206+
// == 0, root_kind != 0), so `entry` holds it here - same terminal-root
207+
// semantics reconstructChain() uses for *out_root_kind. The parent-side
208+
// entry read before the loop would almost always yield 0 (interior entries
209+
// carry root_kind == 0), misreporting every walk-reconstructed chain and
210+
// defeating suppressChainEvent()'s transient-root gate.
211+
root_kind = entry.root_kind;
185212
terminal = entry;
186213
} else if (parent_tag == 0 && frontier_tag > 0) {
187214
// Root-referenced candidate: chain is just [candidate_klass].
@@ -305,14 +332,12 @@ void ReferenceChainTracker::pollWatchedTargets(jvmtiEnv *jvmti, JNIEnv *jni) {
305332
klass_id, MAX_LEAK_CANDIDATES_FROM_LT);
306333
continue;
307334
}
308-
// Tag this candidate's specific representative object with a distinct marker tag
309-
// (MARKER_TAG_BASE - slot) so heapReferenceCallback() can identify that exact object by
310-
// identity when the walk reaches it - matching by class alone would record a chain for
311-
// whichever instance of that class the walk happens to visit, not necessarily the one
312-
// LivenessTracker flagged as growing.
335+
// Candidate admission: the marker->leak-tag migration retired
336+
// pre-tagging the representative object (the retired marker-tag decode
337+
// branches are gone); this candidate is discovered when the walk or the
338+
// poll intercepts one of its leak-tagged instances.
313339
int slot = _candidate_count;
314340
_candidate_klass_ids[slot] = klass_id;
315-
_candidate_tags[slot] = 0; // no marker tags — using leak tags now
316341
_candidate_count = slot + 1;
317342
TEST_LOG_SUMMARY("ReferenceChainTracker::pollWatchedTargets canary: admitted klass_id=%u "
318343
"into slot=%d (candidate_count now %d)",
@@ -428,57 +453,19 @@ void ReferenceChainTracker::pollWatchedTargets(jvmtiEnv *jvmti, JNIEnv *jni) {
428453
// Read the existing tag; seeding here would bypass the forward walk.
429454
jlong tag = getTag(jvmti, obj);
430455

431-
// Canary search: if this candidate was pre-tagged with a marker tag (negative, set above), use
432-
// the canary chain reconstruction.
433-
if (tag <= MARKER_TAG_BASE) {
434-
// The marker tag encodes the slot this object was pre-tagged at (MARKER_TAG_BASE - slot,
435-
// mirroring heapReferenceCallback()'s own decode at its MARKER_TAG_BASE check).
436-
int candidate_slot = (int)(MARKER_TAG_BASE - tag);
437-
if (candidate_slot < 0 || candidate_slot >= _candidate_count) {
438-
TEST_LOG_SUMMARY("ReferenceChainTracker::pollWatchedTargets canary candidate[%d] "
439-
"klass_id=%u marker_tag=%lld decodes to out-of-range slot=%d "
440-
"(candidate_count=%d) - skipping",
441-
i, klass_id, (long long)tag, candidate_slot, _candidate_count);
442-
jni->DeleteLocalRef(obj);
443-
continue;
444-
}
445-
bool need_refresh = false;
446-
jlong canary_ftag = _candidate_frontier_tags[candidate_slot];
447-
_resolved_chains_lock.lock();
448-
auto it = _resolved_chains.find(canary_ftag);
449-
need_refresh = (it == _resolved_chains.end() ||
450-
it->second.source_search_ns != current_search_ns);
451-
_resolved_chains_lock.unlock();
452-
TEST_LOG_SUMMARY("ReferenceChainTracker::pollWatchedTargets canary candidate[%d] "
453-
"klass_id=%u marker_tag=%lld slot=%d needRefresh=%d",
454-
i, klass_id, (long long)tag, candidate_slot, need_refresh);
455-
if (need_refresh) {
456-
ReferenceChainEvent event;
457-
bool built = buildCanaryChainEvent(candidate_slot, &event);
458-
TEST_LOG_SUMMARY("ReferenceChainTracker::pollWatchedTargets canary "
459-
"buildCanaryChainEvent(slot=%d) -> %d",
460-
candidate_slot, built);
461-
if (built && suppressChainEvent(event)) {
462-
TEST_LOG("ReferenceChainTracker::pollWatchedTargets "
463-
"filtered depth=%u root_kind=%d canary_ftag=%lld "
464-
"klass_id=%u (canary path)",
465-
event._depth, (int)event._root_kind,
466-
(long long)canary_ftag, klass_id);
467-
built = false;
468-
invalidateResolvedChain(canary_ftag);
469-
}
470-
if (built) {
471-
event._start_time = TSC::ticks();
472-
cacheResolvedChain(canary_ftag, std::move(event),
473-
canary_ftag, current_search_ns);
474-
}
475-
}
476-
// Fall through to discovered-instances check below — the canary representative may not have
477-
// been reached by BFS yet, but other instances of the same class may have been admitted and
478-
// their chains can be built now.
479-
}
480-
481-
// Normal (non-canary) path: tag > 0 means the walk visited this object and assigned it a
456+
// NOTE: the retired canary marker-tag decode used to live here (an object
457+
// pre-tagged with MARKER_TAG_BASE - slot took the legacy canary chain
458+
// reconstruction). The marker->leak-tag migration stopped pre-tagging
459+
// candidate representatives entirely - no JVMTI tag in the process can
460+
// ever be <= MARKER_TAG_BASE (-2^62): leak tags are positive
461+
// (LEAK_TAG_BASE), frontier tags positive, class tags small negative
462+
// magnitudes - so the branch was unreachable, and a stale negative tag
463+
// (a class tag) could never silently take the legacy canary path.
464+
// Candidate chain reconstruction now runs through the leak-tag discovery
465+
// block below and the dead-representative canary path earlier in this
466+
// loop.
467+
468+
// Normal path: tag > 0 means the walk visited this object and assigned it a
482469
// frontier tag.
483470

484471
// Keep the holder chain's root warm in the rotation queue: a growing container's current

‎ddprof-lib/src/main/cpp/referenceChainTraversal.cpp‎

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1164,25 +1164,15 @@ bool ReferenceChainTracker::runPass(jvmtiEnv *jvmti, JNIEnv *jni,
11641164

11651165
if (load(_search_state) != SearchState::RUNNING) {
11661166
_tags_released = releaseSearchTags(jvmti, jni);
1167-
// Release canary marker tags: use GetObjectsWithTags to find all live marker-tagged objects and
1168-
// clear them.
11691167
if (_candidate_count > 0) {
1170-
for (int i = 0; i < _candidate_count; i++) {
1171-
jlong tag = _candidate_tags[i];
1172-
jint count = 0;
1173-
jobject *objects = nullptr;
1174-
jlong *result_tags = nullptr;
1175-
jvmtiError cerr = jvmti->GetObjectsWithTags(
1176-
1, &tag, &count, &objects, &result_tags);
1177-
if (cerr == JVMTI_ERROR_NONE && count > 0) {
1178-
for (jint j = 0; j < count; j++) {
1179-
jvmti->SetTag(objects[j], 0);
1180-
jni->DeleteLocalRef(objects[j]);
1181-
}
1182-
jvmti->Deallocate((unsigned char *)objects);
1183-
jvmti->Deallocate((unsigned char *)result_tags);
1184-
}
1185-
}
1168+
// No marker-tag release pass: the marker->leak-tag migration retired
1169+
// pre-tagged candidate representatives (nothing sets _candidate_tags
1170+
// anymore), so there are no per-candidate marker JVMTI tags to clear -
1171+
// releaseSearchTags() above owns every live tag this search minted.
1172+
// (The old GetObjectsWithTags(1, &_candidate_tags[i]) loop here was
1173+
// worse than dead: with every _candidate_tags[i] left at 0 it asked
1174+
// JVMTI to enumerate ALL UNTAGGED objects - potentially the whole
1175+
// heap - once per candidate slot on every search stop.)
11861176
_candidate_count = 0;
11871177
_candidate_found_bits = 0;
11881178
memset(_candidate_discovered_count, 0, sizeof(_candidate_discovered_count));

‎ddprof-lib/src/main/cpp/referenceChainWalk.cpp‎

Lines changed: 9 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -168,59 +168,15 @@ jint JNICALL ReferenceChainTracker::heapReferenceCallback(
168168
edge_referrer_class_tag = *referrer_tag_ptr;
169169
}
170170

171-
// Canary pruning: if this object is the pre-tagged representative of a leaked candidate (marker
172-
// tag MARKER_TAG_BASE - i, negative), record its chain link but do NOT enqueue its children
173-
// (treat as a leaf).
174-
if (ctx->tracker->_candidate_count > 0 &&
175-
*tag_ptr <= ReferenceChainTracker::MARKER_TAG_BASE) {
176-
int candidate_idx = (int)(ReferenceChainTracker::MARKER_TAG_BASE - *tag_ptr);
177-
if (candidate_idx >= 0 && candidate_idx < ctx->tracker->_candidate_count) {
178-
jlong rtag = (referrer_tag_ptr != nullptr) ? *referrer_tag_ptr : 0;
179-
u32 candidate_klass = ctx->tracker->classTags()->resolve(class_tag);
180-
if (rtag > 0) {
181-
FrontierEntry parent{};
182-
if (ctx->frontier->lookup(rtag, &parent)) {
183-
// Use the marker tag itself as the frontier table key - it is already a unique
184-
// per-candidate value, so no nextTag() is needed.
185-
jlong frontier_tag = *tag_ptr;
186-
ctx->frontier->insert(frontier_tag, rtag,
187-
parent.referrer_klass,
188-
parent.depth + 1,
189-
FrontierEntryState::FRONTIER,
190-
parent.root_kind,
191-
/*class_tag=*/0, edge_field_index,
192-
(u8)reference_kind);
193-
ctx->tracker->_candidate_parent_tags[candidate_idx] = rtag;
194-
ctx->tracker->_candidate_frontier_tags[candidate_idx] = frontier_tag;
195-
ctx->tracker->_candidate_referrer_klasses[candidate_idx] = candidate_klass;
196-
ctx->tracker->_candidate_depths[candidate_idx] = parent.depth + 1;
197-
ctx->tracker->_candidate_found_bits |= (1ULL << candidate_idx);
198-
TEST_LOG("ReferenceChainTracker::heapReferenceCallback canary "
199-
"pruned candidate %d (klass_id=%u frontier_tag=%lld)",
200-
candidate_idx, candidate_klass, (long long)frontier_tag);
201-
}
202-
} else {
203-
// Root-referenced candidate.
204-
jlong frontier_tag = *tag_ptr;
205-
ctx->frontier->insert(frontier_tag, 0,
206-
candidate_klass, 1,
207-
FrontierEntryState::FRONTIER,
208-
(u8)reference_kind,
209-
/*class_tag=*/0, edge_field_index,
210-
/*edge_kind=*/0, edge_referrer_class_tag);
211-
ctx->tracker->_candidate_parent_tags[candidate_idx] = 0;
212-
ctx->tracker->_candidate_frontier_tags[candidate_idx] = frontier_tag;
213-
ctx->tracker->_candidate_referrer_klasses[candidate_idx] = candidate_klass;
214-
ctx->tracker->_candidate_depths[candidate_idx] = 1;
215-
ctx->tracker->_candidate_found_bits |= (1ULL << candidate_idx);
216-
TEST_LOG("ReferenceChainTracker::heapReferenceCallback canary "
217-
"pruned root-referenced candidate %d (klass_id=%u)",
218-
candidate_idx, candidate_klass);
219-
}
220-
// Do NOT enqueue children for this object.
221-
return 0;
222-
}
223-
}
171+
// NOTE: the retired canary marker-tag decode used to live here (objects
172+
// pre-tagged with MARKER_TAG_BASE - i were pruned as leaves and recorded as
173+
// chain roots). The marker->leak-tag migration stopped pre-tagging candidate
174+
// representatives entirely - no JVMTI tag in the process can ever be
175+
// <= MARKER_TAG_BASE (-2^62): leak tags are positive (LEAK_TAG_BASE),
176+
// frontier tags positive, class tags small negative magnitudes - so the
177+
// branch was unreachable. Candidate discovery now runs exclusively through
178+
// the leak-tag interception in pollWatchedTargets() (which also records
179+
// _candidate_found_bits/_candidate_frontier_tags).
224180

225181
if (*tag_ptr < 0) {
226182
if (ctx->static_field_seed &&

0 commit comments

Comments
 (0)