Skip to content

sdk/trace: fix deadlock in RecordError on reentrant error formatting - #8846

Open
sooraj-sky wants to merge 2 commits into
open-telemetry:mainfrom
sooraj-sky:fix/record-error-reentrancy-deadlock
Open

sdk/trace: fix deadlock in RecordError on reentrant error formatting#8846
sooraj-sky wants to merge 2 commits into
open-telemetry:mainfrom
sooraj-sky:fix/record-error-reentrancy-deadlock

Conversation

@sooraj-sky

Copy link
Copy Markdown

Summary

  • recordingSpan.RecordError acquired the span mutex before evaluating the caller-supplied error via err.Error(). If that method calls back into the same span (e.g. AddEvent), it deadlocks on the non-reentrant mutex.
  • This was introduced by Performance improvements for the trace SDK in Span. #5874, which moved lock acquisition ahead of error formatting.
  • Fix: evaluate err.Error() and the exception type before acquiring the lock, so arbitrary caller code in Error() can safely record telemetry on the span without deadlocking.
  • Added the regression test from the issue (TestRecordErrorAllowsReentrantErrorFormatting), which times out on main and passes after the fix.
  • Updated CHANGELOG.md under Unreleased/Fixed.

Fixes #8782

Test plan

  • go test ./trace -run '^TestRecordErrorAllowsReentrantErrorFormatting$' -count=1 — times out/deadlocks before the fix, passes after
  • go test ./trace/... -count=1 — all 365 tests pass
  • make precommit — lint, generate, go-mod-tidy, license-check, misspell, verify-readmes, verify-mods, and the default test suite all pass

RecordError acquired the span mutex before evaluating the caller-supplied
error via err.Error(). If that method calls back into the same span (e.g.
AddEvent), it deadlocks on the non-reentrant mutex.

Evaluate err.Error() and the exception type before acquiring the lock so
arbitrary caller code in Error() can safely record telemetry on the span.

Fixes open-telemetry#8782

Signed-off-by: Sooraj Macbook <ops@skywalks.in>
Signed-off-by: Sooraj Macbook <ops@skywalks.in>

@MrAlias MrAlias 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.

The callback needs to remain outside the span mutex without changing RecordError's no-op contract; the regression coverage should pin both sides of that invariant.

_, span := tp.Tracer(t.Name()).Start(t.Context(), "span")
t.Cleanup(func() { span.End() })

span.RecordError(reentrantRecordError{span: span})

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.

This only proves that the call returns. It would also pass if RecordError became a no-op or stopped recording either the reentrant event or the exception event, so it does not protect the behavior this fix is changing. Could we inspect the recording span's events and assert that both "from Error" and semconv.ExceptionEventName are present, including the exception message?

Comment thread sdk/trace/span.go
// err.Error() is arbitrary user code and may call back into this span
// (e.g. AddEvent), which would deadlock on the non-reentrant mutex below.
errType := typeStr(err)
errMsg := err.Error()

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.

RecordError promises to do nothing when the span is not recording, but err.Error() now runs before that state is checked. After span.End(), this can execute arbitrary formatter side effects or panic even though the method should be a no-op. Could we perform an initial recording check, format without holding the mutex, and recheck under the lock before adding the event? A test with an ended span and an error that records whether Error() was called would pin this contract.

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.

sdk/trace: RecordError deadlocks when error.Error re-enters the span

2 participants