Commit 8fb182d0 authored by Austin Clements's avatar Austin Clements

runtime: never pass stack pointers to gopark

gopark calls the unlock function after setting the G to _Gwaiting.
This means it's generally unsafe to access the G's stack from the
unlock function because the G may start running on another P. Once we
start shrinking stacks concurrently, a stack shrink could also move
the stack the moment after it enters _Gwaiting and before the unlock
function is called.

Document this restriction and fix the two places where we currently
violate it.

This is unlikely to be a problem in practice for these two places
right now, but they're already skating on thin ice. For example, the
following sequence could in principle cause corruption, deadlock, or a
panic in the select code:

On M1/P1:
1. G1 selects on channels A and B.
2. selectgoImpl calls gopark.
3. gopark puts G1 in _Gwaiting.
4. gopark calls selparkcommit.
5. selparkcommit releases the lock on channel A.

On M2/P2:
6. G2 sends to channel A.
7. The send puts G1 in _Grunnable and puts it on P2's run queue.
8. The scheduler runs, selects G1, puts it in _Grunning, and resumes G1.
9. On G1, the sellock immediately following the gopark gets called.
10. sellock grows and moves the stack.

On M1/P1:
11. selparkcommit continues to scan the lock order for the next
channel to unlock, but it's now reading from a freed (and possibly
reused) stack.

This shouldn't happen in practice because step 10 isn't the first call
to sellock, so the stack should already be big enough. However, once
we start shrinking stacks concurrently, this reasoning won't work any
more.

For #12967.

Change-Id: I3660c5be37e5be9f87433cb8141bdfdf37fadc4c
Reviewed-on: https://go-review.googlesource.com/20038Reviewed-by: 's avatarRick Hudson <rlh@golang.org>
Run-TryBot: Austin Clements <austin@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
parent 005140a7
......@@ -1345,15 +1345,21 @@ func gcBgMarkPrepare() {
}
func gcBgMarkWorker(_p_ *p) {
type parkInfo struct {
m *m // Release this m on park.
attach *p // If non-nil, attach to this p on park.
}
var park parkInfo
gp := getg()
park.m = acquirem()
park.attach = _p_
type parkInfo struct {
m muintptr // Release this m on park.
attach puintptr // If non-nil, attach to this p on park.
}
// We pass park to a gopark unlock function, so it can't be on
// the stack (see gopark). Prevent deadlock from recursively
// starting GC by disabling preemption.
gp.m.preemptoff = "GC worker init"
park := new(parkInfo)
gp.m.preemptoff = ""
park.m.set(acquirem())
park.attach.set(_p_)
// Inform gcBgMarkStartWorkers that this worker is ready.
// After this point, the background mark worker is scheduled
// cooperatively by gcController.findRunnable. Hence, it must
......@@ -1372,7 +1378,7 @@ func gcBgMarkWorker(_p_ *p) {
// The worker G is no longer running, so it's
// now safe to allow preemption.
releasem(park.m)
releasem(park.m.ptr())
// If the worker isn't attached to its P,
// attach now. During initialization and after
......@@ -1381,9 +1387,9 @@ func gcBgMarkWorker(_p_ *p) {
// attach, the owner P may schedule the
// worker, so this must be done after the G is
// stopped.
if park.attach != nil {
p := park.attach
park.attach = nil
if park.attach != 0 {
p := park.attach.ptr()
park.attach.set(nil)
// cas the worker because we may be
// racing with a new worker starting
// on this P.
......@@ -1394,7 +1400,7 @@ func gcBgMarkWorker(_p_ *p) {
}
}
return true
}, noescape(unsafe.Pointer(&park)), "GC worker (idle)", traceEvGoBlock, 0)
}, unsafe.Pointer(park), "GC worker (idle)", traceEvGoBlock, 0)
// Loop until the P dies and disassociates this
// worker (the P may later be reused, in which case
......@@ -1406,7 +1412,7 @@ func gcBgMarkWorker(_p_ *p) {
// Disable preemption so we can use the gcw. If the
// scheduler wants to preempt us, we'll stop draining,
// dispose the gcw, and then preempt.
park.m = acquirem()
park.m.set(acquirem())
if gcBlackenEnabled == 0 {
throw("gcBgMarkWorker: blackening not enabled")
......@@ -1469,7 +1475,7 @@ func gcBgMarkWorker(_p_ *p) {
// findRunnableGCWorker doesn't try to
// schedule it.
_p_.gcBgMarkWorker.set(nil)
releasem(park.m)
releasem(park.m.ptr())
gcMarkDone()
......@@ -1479,8 +1485,8 @@ func gcBgMarkWorker(_p_ *p) {
// We may be running on a different P at this
// point, so we can't reattach until this G is
// parked.
park.m = acquirem()
park.attach = _p_
park.m.set(acquirem())
park.attach.set(_p_)
}
}
}
......
......@@ -245,6 +245,8 @@ func Gosched() {
// Puts the current goroutine into a waiting state and calls unlockf.
// If unlockf returns false, the goroutine is resumed.
// unlockf must not access this G's stack, as it may be moved between
// the call to gopark and the call to unlockf.
func gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer, reason string, traceEv byte, traceskip int) {
mp := acquirem()
gp := mp.curg
......
......@@ -196,13 +196,27 @@ func selunlock(scases []scase, lockorder []uint16) {
}
}
func selparkcommit(gp *g, usel unsafe.Pointer) bool {
sel := (*hselect)(usel)
scaseslice := slice{unsafe.Pointer(&sel.scase), int(sel.ncase), int(sel.ncase)}
scases := *(*[]scase)(unsafe.Pointer(&scaseslice))
lockslice := slice{unsafe.Pointer(sel.lockorder), int(sel.ncase), int(sel.ncase)}
lockorder := *(*[]uint16)(unsafe.Pointer(&lockslice))
selunlock(scases, lockorder)
func selparkcommit(gp *g, _ unsafe.Pointer) bool {
// This must not access gp's stack (see gopark). In
// particular, it must not access the *hselect. That's okay,
// because by the time this is called, gp.waiting has all
// channels in lock order.
var lastc *hchan
for sg := gp.waiting; sg != nil; sg = sg.waitlink {
if sg.c != lastc && lastc != nil {
// As soon as we unlock the channel, fields in
// any sudog with that channel may change,
// including c and waitlink. Since multiple
// sudogs may have the same channel, we unlock
// only after we've passed the last instance
// of a channel.
unlock(&lastc.lock)
}
lastc = sg.c
}
if lastc != nil {
unlock(&lastc.lock)
}
return true
}
......@@ -406,7 +420,7 @@ loop:
// wait for someone to wake us up
gp.param = nil
gopark(selparkcommit, unsafe.Pointer(sel), "select", traceEvGoBlockSelect, 2)
gopark(selparkcommit, nil, "select", traceEvGoBlockSelect, 2)
// someone woke us up
sellock(scases, lockorder)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment