Skip to content

Possible out-of-bounds Context read - #774

Open
zhengyu123 wants to merge 4 commits into
mainfrom
zgu/out-of-bound-context
Open

Possible out-of-bounds Context read#774
zhengyu123 wants to merge 4 commits into
mainfrom
zgu/out-of-bound-context

Conversation

@zhengyu123

@zhengyu123 zhengyu123 commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?:
Fixes an out-of-bounds read in Context::get_tag() (now bounds-checked getTag/setTag) that could be triggered by configuring more than DD_TAGS_CAPACITY (10) custom context attributes via the attributes= profiler argument.

  • Context::tags is a fixed Tag[DD_TAGS_CAPACITY] array, but attributes= accepted an unbounded number of names and _num_context_attributes was set to that unbounded count with no cap.
  • Recording::writeContextSnapshot() looped up to numContextAttributes() calling the unchecked get_tag(i), so configuring 11+ attributes read past the end of Context into adjacent memory on every datadog.HeapLiveObject event.
  • Fix: Profiler::start() now caps args._context_attributes at DD_TAGS_CAPACITY (logging a warning when truncating) before it drives both the JFR metadata schema and _num_context_attributes, so the schema and the per-event write count always agree. Context::get_tag()/setTag() are replaced with encapsulated, bounds-checked getTag/setTag accessors (private tags[], shared isValidIndex() helper) as defense in depth for any future caller.

Motivation:
Security review flagged that attributes= had no upper bound on the number of configured names, while the native Context struct backing per-event tag storage is fixed-size. An out-of-bounds read on every liveness-tracking event can disclose adjacent native heap memory into the JFR recording, or crash the process (e.g. under ASan).

Additional Notes:
Iterated based on review feedback:

  • Tighten API: replaced direct tags[] access with private storage + getTag/setTag.
  • Fix: gave setTag a real runtime bounds check (not just a debug-only assert), matching getTag's existing clamp-to-default behavior.
  • Dedup code: factored the repeated bounds condition out into a shared isValidIndex() helper used by both accessors.

How to test the change?:
Added ddprof-test/.../com/datadoghq/profiler/TooManyContextAttributesTest.java, which configures attributes= with 13 names (3 over capacity) together with liveness tracking (memory=...:L, the code path that exercises writeContextSnapshot/getTag), generates enough live allocation volume to produce datadog.HeapLiveObject samples, and asserts:

  • the recording parses cleanly (would previously corrupt the JFR event stream or crash under ASan on the out-of-bounds read), and
  • jdk.ActiveSetting reports exactly the first 10 attribute names, confirming the list is capped rather than the requested 13.

For Datadog employees:

  • If this PR touches code that signs or publishes builds or packages, or handles
    credentials of any kind, I've requested a security review (run the dd:platform-security-review
    skill, or file a request via the PSEC review form).
    bewaire also runs automatically on every PR.
  • This PR doesn't touch any of that.
  • JIRA: PROF-15847

Unsure? Have a question? Request a review!

@dd-octo-sts

dd-octo-sts Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Scan-Build Report

User:runner@runnervmgx7h7
Working Directory:/home/runner/work/java-profiler/java-profiler/ddprof-lib/src/test/make
Command Line:make -j4 all
Clang Version:Ubuntu clang version 18.1.3 (1ubuntu1)
Date:Tue Sep 1 19:42:32 2026

Bug Summary

Bug TypeQuantityDisplay?
All Bugs1
Logic error
Dereference of null pointer1

Reports

Bug Group Bug Type ▾ File Function/Method Line Path Length
Logic errorDereference of null pointerfaultInjection.cppcrashNow242

@dd-octo-sts

dd-octo-sts Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

CI Test Results

Run: #33619870379 | Commit: 8d8bb0f | Duration: 18m 34s (longest job)

All 32 test jobs passed

Status Overview

JDK glibc-aarch64/debug glibc-amd64/debug musl-aarch64/debug musl-amd64/debug
8 - - -
8-ibm - - -
8-j9 - -
8-librca - -
8-orcl - - -
11 - - -
11-j9 - -
11-librca - -
17 - -
17-graal - -
17-j9 - -
17-librca - -
21 - -
21-graal - -
21-librca - -
25 - -
25-graal - -
25-librca - -

