Skip to content

Commit 233dd50

Browse files
committed
Use lock to prevent racy profile writes.
1 parent 77e0b31 commit 233dd50

File tree

3 files changed

+20
-43
lines changed

3 files changed

+20
-43
lines changed

src/runtime/mprof.go

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1341,31 +1341,6 @@ func goroutineLeakProfileWithLabelsConcurrent(p []profilerecord.StackRecord, lab
13411341
// Visit each leaked goroutine and try to record its stack.
13421342
var offset int
13431343
forEachGRace(func(gp1 *g) {
1344-
// NOTE(vsaioc): Each goroutine leak profile request is preceded by a run of the GC
1345-
// in goroutine leak detection mode. At the beginning of the GC cycle, all previously
1346-
// reported goroutine leaks are reset to _Gwaiting. As a result, incomplete goroutine
1347-
// leak profiles may be produced if multiple goroutine leak profile requests are issued
1348-
// concurrently.
1349-
//
1350-
// Example trace:
1351-
//
1352-
// G1 | GC | G2
1353-
// ----------------------+-----------------------------+---------------------
1354-
// Request profile | . | .
1355-
// . | . | Request profile
1356-
// . | [G1] Resets leaked g status | .
1357-
// . | [G1] Leaks detected | .
1358-
// . | <New cycle> | .
1359-
// . | [G2] Resets leaked g status | .
1360-
// . | . | Write profile
1361-
// . | [G2] Leaks detected | .
1362-
// Write profile | . | .
1363-
// ----------------------+-----------------------------+---------------------
1364-
// G1 completes profile |/\/\/\/\/\/\/\/\/\/\/\/\/\/\/| G2 misses leaks
1365-
//
1366-
// While this is a bug, normal use cases presume that goroutine leak profile
1367-
// requests are issued on a single track. Adding synchronization between
1368-
// goroutine leak profile requests would only needlessly increase overhead.
13691344
if readgstatus(gp1)&^_Gscan == _Gleaked {
13701345
systemstack(func() { saveg(^uintptr(0), ^uintptr(0), gp1, &p[offset], pcbuf) })
13711346
if labels != nil {

src/runtime/pprof/pprof.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,15 @@ var mutexProfile = &Profile{
228228
write: writeMutex,
229229
}
230230

231+
// goroutineLeakProfileLock ensures that the goroutine leak profile writer observes the
232+
// leaked goroutines discovered during the goroutine leak detection GC cycle
233+
// that was triggered when the profile was requested.
234+
//
235+
// This is needed to prevent a race condition between the garbage collector
236+
// and the goroutine leak profile writer when multiple profile requests are
237+
// issued concurrently.
238+
var goroutineLeakProfileLock sync.Mutex
239+
231240
func lockProfiles() {
232241
profiles.mu.Lock()
233242
if profiles.m == nil {
@@ -763,6 +772,15 @@ func writeGoroutine(w io.Writer, debug int) error {
763772
// writeGoroutineLeak first invokes a GC cycle that performs goroutine leak detection.
764773
// It then writes the goroutine profile, filtering for leaked goroutines.
765774
func writeGoroutineLeak(w io.Writer, debug int) error {
775+
// Acquire the goroutine leak detection lock and release
776+
// it after the goroutine leak profile is written.
777+
//
778+
// While the critical section is long, this is needed to prevent
779+
// a race condition between the garbage collector and the goroutine
780+
// leak profile writer when multiple profile requests are issued concurrently.
781+
goroutineLeakProfileLock.Lock()
782+
defer goroutineLeakProfileLock.Unlock()
783+
766784
// Run the GC with leak detection first so that leaked goroutines
767785
// may transition to the leaked state.
768786
runtime_goroutineLeakGC()

src/runtime/pprof/pprof_test.go

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1727,16 +1727,8 @@ func TestGoroutineLeakProfileConcurrency(t *testing.T) {
17271727
for ctx.Err() == nil {
17281728
var w strings.Builder
17291729
goroutineLeakProf.WriteTo(&w, 1)
1730-
// NOTE(vsaioc): We cannot always guarantee that the leak will
1731-
// actually be recorded in the profile when making concurrent
1732-
// goroutine leak requests, because the GC runs concurrently with
1733-
// the profiler and may reset the leaked goroutines' status before
1734-
// a concurrent profiler has the chance to record them. However,
1735-
// the goroutine leak count will persist throughout.
1736-
//
1737-
// Other tests are not expected to leak goroutines,
1738-
// so the count should be consistent.
17391730
countLeaks(t, 2*leakCount, w.String())
1731+
includesLeak(t, "goroutineleak", w.String())
17401732
}
17411733
}()
17421734
})
@@ -1760,16 +1752,8 @@ func TestGoroutineLeakProfileConcurrency(t *testing.T) {
17601752
for ctx.Err() == nil {
17611753
var w strings.Builder
17621754
goroutineLeakProf.WriteTo(&w, 1)
1763-
// NOTE(vsaioc): We cannot always guarantee that the leak will
1764-
// actually be recorded in the profile when making concurrent
1765-
// goroutine leak requests, because the GC runs concurrently with
1766-
// the profiler and may reset the leaked goroutines' status before
1767-
// a concurrent profiler has the chance to record them. However,
1768-
// the goroutine leak count will persist throughout.
1769-
//
1770-
// Other tests are not expected to leak goroutines,
1771-
// so the count should be consistent.
17721755
countLeaks(t, 2*leakCount, w.String())
1756+
includesLeak(t, "goroutineleak", w.String())
17731757
}
17741758
}()
17751759
go func() {

0 commit comments

Comments
 (0)