Skip to content

fix(api): serialize tracer Stop against GetResult - #4973

Open
envestcc wants to merge 1 commit into
masterfrom
fix/tracer-stop-getresult-race
Open

fix(api): serialize tracer Stop against GetResult#4973
envestcc wants to merge 1 commit into
masterfrom
fix/tracer-stop-getresult-race

Conversation

@envestcc

Copy link
Copy Markdown
Member

The race

parseTracer arms a timeout watchdog that calls tracer.Stop from its own goroutine, while the goroutine running the trace calls GetResult. geth's StructLogger stores the interruption reason on a plain field:

func (l *StructLogger) Stop(err error) {
	l.reason = err            // plain write
	l.interrupt.Store(true)   // atomic, but *after*
}

func (l *StructLogger) GetResult() (json.RawMessage, error) {
	if l.reason != nil {      // plain read, never consults l.interrupt
		return nil, l.reason
	}
	...

reason is written and read without synchronisation, and the atomic interrupt establishes no happens-before edge for it because GetResult never reads it. Any debug_trace* request that hits its deadline can trip this.

The fix

Wrap the tracer so Stop and GetResult hold a common mutex. Hooks are passed through untouched — they run on the tracing goroutine and coordinate through l.interrupt, which is already atomic. Nothing in the geth fork is modified.

The guard is applied to every branch of parseTracer (default struct logger, configured struct logger, and named JS/native tracers), since the watchdog covers all of them.

Reproduction

TestParseTracerDefaultLoggerTimeoutFires already exercises the path; the race detector is what surfaces it, so no new test is needed.

On master (85fd452):

$ go test -count=1 -gcflags="all=-N -l" -short -race \
    -run TestParseTracerDefaultLoggerTimeoutFires ./api/
exit=1   WARNING: DATA RACE x2

With this commit:

exit=0   WARNING: DATA RACE x0

Full ./api/ package under the same flags: ok, 0 races.

Notes

Found while integrating this fix into rc_2.5.0; opening it separately against master so the branches don't diverge on it.

The trace timeout watchdog armed by parseTracer calls tracer.Stop from
its own goroutine while the goroutine running the trace calls
GetResult. geth's StructLogger stores the interruption reason on a plain
field: Stop writes l.reason and only then flips the atomic l.interrupt,
and GetResult reads l.reason without consulting that atomic. The two
therefore overlap with no happens-before edge between them, which the
race detector reports on every debug_trace* path that can time out.

Wrap the tracer so Stop and GetResult hold a common mutex. Hooks are
passed through untouched -- they run on the tracing goroutine and
coordinate through l.interrupt, which is already atomic.

Surfaced by TestParseTracerDefaultLoggerTimeoutFires under -race.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@envestcc
envestcc requested a review from a team as a code owner August 17, 2026 03:18
@sonarqubecloud

Copy link
Copy Markdown

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