Skip to content

Self-referential attribute value crashes the instrumented application with RecursionError - #5565

Open
dwin-gharibi wants to merge 4 commits into
open-telemetry:mainfrom
dwin-gharibi:fix/attribute-value-recursion-guard
Open

Self-referential attribute value crashes the instrumented application with RecursionError#5565
dwin-gharibi wants to merge 4 commits into
open-telemetry:mainfrom
dwin-gharibi:fix/attribute-value-recursion-guard

Conversation

@dwin-gharibi

Copy link
Copy Markdown

Closes #5564.

Description

_clean_attribute_value walks nested Sequence and Mapping attribute values recursively with no cycle detection and no depth bound. A self-referential list or dict recurses until the interpreter stack is exhausted, and the resulting RecursionError propagates out of the public telemetry API into the calling application.

This became reachable when AnyValue was widened to accept arbitrarily nested values. Previously such an object would simply have been stringified.

Root cause

opentelemetry-api/src/opentelemetry/attributes/__init__.py: the Sequence branch at line 65 and the Mapping branch at line 87 recurse unconditionally. Recursion is bounded only by sys.getrecursionlimit().

Approach

Bound the recursion at 100 levels. The worker raises an internal marker past that depth and nothing inside it catches, so a value with an unrepresentable branch is rejected whole rather than left as a truncated husk of empty nesting.

Rejection is deliberately scoped to a single attribute value. A new _clean_attributes handles the attributes container, cleaning each value independently so one bad entry becomes None without discarding its siblings. Key validation moves into a shared _clean_key.

Measurement.__post_init__ previously passed the whole attributes mapping through the single-value path, so a rejected value would have dropped every attribute on the measurement. It now uses _clean_attributes, matching BoundedAttributes.

Files changed

  • opentelemetry-api/src/opentelemetry/attributes/__init__.py
  • opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/measurement.py
  • opentelemetry-api/tests/attributes/test_attributes.py
  • opentelemetry-sdk/tests/test_attribute_nesting_depth.py
  • .changelog/5562.fixed

Testing

Unit coverage in opentelemetry-api for cyclic sequences, cyclic mappings, excessive depth, and - importantly - that nesting within the limit is still cleaned normally, so the guard cannot silently swallow legitimate data.

Integration coverage in opentelemetry-sdk exercises all three signals plus Resource.create, and asserts that a sibling attribute recorded alongside a rejected one survives. All seven fail with RecursionError before the change; the sibling-preservation test is what caught the Measurement container bug.

Result: 279 passed in opentelemetry-api (273 baseline plus 6 new), 862 passed in opentelemetry-sdk (855 baseline plus 7 new).

Risk / compatibility

Behavioural change only for values that previously crashed. The 100 level limit is far above any realistic attribute: OTLP consumers and storage backends are not designed for deeply nested attribute values, and the semantic conventions do not define any. A rejected value is logged at WARNING so it is diagnosable rather than silent.

`AnyValue` accepts arbitrarily nested Sequence and Mapping values, and
`_clean_attribute_value` walks them recursively. Add coverage asserting that a
self-referential list, a self-referential mapping and a pathologically deep
value are rejected instead of exhausting the interpreter stack, and that
nesting within the limit is still cleaned normally.

These tests fail with RecursionError against the current implementation.
Cover the end-to-end contract across all three signals plus resource
construction: set_attribute, set_attributes, add_event, Resource.create,
Counter.add and Logger.emit must not raise when handed a self-referential
value, and an unusable value must not discard the attributes beside it.

All seven fail with RecursionError against the current implementation.
Since `AnyValue` was widened to accept arbitrarily nested Sequence and Mapping
values, `_clean_attribute_value` walks user data recursively with no depth
bound. A self-referential list or dict therefore recurses until the
interpreter stack is exhausted and raises RecursionError straight out of
`set_attribute`, `set_attributes`, `add_event`, `Resource.create`,
`Counter.add` and `Logger.emit`. Telemetry must never be able to crash the
application it is observing.

Bound the recursion at 100 levels. The worker raises an internal marker past
that depth and nothing inside it catches, so a value with an unrepresentable
branch is rejected whole rather than left as a truncated husk.

Rejection is scoped to a single attribute value: add `_clean_attributes` for
the attributes container, which cleans each value independently so one bad
entry becomes None without discarding its siblings. `BoundedAttributes` and
`Measurement` both clean containers and now use it -- previously `Measurement`
passed the whole mapping through the single-value path, so any rejected value
would have dropped every attribute on the measurement.

Key validation is extracted into `_clean_key` and shared by both paths.
Copilot AI lite review requested due to automatic review settings August 23, 2026 16:04
@dwin-gharibi
dwin-gharibi requested a review from a team as a code owner August 23, 2026 16:04

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@opentelemetry-pr-dashboard

opentelemetry-pr-dashboard Bot commented Aug 23, 2026

Copy link
Copy Markdown

Pull request dashboard status

Waiting on the author · refreshed 2026-08-24 13:18 UTC

Respond to 1 review item (e.g. link a commit, explain why not, ask a follow-up):

  • Inline threads: 1
Status above doesn't look right?
  • Just replied or pushed? Anything around or after the refresh time above may not be picked up yet — give it a few minutes.
  • Should this be with reviewers? Comment /dashboard route:reviewers to route it to them.
  • Anything wrong — including the routing? Report it with what you expected; it helps us improve the dashboard.

# cleaning them recurses. Cap the depth: without it a self-referential value
# raises RecursionError out of set_attribute()/Resource.create()/Counter.add()
# and takes down the instrumented application.
_MAX_NESTING_DEPTH = 100

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.

100 seems too deep. Maybe we should cap this at like 20 ? @xrmx WDYT ?

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

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

Self-referential attribute value crashes the instrumented application with RecursionError

3 participants