Skip to content

Fix TraceStateBuilder.remove double-counting a repeated removal - #8763

Open
TimurRakhmatullin86 wants to merge 1 commit into
open-telemetry:mainfrom
TimurRakhmatullin86:fix/tracestate-builder-double-remove
Open

Fix TraceStateBuilder.remove double-counting a repeated removal#8763
TimurRakhmatullin86 wants to merge 1 commit into
open-telemetry:mainfrom
TimurRakhmatullin86:fix/tracestate-builder-double-remove

Conversation

@TimurRakhmatullin86

Copy link
Copy Markdown
Contributor

Problem

ArrayBasedTraceStateBuilder.remove(key) decrements numEntries unconditionally, even when the key's slot is already a null tombstone from a previous remove of the same key. put() guards the symmetric update (if (currentValue == null) { numEntries++; }); remove() has no mirror guard, so a repeated remove of an already-removed key double-decrements the count.

Because build() trusts numEntries, a repeated removal corrupts the result three different ways:

// 1) silent data loss — a valid entry disappears
TraceState.builder().put("a","1").put("b","2").remove("a").remove("a").build();
// numEntries: 2 -> 1 -> 0  =>  build() returns empty();  get("b") == null

// 2) null-valued entry — violates the non-null value contract
TraceState.builder().put("a","1").remove("a").remove("a").build();
// numEntries: 1 -> 0 -> -1 => size()==2 fast path returns [a, null]; size()==1, get("a")==null

// 3) crash
TraceState.builder().put("a","1").put("b","2").put("c","3").remove("a").remove("a").build();
// numEntries under-counts => entries[] is too small => ArrayIndexOutOfBoundsException

The remove javadoc says it removes the entry "if it is present", so a second remove of a key that is no longer present must be a no-op — the same as removing a key that was never added (which removeNotPresent already covers).

Fix

Only account for the removal (set the tombstone and decrement) when the entry is still present, mirroring the guard in put().

Tests

Added three cases in TraceStateTest for the data-loss, null-value, and ArrayIndexOutOfBoundsException paths. All three fail against the current code and pass with the fix; the existing TraceStateTest cases stay green.

@TimurRakhmatullin86
TimurRakhmatullin86 requested a review from a team as a code owner September 1, 2026 14:31
ArrayBasedTraceStateBuilder.remove decremented numEntries unconditionally,
even when the entry was already a null tombstone from a previous remove of the
same key. put() guards the symmetric update with if (currentValue == null);
remove() lacked the mirror guard.

So removing an already-removed key drove numEntries below the real count and
corrupted build(): it either returned empty() and dropped unrelated valid
entries, emitted a TraceState with a null value via the size()==2 fast path,
or threw ArrayIndexOutOfBoundsException from the undersized entries[] array.
The remove javadoc says it removes the entry 'if it is present', so a repeated
remove must be a no-op.

Only decrement when the entry is still present, and add tests for the three
corruption paths.

Signed-off-by: Timur Rakhmatullin <174210871+TimurRakhmatullin86@users.noreply.github.com>
@opentelemetry-pr-dashboard

opentelemetry-pr-dashboard Bot commented Sep 1, 2026

Copy link
Copy Markdown

Pull request dashboard status

Waiting on reviewers · refreshed 2026-09-02 14:36 UTC

Review the latest changes.

Also blocked by: Merge conflicts.

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.
  • Anything look wrong? Report it with what you expected; it helps us improve the dashboard.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant