Skip to content

Commit 00194f8

Browse files
committed
fix: address review feedback on PR #598
- setContextAttributesByIdAndBytes: return false when constantIds.length > MAX_CUSTOM_SLOTS - snapshotTags(int[]): zero indices beyond attributes.size() to prevent foreign sidecar leakage - Add 5 acceptance tests
1 parent 97e40c8 commit 00194f8

3 files changed

Lines changed: 188 additions & 6 deletions

File tree

ddprof-lib/src/main/java/com/datadoghq/profiler/ContextSetter.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.datadoghq.profiler;
22

33
import java.util.ArrayList;
4+
import java.util.Arrays;
45
import java.util.HashSet;
56
import java.util.List;
67
import java.util.Set;
@@ -31,12 +32,16 @@ public int[] snapshotTags() {
3132
}
3233

3334
/**
34-
* Copies current sidecar encodings into {@code snapshot}. The copy only runs when
35-
* {@code snapshot.length >= attributes.size()}; callers must size the array accordingly.
35+
* Copies current sidecar encodings into {@code snapshot}. The array must have at least
36+
* {@code attributes.size()} elements; arrays shorter than {@code attributes.size()} are
37+
* silently ignored. Indices {@code [attributes.size(), snapshot.length)} are zeroed after
38+
* copying to prevent stale data from leaking to the caller.
39+
* Use the no-arg {@link #snapshotTags()} overload to obtain a correctly sized array.
3640
*/
3741
public void snapshotTags(int[] snapshot) {
3842
if (snapshot.length >= attributes.size()) {
3943
profiler.copyTags(snapshot);
44+
Arrays.fill(snapshot, attributes.size(), snapshot.length, 0);
4045
}
4146
}
4247

ddprof-lib/src/main/java/com/datadoghq/profiler/ThreadContext.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -361,15 +361,19 @@ private boolean writeSlot(int keyIndex, int encoding, byte[] utf8) {
361361
* @param utf8 per-slot UTF-8 value bytes; must be non-null and at most
362362
* {@value #MAX_VALUE_BYTES} bytes for every slot whose constantId {@code > 0}
363363
* @return true if every slot with {@code constantId > 0} was written; false on invalid
364-
* arguments (null arrays, length mismatch, or a missing/oversized {@code utf8} entry),
365-
* if the record was not valid before the call (nothing published), or if any slot
366-
* overflowed attrs_data (that slot's sidecar is zeroed)
364+
* arguments (null arrays, length mismatch, a missing/oversized {@code utf8} entry,
365+
* or {@code constantIds.length > MAX_CUSTOM_SLOTS}), if the record was not valid
366+
* before the call (nothing published), or if any slot overflowed attrs_data (that
367+
* slot's sidecar is zeroed)
367368
*/
368369
public boolean setContextAttributesByIdAndBytes(int[] constantIds, byte[][] utf8) {
369370
if (constantIds == null || utf8 == null || constantIds.length != utf8.length) {
370371
return false;
371372
}
372-
int len = Math.min(constantIds.length, MAX_CUSTOM_SLOTS);
373+
if (constantIds.length > MAX_CUSTOM_SLOTS) {
374+
return false;
375+
}
376+
int len = constantIds.length;
373377
// Validate before mutating so malformed input never leaves a half-written record.
374378
for (int i = 0; i < len; i++) {
375379
if (constantIds[i] > 0) {

ddprof-test/src/test/java/com/datadoghq/profiler/context/TagContextTest.java

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,179 @@ public void testReapplyByIdAndBytesOverflowRollback() throws Exception {
406406
"overflowed slot must read null — the entry never landed in attrs_data");
407407
}
408408

409+
// -----------------------------------------------------------------------
410+
// Acceptance tests for the MAX_CUSTOM_SLOTS guard fixes
411+
// -----------------------------------------------------------------------
412+
413+
/**
414+
* Test 1: setContextValuesByIdAndBytes must return false immediately when
415+
* the arrays are longer than MAX_CUSTOM_SLOTS (10), and must not perform
416+
* any partial write before the rejection.
417+
*/
418+
@Test
419+
public void testSetContextValuesByIdAndBytesRejectsArraysLongerThanMaxSlots() throws Exception {
420+
Assumptions.assumeTrue(!Platform.isJ9());
421+
registerCurrentThreadForWallClockProfiling();
422+
ContextSetter contextSetter = new ContextSetter(profiler, Arrays.asList("tag1", "tag2"));
423+
int slot = contextSetter.offsetOf("tag1");
424+
425+
// Establish a known value and capture its constant ID.
426+
assertTrue(contextSetter.setContextValue("tag1", "original"));
427+
int savedId = contextSetter.snapshotTags()[slot];
428+
assertNotEquals(0, savedId);
429+
430+
// Build arrays of length 11 (> MAX_CUSTOM_SLOTS = 10).
431+
int[] ids = new int[11];
432+
byte[][] utf8 = new byte[11][];
433+
ids[0] = savedId;
434+
utf8[0] = "original".getBytes(StandardCharsets.UTF_8);
435+
// All other entries remain 0 / null.
436+
437+
// The call must be rejected outright.
438+
assertFalse(contextSetter.setContextValuesByIdAndBytes(ids, utf8),
439+
"setContextValuesByIdAndBytes must return false when array length > MAX_CUSTOM_SLOTS");
440+
441+
// No partial write: the sidecar for slot 0 must be unchanged.
442+
assertEquals(savedId, contextSetter.snapshotTags()[slot],
443+
"sidecar must not be modified before the length guard fires");
444+
}
445+
446+
/**
447+
* Test 2: setContextValuesByIdAndBytes must accept arrays of exactly
448+
* MAX_CUSTOM_SLOTS (10) and return true, restoring all sidecar values.
449+
*/
450+
@Test
451+
public void testSetContextValuesByIdAndBytesAcceptsExactlyMaxSlots() throws Exception {
452+
Assumptions.assumeTrue(!Platform.isJ9());
453+
registerCurrentThreadForWallClockProfiling();
454+
List<String> attrs = new ArrayList<>();
455+
for (int i = 1; i <= 10; i++) {
456+
attrs.add("tag" + i);
457+
}
458+
ContextSetter contextSetter = new ContextSetter(profiler, attrs);
459+
460+
// Set all 10 attributes to distinct values and capture constant IDs + bytes.
461+
int[] savedIds = new int[10];
462+
byte[][] savedBytes = new byte[10][];
463+
for (int i = 0; i < 10; i++) {
464+
String value = "val" + i;
465+
assertTrue(contextSetter.setContextValue("tag" + (i + 1), value));
466+
savedBytes[i] = value.getBytes(StandardCharsets.UTF_8);
467+
}
468+
int[] snapshot = contextSetter.snapshotTags();
469+
for (int i = 0; i < 10; i++) {
470+
savedIds[i] = snapshot[i];
471+
assertNotEquals(0, savedIds[i], "tag" + (i + 1) + " must have a non-zero sidecar ID");
472+
}
473+
474+
// Wipe all slots via setContext (span activation).
475+
profiler.setContext(1L, 42L, 0L, 42L);
476+
477+
// Reapply with exactly-10-element arrays — must succeed.
478+
assertTrue(contextSetter.setContextValuesByIdAndBytes(savedIds, savedBytes),
479+
"setContextValuesByIdAndBytes must return true for arrays of length == MAX_CUSTOM_SLOTS");
480+
481+
// All 10 sidecar IDs must be restored.
482+
int[] restored = contextSetter.snapshotTags();
483+
for (int i = 0; i < 10; i++) {
484+
assertEquals(savedIds[i], restored[i],
485+
"sidecar for tag" + (i + 1) + " must be restored after reapply");
486+
}
487+
}
488+
489+
/**
490+
* Test 3: snapshotTags(int[]) with an oversized buffer (length > attributes.size())
491+
* must write the managed indices [0, attributes.size()) with the current sidecar values,
492+
* and zero out the extra indices [attributes.size(), snapshot.length).
493+
*/
494+
@Test
495+
public void testSnapshotTagsOversizedBufferCopiesAndZerosExtras() throws Exception {
496+
Assumptions.assumeTrue(!Platform.isJ9());
497+
registerCurrentThreadForWallClockProfiling();
498+
ContextSetter contextSetter = new ContextSetter(profiler, Arrays.asList("tag1", "tag2"));
499+
500+
assertTrue(contextSetter.setContextValue("tag1", "v1"));
501+
assertTrue(contextSetter.setContextValue("tag2", "v2"));
502+
503+
// Verify no-arg overload returns valid IDs.
504+
int[] canonical = contextSetter.snapshotTags();
505+
assertNotEquals(0, canonical[0]);
506+
assertNotEquals(0, canonical[1]);
507+
508+
// Oversized buffer: length 5 > attributes.size() == 2.
509+
int[] oversized = new int[5];
510+
Arrays.fill(oversized, -1);
511+
contextSetter.snapshotTags(oversized);
512+
513+
// Managed indices [0, attributes.size()) must contain the current sidecar values.
514+
assertEquals(canonical[0], oversized[0],
515+
"oversized buffer[0] must match no-arg snapshotTags()[0]");
516+
assertEquals(canonical[1], oversized[1],
517+
"oversized buffer[1] must match no-arg snapshotTags()[1]");
518+
519+
// Extra indices [attributes.size(), snapshot.length) must be zeroed.
520+
for (int i = 2; i < oversized.length; i++) {
521+
assertEquals(0, oversized[i],
522+
"oversized buffer element [" + i + "] must be zeroed by snapshotTags");
523+
}
524+
525+
// No-arg overload must still work correctly.
526+
int[] check = contextSetter.snapshotTags();
527+
assertEquals(canonical[0], check[0]);
528+
assertEquals(canonical[1], check[1]);
529+
}
530+
531+
/**
532+
* Test 4: snapshotTags(int[]) with an undersized buffer (length < attributes.size())
533+
* must be a no-op — existing no-op semantics must be preserved.
534+
*/
535+
@Test
536+
public void testSnapshotTagsUndersizedBufferIsNoOp() throws Exception {
537+
Assumptions.assumeTrue(!Platform.isJ9());
538+
registerCurrentThreadForWallClockProfiling();
539+
ContextSetter contextSetter = new ContextSetter(profiler, Arrays.asList("tag1", "tag2", "tag3"));
540+
541+
assertTrue(contextSetter.setContextValue("tag1", "a"));
542+
assertTrue(contextSetter.setContextValue("tag2", "b"));
543+
assertTrue(contextSetter.setContextValue("tag3", "c"));
544+
545+
// Undersized buffer: length 1 < attributes.size() == 3.
546+
int[] undersized = new int[1];
547+
undersized[0] = -1;
548+
contextSetter.snapshotTags(undersized);
549+
550+
assertEquals(-1, undersized[0],
551+
"undersized buffer must not be written by snapshotTags");
552+
}
553+
554+
/**
555+
* Test 5: snapshotTags(int[]) with an exact-size buffer (length == attributes.size())
556+
* must copy the current sidecar values correctly.
557+
*/
558+
@Test
559+
public void testSnapshotTagsExactSizeBufferCopiesCorrectly() throws Exception {
560+
Assumptions.assumeTrue(!Platform.isJ9());
561+
registerCurrentThreadForWallClockProfiling();
562+
ContextSetter contextSetter = new ContextSetter(profiler, Arrays.asList("tag1", "tag2"));
563+
564+
assertTrue(contextSetter.setContextValue("tag1", "x"));
565+
assertTrue(contextSetter.setContextValue("tag2", "y"));
566+
567+
// No-arg overload to obtain expected values.
568+
int[] canonical = contextSetter.snapshotTags();
569+
assertNotEquals(0, canonical[0]);
570+
assertNotEquals(0, canonical[1]);
571+
572+
// Exact-size buffer: length 2 == attributes.size() == 2.
573+
int[] exact = new int[2];
574+
contextSetter.snapshotTags(exact);
575+
576+
assertEquals(canonical[0], exact[0],
577+
"exact-size buffer[0] must match no-arg snapshotTags()[0]");
578+
assertEquals(canonical[1], exact[1],
579+
"exact-size buffer[1] must match no-arg snapshotTags()[1]");
580+
}
581+
409582
private void work(ContextSetter contextSetter, String contextAttribute, String contextValue)
410583
throws InterruptedException {
411584
assertTrue(contextSetter.setContextValue(contextAttribute, contextValue));

0 commit comments

Comments
 (0)