runtime: ensure mheap lock stack growth invariant is maintained

Currently there's an invariant in the runtime wherein the heap lock
can only be acquired on the system stack, otherwise a self-deadlock
could occur if the stack grows while the lock is held.

This invariant is upheld and documented in a number of situations (e.g.
allocManual, freeManual) but there are other places where the invariant
is either not maintained at all which risks self-deadlock (e.g.
setGCPercent, gcResetMarkState, allocmcache) or is maintained but
undocumented (e.g. gcSweep, readGCStats_m).

This change adds go:systemstack to any function that acquires the heap
lock or adds a systemstack(func() { ... }) around the critical section,
where appropriate. It also documents the invariant on (*mheap).lock
directly and updates repetitive documentation to refer to that comment.

Fixes #32105.

Change-Id: I702b1290709c118b837389c78efde25c51a2cafb
Reviewed-on: https://go-review.googlesource.com/c/go/+/177857
Run-TryBot: Michael Knyszek <mknyszek@google.com>
Reviewed-by: Austin Clements <austin@google.com>
diff --git a/src/runtime/export_test.go b/src/runtime/export_test.go
index 3c3e110..62b7730 100644
--- a/src/runtime/export_test.go
+++ b/src/runtime/export_test.go
@@ -545,18 +545,23 @@
 }
 
 func AllocSpan(base, npages uintptr, scavenged bool) Span {
-	lock(&mheap_.lock)
-	s := (*mspan)(mheap_.spanalloc.alloc())
-	unlock(&mheap_.lock)
+	var s *mspan
+	systemstack(func() {
+		lock(&mheap_.lock)
+		s = (*mspan)(mheap_.spanalloc.alloc())
+		unlock(&mheap_.lock)
+	})
 	s.init(base, npages)
 	s.scavenged = scavenged
 	return Span{s}
 }
 
 func (s *Span) Free() {
-	lock(&mheap_.lock)
-	mheap_.spanalloc.free(unsafe.Pointer(s.mspan))
-	unlock(&mheap_.lock)
+	systemstack(func() {
+		lock(&mheap_.lock)
+		mheap_.spanalloc.free(unsafe.Pointer(s.mspan))
+		unlock(&mheap_.lock)
+	})
 	s.mspan = nil
 }
 
@@ -629,9 +634,11 @@
 	// allocation which requires the mheap_ lock to manipulate.
 	// Locking here is safe because the treap itself never allocs
 	// or otherwise ends up grabbing this lock.
-	lock(&mheap_.lock)
-	t.insert(s.mspan)
-	unlock(&mheap_.lock)
+	systemstack(func() {
+		lock(&mheap_.lock)
+		t.insert(s.mspan)
+		unlock(&mheap_.lock)
+	})
 	t.CheckInvariants()
 }
 
@@ -644,17 +651,21 @@
 	// freeing which requires the mheap_ lock to manipulate.
 	// Locking here is safe because the treap itself never allocs
 	// or otherwise ends up grabbing this lock.
-	lock(&mheap_.lock)
-	t.erase(i.treapIter)
-	unlock(&mheap_.lock)
+	systemstack(func() {
+		lock(&mheap_.lock)
+		t.erase(i.treapIter)
+		unlock(&mheap_.lock)
+	})
 	t.CheckInvariants()
 }
 
 func (t *Treap) RemoveSpan(s Span) {
 	// See Erase about locking.
-	lock(&mheap_.lock)
-	t.removeSpan(s.mspan)
-	unlock(&mheap_.lock)
+	systemstack(func() {
+		lock(&mheap_.lock)
+		t.removeSpan(s.mspan)
+		unlock(&mheap_.lock)
+	})
 	t.CheckInvariants()
 }
 