Legend: ✅ passed | ❌ failed | ⚪ skipped | 🚫 cancelled

Summary: Total: 32 | Passed: 32 | Failed: 0


Updated: 2026-09-02 10:52:50 UTC

@dd-octo-sts

dd-octo-sts Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

All 40 integration tests passed

📊 Dashboard · 👷 Pipeline · 📦 fdfffbe2

@zhengyu123
zhengyu123 marked this pull request as ready for review September 1, 2026 20:04
@zhengyu123
zhengyu123 requested a review from a team as a code owner September 1, 2026 20:04

@datadog-datadog-us1-prod datadog-datadog-us1-prod Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Datadog Autotest: PASS

More details

The 10-attribute cap keeps the JFR schema and event field count within Context storage. The checked accessors also reject invalid tag indexes.

Was this helpful? React 👍 or 👎

Open Bits AI session

🤖 Datadog Autotest · Commit fdfffbe · What is Autotest? · @DataDog review to ask questions · Any feedback? Reach out in #autotest

@rkennke rkennke added the sphinx:spotcheck Sphinx: spot-check recommended label Sep 2, 2026

@jbachorik jbachorik left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@rkennke rkennke left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated sphinx review — spot-check findings below, none HIGH/CRITICAL.

// aborted the JVM above. On any build, a mismatched schema/field count would make
// this parse fail or throw - reaching here with samples already proves the fix.
JfrEvents liveObjectEvents = verifyEvents("datadog.HeapLiveObject", false);
assertTrue(liveObjectEvents.hasItems(), "expected datadog.HeapLiveObject samples");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MEDIUM (robustness): The deterministic capping assertions run after a GC/timing-dependent assertion (liveObjectEvents.hasItems()). If no datadog.HeapLiveObject sample is produced on a given run, the test fails before ever checking that attributes= was capped at MAX_CONTEXT_SLOTS, so a flake and a real regression of the fix look identical.

Suggestion: reorder the method body so the deterministic jdk.ActiveSetting cap assertions run first, then assert liveObjectEvents.hasItems() last.

// Always enable library trap to catch wasmtime loading and patch its broken sigaction
switchLibraryTrap(true);

if (args._context_attributes.size() > DD_TAGS_CAPACITY) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LOW (completeness): The DD_TAGS_CAPACITY invariant is enforced at a single call site deep inside Profiler::start() (after switchLibraryTrap/startRefresher), not at the point where the list is built in Arguments::parse (arguments.cpp:322/325), which still accepts an unbounded number of names. Any future reader of args._context_attributes added earlier in start() — or any other entry point feeding JfrMetadata::initialize — would see the uncapped list again.

Suggestion: apply (or additionally apply) the cap in Arguments::parse where _context_attributes is populated.

return i >= 0 && (u32)i < DD_TAGS_CAPACITY;
}
public:
u32 getTag(int i) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LOW (consistency): getTag/setTag take a signed int index while both call sites (flightRecorder.cpp:2117 and threadLocalData.cpp:142) iterate with size_t, producing an implicit narrowing conversion on every call and requiring the extra i >= 0 half of isValidIndex. The neighboring, equivalent accessor ProfiledThread::getOtelTagEncoding (threadLocalData.h:404) already uses an unsigned index with a single-sided check.

Suggestion: take the index as u32 (or size_t) in both getTag/setTag and isValidIndex; isValidIndex then reduces to a single i < DD_TAGS_CAPACITY comparison.


Tag get_tag(int i) { return tags[i]; }
static bool isValidIndex(int i) {
return i >= 0 && (u32)i < DD_TAGS_CAPACITY;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LOW (test-adequacy, mutation-testing): No test in this PR would detect removing the i >= 0 && part of the bounds check in isValidIndex.

Suggestion: add a test verifying isValidIndex rejects negative indices.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

sphinx:spotcheck Sphinx: spot-check recommended

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants