Skip to content

Commit 44497eb

Browse files
committed
runtime: fix goroutine priority elevation
Currently it's possible for user code to exploit the high scheduler priority of the GC worker in conjunction with the runnext optimization to elevate a user goroutine to high priority so it will always run even if there are other runnable goroutines. For example, if a goroutine is in a tight allocation loop, the following can happen: 1. Goroutine 1 allocates, triggering a GC. 2. G 1 attempts an assist, but fails and blocks. 3. The scheduler runs the GC worker, since it is high priority. Note that this also starts a new scheduler quantum. 4. The GC worker does enough work to satisfy the assist. 5. The GC worker readies G 1, putting it in runnext. 6. GC finishes and the scheduler runs G 1 from runnext, giving it the rest of the GC worker's quantum. 7. Go to 1. Even if there are other goroutines on the run queue, they never get a chance to run in the above sequence. This requires a confluence of circumstances that make it unlikely, though not impossible, that it would happen in "real" code. In the test added by this commit, we force this confluence by setting GOMAXPROCS to 1 and GOGC to 1 so it's easy for the test to repeated trigger GC and wake from a blocked assist. We fix this by making GC always put user goroutines at the end of the run queue, instead of in runnext. This makes it so user code can't piggy-back on the GC's high priority to make a user goroutine act like it has high priority. The only other situation where GC wakes user goroutines is waking all blocked assists at the end, but this uses the global run queue and hence doesn't have this problem. Fixes #15706. Change-Id: I1589dee4b7b7d0c9c8575ed3472226084dfce8bc Reviewed-on: https://go-review.googlesource.com/23172 Reviewed-by: Rick Hudson <[email protected]>
1 parent 9174058 commit 44497eb

File tree

3 files changed

+50
-1
lines changed

3 files changed

+50
-1
lines changed

src/runtime/mgcmark.go

+7-1
Original file line numberDiff line numberDiff line change
@@ -601,7 +601,13 @@ func gcFlushBgCredit(scanWork int64) {
601601
gp.gcAssistBytes = 0
602602
xgp := gp
603603
gp = gp.schedlink.ptr()
604-
ready(xgp, 0, true)
604+
// It's important that we *not* put xgp in
605+
// runnext. Otherwise, it's possible for user
606+
// code to exploit the GC worker's high
607+
// scheduler priority to get itself always run
608+
// before other goroutines and always in the
609+
// fresh quantum started by GC.
610+
ready(xgp, 0, false)
605611
} else {
606612
// Partially satisfy this assist.
607613
gp.gcAssistBytes += scanBytes

src/runtime/proc_test.go

+8
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,14 @@ func TestGCFairness(t *testing.T) {
344344
}
345345
}
346346

347+
func TestGCFairness2(t *testing.T) {
348+
output := runTestProg(t, "testprog", "GCFairness2")
349+
want := "OK\n"
350+
if output != want {
351+
t.Fatalf("want %s, got %s\n", want, output)
352+
}
353+
}
354+
347355
func TestNumGoroutine(t *testing.T) {
348356
output := runTestProg(t, "testprog", "NumGoroutine")
349357
want := "1\n"

src/runtime/testdata/testprog/gc.go

+35
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,14 @@ import (
88
"fmt"
99
"os"
1010
"runtime"
11+
"runtime/debug"
12+
"sync/atomic"
1113
"time"
1214
)
1315

1416
func init() {
1517
register("GCFairness", GCFairness)
18+
register("GCFairness2", GCFairness2)
1619
register("GCSys", GCSys)
1720
}
1821

@@ -72,3 +75,35 @@ func GCFairness() {
7275
time.Sleep(10 * time.Millisecond)
7376
fmt.Println("OK")
7477
}
78+
79+
func GCFairness2() {
80+
// Make sure user code can't exploit the GC's high priority
81+
// scheduling to make scheduling of user code unfair. See
82+
// issue #15706.
83+
runtime.GOMAXPROCS(1)
84+
debug.SetGCPercent(1)
85+
var count [3]int64
86+
var sink [3]interface{}
87+
for i := range count {
88+
go func(i int) {
89+
for {
90+
sink[i] = make([]byte, 1024)
91+
atomic.AddInt64(&count[i], 1)
92+
}
93+
}(i)
94+
}
95+
// Note: If the unfairness is really bad, it may not even get
96+
// past the sleep.
97+
//
98+
// If the scheduling rules change, this may not be enough time
99+
// to let all goroutines run, but for now we cycle through
100+
// them rapidly.
101+
time.Sleep(30 * time.Millisecond)
102+
for i := range count {
103+
if atomic.LoadInt64(&count[i]) == 0 {
104+
fmt.Printf("goroutine %d did not run\n", i)
105+
return
106+
}
107+
}
108+
fmt.Println("OK")
109+
}

0 commit comments

Comments
 (0)