Skip to content

Commit 4f86f22

Browse files
neildgopherbot
authored andcommitted
testing/synctest, runtime: avoid panic when using linker-alloc WG from bubble
We associate WaitGroups with synctest bubbles by attaching a special to the WaitGroup. It is not possible to attach a special to a linker-allocated value, such as: var wg sync.WaitGroup Avoid panicking when accessing a linker-allocated WaitGroup from a bubble. We have no way to associate these WaitGroups with a bubble, so just treat them as always unbubbled. This is probably fine, since the WaitGroup was always created outside the bubble in this case. Fixes #74005 Change-Id: Ic71514b0b8d0cecd62e45cc929ffcbeb16f54a55 Reviewed-on: https://go-review.googlesource.com/c/go/+/679695 Reviewed-by: Michael Knyszek <mknyszek@google.com> Auto-Submit: Damien Neil <dneil@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
1 parent 773701a commit 4f86f22

File tree

7 files changed

+97
-17
lines changed

7 files changed

+97
-17
lines changed

src/internal/synctest/synctest.go

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
package synctest
99

1010
import (
11+
"internal/abi"
1112
"unsafe"
1213
)
1314

@@ -22,14 +23,25 @@ func Wait()
2223
//go:linkname IsInBubble
2324
func IsInBubble() bool
2425

25-
// Associate associates p with the current bubble.
26-
// It returns false if p has an existing association with a different bubble.
27-
func Associate[T any](p *T) (ok bool) {
28-
return associate(unsafe.Pointer(p))
26+
// Association is the state of a pointer's bubble association.
27+
type Association int
28+
29+
const (
30+
Unbubbled = Association(iota) // not associated with any bubble
31+
CurrentBubble // associated with the current bubble
32+
OtherBubble // associated with a different bubble
33+
)
34+
35+
// Associate attempts to associate p with the current bubble.
36+
// It returns the new association status of p.
37+
func Associate[T any](p *T) Association {
38+
// Ensure p escapes to permit us to attach a special to it.
39+
escapedP := abi.Escape(p)
40+
return Association(associate(unsafe.Pointer(escapedP)))
2941
}
3042

3143
//go:linkname associate
32-
func associate(p unsafe.Pointer) bool
44+
func associate(p unsafe.Pointer) int
3345

3446
// Disassociate disassociates p from any bubble.
3547
func Disassociate[T any](p *T) {

src/internal/synctest/synctest_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -731,6 +731,34 @@ func TestWaitGroupMovedBetweenBubblesAfterWait(t *testing.T) {
731731
})
732732
}
733733