diff --git a/src/runtime/mcache.go b/src/runtime/mcache.go
index 7895e48..0cb21f7 100644
--- a/src/runtime/mcache.go
+++ b/src/runtime/mcache.go
@@ -83,10 +83,13 @@
 var emptymspan mspan
 
 func allocmcache() *mcache {
-	lock(&mheap_.lock)
-	c := (*mcache)(mheap_.cachealloc.alloc())
-	c.flushGen = mheap_.sweepgen
-	unlock(&mheap_.lock)
+	var c *mcache
+	systemstack(func() {
+		lock(&mheap_.lock)
+		c = (*mcache)(mheap_.cachealloc.alloc())
+		c.flushGen = mheap_.sweepgen
+		unlock(&mheap_.lock)
+	})
 	for i := range c.alloc {
 		c.alloc[i] = &emptymspan
 	}
diff --git a/src/runtime/mgc.go b/src/runtime/mgc.go
index 9eaacd9..823b556 100644
--- a/src/runtime/mgc.go
+++ b/src/runtime/mgc.go
@@ -216,16 +216,19 @@
 
 //go:linkname setGCPercent runtime/debug.setGCPercent
 func setGCPercent(in int32) (out int32) {
-	lock(&mheap_.lock)
-	out = gcpercent
-	if in < 0 {
-		in = -1
-	}
-	gcpercent = in
-	heapminimum = defaultHeapMinimum * uint64(gcpercent) / 100
-	// Update pacing in response to gcpercent change.
-	gcSetTriggerRatio(memstats.triggerRatio)
-	unlock(&mheap_.lock)
+	// Run on the system stack since we grab the heap lock.
+	systemstack(func() {
+		lock(&mheap_.lock)
+		out = gcpercent
+		if in < 0 {
+			in = -1
+		}
+		gcpercent = in
+		heapminimum = defaultHeapMinimum * uint64(gcpercent) / 100
+		// Update pacing in response to gcpercent change.
+		gcSetTriggerRatio(memstats.triggerRatio)
+		unlock(&mheap_.lock)
+	})
 
 	// If we just disabled GC, wait for any concurrent GC mark to
 	// finish so we always return with no GC running.
@@ -1261,7 +1264,7 @@
 
 	gcBgMarkStartWorkers()
 
-	gcResetMarkState()
+	systemstack(gcResetMarkState)
 
 	work.stwprocs, work.maxprocs = gomaxprocs, gomaxprocs
 	if work.stwprocs > ncpu {
@@ -2078,6 +2081,9 @@
 	}
 }
 
+// gcSweep must be called on the system stack because it acquires the heap
+// lock. See mheap for details.
+//go:systemstack
 func gcSweep(mode gcMode) {
 	if gcphase != _GCoff {
 		throw("gcSweep being done but phase is not GCoff")
@@ -2134,6 +2140,11 @@
 //
 // This is safe to do without the world stopped because any Gs created
 // during or after this will start out in the reset state.
+//
+// gcResetMarkState must be called on the system stack because it acquires
+// the heap lock. See mheap for details.
+//
+//go:systemstack
 func gcResetMarkState() {
 	// This may be called during a concurrent phase, so make sure
 	// allgs doesn't change.
diff --git a/src/runtime/mheap.go b/src/runtime/mheap.go
index 3297c28..af2818a 100644
--- a/src/runtime/mheap.go
+++ b/src/runtime/mheap.go
@@ -29,6 +29,8 @@
 //
 //go:notinheap
 type mheap struct {
+	// lock must only be acquired on the system stack, otherwise a g
+	// could self-deadlock if its stack grows with the lock held.
 	lock      mutex
 	free      mTreap // free spans
 	sweepgen  uint32 // sweep generation, see comment in mspan
@@ -1095,9 +1097,8 @@
 // The memory backing the returned span may not be zeroed if
 // span.needzero is set.
 //
-// allocManual must be called on the system stack to prevent stack
-// growth. Since this is used by the stack allocator, stack growth
-// during allocManual would self-deadlock.
+// allocManual must be called on the system stack because it acquires
+// the heap lock. See mheap for details.
 //
 //go:systemstack
 func (h *mheap) allocManual(npage uintptr, stat *uint64) *mspan {
@@ -1303,8 +1304,8 @@
 // This must only be called when gcphase == _GCoff. See mSpanState for
 // an explanation.
 //
-// freeManual must be called on the system stack to prevent stack
-// growth, just like allocManual.
+// freeManual must be called on the system stack because it acquires
+// the heap lock. See mheap for details.
 //
 //go:systemstack
 func (h *mheap) freeManual(s *mspan, stat *uint64) {
diff --git a/src/runtime/mstats.go b/src/runtime/mstats.go
index 9250865..421580e 100644
--- a/src/runtime/mstats.go
+++ b/src/runtime/mstats.go
@@ -470,6 +470,9 @@
 	})
 }
 
+// readGCStats_m must be called on the system stack because it acquires the heap
+// lock. See mheap for details.
+//go:systemstack
 func readGCStats_m(pauses *[]uint64) {
 	p := *pauses
 	// Calling code in runtime/debug should make the slice large enough.