Skip to content

Commit 52345f4

Browse files
Fix and detect race condition in clienttrace.go
ct.root can be updated without holding the mutex on ct. Also, addEvent can be called on a nil ct.root. Fix both of these issues by moving the mutex acquisition logic so that ct.root is only touched while the mutex is held.
1 parent 41c4e29 commit 52345f4

File tree

2 files changed

+52
-5
lines changed

2 files changed

+52
-5
lines changed

instrumentation/net/http/httptrace/otelhttptrace/clienttrace.go

+13-5
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,9 @@ func NewClientTrace(ctx context.Context, opts ...ClientTraceOption) *httptrace.C
186186
}
187187

188188
func (ct *clientTracer) start(hook, spanName string, attrs ...attribute.KeyValue) {
189+
ct.mtx.Lock()
190+
defer ct.mtx.Unlock()
191+
189192
if !ct.useSpans {
190193
if ct.root == nil {
191194
ct.root = trace.SpanFromContext(ct.Context)
@@ -194,9 +197,6 @@ func (ct *clientTracer) start(hook, spanName string, attrs ...attribute.KeyValue
194197
return
195198
}
196199

197-
ct.mtx.Lock()
198-
defer ct.mtx.Unlock()
199-
200200
if hookCtx, found := ct.activeHooks[hook]; !found {
201201
var sp trace.Span
202202
ct.activeHooks[hook], sp = ct.tr.Start(ct.getParentContext(hook), spanName, trace.WithAttributes(attrs...), trace.WithSpanKind(trace.SpanKindClient))
@@ -214,6 +214,13 @@ func (ct *clientTracer) start(hook, spanName string, attrs ...attribute.KeyValue
214214
}
215215

216216
func (ct *clientTracer) end(hook string, err error, attrs ...attribute.KeyValue) {
217+
ct.mtx.Lock()
218+
defer ct.mtx.Unlock()
219+
220+
if ct.root == nil {
221+
return
222+
}
223+
217224
if !ct.useSpans {
218225
if err != nil {
219226
attrs = append(attrs, attribute.String(hook+".error", err.Error()))
@@ -222,8 +229,6 @@ func (ct *clientTracer) end(hook string, err error, attrs ...attribute.KeyValue)
222229
return
223230
}
224231

225-
ct.mtx.Lock()
226-
defer ct.mtx.Unlock()
227232
if ctx, ok := ct.activeHooks[hook]; ok {
228233
span := trace.SpanFromContext(ctx)
229234
if err != nil {
@@ -321,6 +326,9 @@ func (ct *clientTracer) tlsHandshakeDone(_ tls.ConnectionState, err error) {
321326
}
322327

323328
func (ct *clientTracer) wroteHeaderField(k string, v []string) {
329+
if ct.root == nil {
330+
return
331+
}
324332
if ct.useSpans && ct.span("http.headers") == nil {
325333
ct.start("http.headers", "http.headers")
326334
}

instrumentation/net/http/httptrace/otelhttptrace/clienttrace_test.go

+39
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ import (
77
"context"
88
"fmt"
99
"net/http"
10+
"net/http/httptest"
1011
"net/http/httptrace"
12+
"sync"
13+
"testing"
1114

1215
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
1316
)
@@ -32,3 +35,39 @@ func ExampleNewClientTrace() {
3235

3336
fmt.Println(resp.Status)
3437
}
38+
39+
// TestNewClientParallelismWithoutSubspans tests running many Gets on a client simultaneously,
40+
// which would trigger a race condition if root were not protected by a mutex.
41+
func TestNewClientParallelismWithoutSubspans(t *testing.T) {
42+
t.Parallel()
43+
44+
makeClientTrace := func(ctx context.Context) *httptrace.ClientTrace {
45+
return NewClientTrace(ctx, WithoutSubSpans())
46+
}
47+
48+
server := httptest.NewServer(nil)
49+
50+
client := http.Client{
51+
Transport: otelhttp.NewTransport(
52+
server.Client().Transport,
53+
otelhttp.WithClientTrace(makeClientTrace),
54+
),
55+
}
56+
57+
var wg sync.WaitGroup
58+
59+
for i := 1; i < 10000; i++ {
60+
wg.Add(1)
61+
go func() {
62+
resp, err := client.Get("https://example.com")
63+
if err != nil {
64+
t.Error(err)
65+
return
66+
}
67+
resp.Body.Close()
68+
wg.Done()
69+
}()
70+
}
71+
72+
wg.Wait()
73+
}

0 commit comments

Comments
 (0)