734+
var testWaitGroupLinkerAllocatedWG sync.WaitGroup
735+
736+
func TestWaitGroupLinkerAllocated(t *testing.T) {
737+
synctest.Run(func() {
738+
// This WaitGroup is probably linker-allocated and has no span,
739+
// so we won't be able to add a special to it associating it with
740+
// this bubble.
741+
//
742+
// Operations on it may not be durably blocking,
743+
// but they shouldn't fail.
744+
testWaitGroupLinkerAllocatedWG.Go(func() {})
745+
testWaitGroupLinkerAllocatedWG.Wait()
746+
})
747+
}
748+
749+
var testWaitGroupHeapAllocatedWG = new(sync.WaitGroup)
750+
751+
func TestWaitGroupHeapAllocated(t *testing.T) {
752+
synctest.Run(func() {
753+
// This package-scoped WaitGroup var should have been heap-allocated,
754+
// so we can associate it with a bubble.
755+
testWaitGroupHeapAllocatedWG.Add(1)
756+
go testWaitGroupHeapAllocatedWG.Wait()
757+
synctest.Wait()
758+
testWaitGroupHeapAllocatedWG.Done()
759+
})
760+
}
761+
734762
func TestHappensBefore(t *testing.T) {
735763
// Use two parallel goroutines accessing different vars to ensure that
736764
// we correctly account for multiple goroutines in the bubble.

src/runtime/export_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1911,3 +1911,9 @@ func (b BitCursor) Write(data *byte, cnt uintptr) {
19111911
func (b BitCursor) Offset(cnt uintptr) BitCursor {
19121912
return BitCursor{b: b.b.offset(cnt)}
19131913
}
1914+
1915+
const (
1916+
BubbleAssocUnbubbled = bubbleAssocUnbubbled
1917+
BubbleAssocCurrentBubble = bubbleAssocCurrentBubble
1918+
BubbleAssocOtherBubble = bubbleAssocOtherBubble
1919+
)

src/runtime/synctest.go

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,13 @@ type specialBubble struct {
363363
bubbleid uint64
364364
}
365365

366+
// Keep these in sync with internal/synctest.
367+
const (
368+
bubbleAssocUnbubbled = iota // not associated with any bubble
369+
bubbleAssocCurrentBubble // associated with the current bubble
370+
bubbleAssocOtherBubble // associated with a different bubble
371+
)
372+
366373
// getOrSetBubbleSpecial checks the special record for p's bubble membership.
367374
//
368375
// If add is true and p is not associated with any bubble,
@@ -371,10 +378,12 @@ type specialBubble struct {
371378
// It returns ok==true if p is associated with bubbleid
372379
// (including if a new association was added),
373380
// and ok==false if not.
374-
func getOrSetBubbleSpecial(p unsafe.Pointer, bubbleid uint64, add bool) (ok bool) {
381+
func getOrSetBubbleSpecial(p unsafe.Pointer, bubbleid uint64, add bool) (assoc int) {
375382
span := spanOfHeap(uintptr(p))
376383
if span == nil {
377-
throw("getOrSetBubbleSpecial on invalid pointer")
384+
// This is probably a package var.
385+
// We can't attach a special to it, so always consider it unbubbled.
386+
return bubbleAssocUnbubbled
378387
}
379388

380389
// Ensure that the span is swept.
@@ -393,7 +402,11 @@ func getOrSetBubbleSpecial(p unsafe.Pointer, bubbleid uint64, add bool) (ok bool
393402
// p is already associated with a bubble.
394403
// Return true iff it's the same bubble.
395404
s := (*specialBubble)((unsafe.Pointer)(*iter))
396-
ok = s.bubbleid == bubbleid
405+
if s.bubbleid == bubbleid {
406+
assoc = bubbleAssocCurrentBubble
407+
} else {
408+
assoc = bubbleAssocOtherBubble
409+
}
397410
} else if add {
398411
// p is not associated with a bubble,
399412
// and we've been asked to add an association.
@@ -404,23 +417,23 @@ func getOrSetBubbleSpecial(p unsafe.Pointer, bubbleid uint64, add bool) (ok bool
404417
s.special.next = *iter
405418
*iter = (*special)(unsafe.Pointer(s))
406419
spanHasSpecials(span)
407-
ok = true
420+
assoc = bubbleAssocCurrentBubble
408421
} else {
409422
// p is not associated with a bubble.
410-
ok = false
423+
assoc = bubbleAssocUnbubbled
411424
}
412425

413426
unlock(&span.speciallock)
414427
releasem(mp)
415428

416-
return ok
429+
return assoc
417430
}
418431

419432
// synctest_associate associates p with the current bubble.
420433
// It returns false if p is already associated with a different bubble.
421434
//
422435
//go:linkname synctest_associate internal/synctest.associate
423-
func synctest_associate(p unsafe.Pointer) (ok bool) {
436+
func synctest_associate(p unsafe.Pointer) int {
424437
return getOrSetBubbleSpecial(p, getg().bubble.id, true)
425438
}
426439

@@ -435,5 +448,5 @@ func synctest_disassociate(p unsafe.Pointer) {
435448
//
436449
//go:linkname synctest_isAssociated internal/synctest.isAssociated
437450
func synctest_isAssociated(p unsafe.Pointer) bool {
438-
return getOrSetBubbleSpecial(p, getg().bubble.id, false)
451+
return getOrSetBubbleSpecial(p, getg().bubble.id, false) == bubbleAssocCurrentBubble
439452
}

src/runtime/synctest_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
package runtime_test
66

77
import (
8+
"internal/synctest"
9+
"runtime"
810
"testing"
911
)
1012

@@ -15,3 +17,13 @@ func TestSynctest(t *testing.T) {
1517
t.Fatalf("output:\n%s\n\nwanted:\n%s", output, want)
1618
}
1719
}
20+
21+
// TestSynctestAssocConsts verifies that constants defined
22+
// in both runtime and internal/synctest match.
23+
func TestSynctestAssocConsts(t *testing.T) {
24+
if runtime.BubbleAssocUnbubbled != synctest.Unbubbled ||
25+
runtime.BubbleAssocCurrentBubble != synctest.CurrentBubble ||
26+
runtime.BubbleAssocOtherBubble != synctest.OtherBubble {
27+
t.Fatal("mismatch: runtime.BubbleAssoc? != synctest.*")
28+
}
29+
}

src/sync/waitgroup.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,13 +83,17 @@ func (wg *WaitGroup) Add(delta int) {
8383
race.Disable()
8484
defer race.Enable()
8585
}
86+
bubbled := false
8687
if synctest.IsInBubble() {
8788
// If Add is called from within a bubble, then all Add calls must be made
8889
// from the same bubble.
89-
if !synctest.Associate(wg) {
90+
switch synctest.Associate(wg) {
91+
case synctest.Unbubbled:
92+
case synctest.OtherBubble:
9093
// wg is already associated with a different bubble.
9194
fatal("sync: WaitGroup.Add called from multiple synctest bubbles")
92-
} else {
95+
case synctest.CurrentBubble:
96+
bubbled = true
9397
state := wg.state.Or(waitGroupBubbleFlag)
9498
if state != 0 && state&waitGroupBubbleFlag == 0 {
9599
// Add has been called from outside this bubble.
@@ -98,7 +102,7 @@ func (wg *WaitGroup) Add(delta int) {
98102
}
99103
}
100104
state := wg.state.Add(uint64(delta) << 32)
101-
if state&waitGroupBubbleFlag != 0 && !synctest.IsInBubble() {
105+
if state&waitGroupBubbleFlag != 0 && !bubbled {
102106
// Add has been called from within a synctest bubble (and we aren't in one).
103107
fatal("sync: WaitGroup.Add called from inside and outside synctest bubble")
104108
}
@@ -116,7 +120,7 @@ func (wg *WaitGroup) Add(delta int) {
116120
if w != 0 && delta > 0 && v == int32(delta) {
117121
panic("sync: WaitGroup misuse: Add called concurrently with Wait")
118122
}
119-
if v == 0 && state&waitGroupBubbleFlag != 0 {
123+
if v == 0 && bubbled {
120124
// Disassociate the WaitGroup from its bubble.
121125
synctest.Disassociate(wg)
122126
if w == 0 {

src/testing/synctest/synctest.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,11 @@
9393
// A [sync.WaitGroup] becomes associated with a bubble on the first
9494
// call to Add or Go. Once a WaitGroup is associated with a bubble,
9595
// calling Add or Go from outside that bubble is a fatal error.
96+
// (As a technical limitation, a WaitGroup defined as a package
97+
// variable, such as "var wg sync.WaitGroup", cannot be associated
98+
// with a bubble and operations on it may not be durably blocking.
99+
// This limitation does not apply to a *WaitGroup stored in a
100+
// package variable, such as "var wg = new(sync.WaitGroup)".)
96101
//
97102
// [sync.Cond.Wait] is durably blocking. Waking a goroutine in a bubble
98103
// blocked on Cond.Wait from outside the bubble is a fatal error.

0 commit comments

Comments
 (0)