Skip to content

Commit ea2de33

Browse files
committed
runtime: detect and report zombie slots during sweeping
A zombie slot is a slot that is marked, but isn't allocated. This can indicate a bug in the GC, or a bad use of unsafe.Pointer. Currently, the sweeper has best-effort detection for zombie slots: if there are more marked slots than allocated slots, then there must have been a zombie slot. However, this is imprecise since it only compares totals and it reports almost no information that may be helpful to debug the issue. Add a precise check that compares the mark and allocation bitmaps and reports detailed information if it detects a zombie slot. No appreciable effect on performance as measured by the sweet benchmarks: name old time/op new time/op delta BiogoIgor 15.8s ± 2% 15.8s ± 2% ~ (p=0.421 n=24+25) BiogoKrishna 15.6s ± 2% 15.8s ± 5% ~ (p=0.082 n=22+23) BleveIndexBatch100 4.90s ± 3% 4.88s ± 2% ~ (p=0.627 n=25+24) CompileTemplate 204ms ± 1% 205ms ± 0% +0.22% (p=0.010 n=24+23) CompileUnicode 77.8ms ± 2% 78.0ms ± 1% ~ (p=0.236 n=25+24) CompileGoTypes 729ms ± 0% 731ms ± 0% +0.26% (p=0.000 n=24+24) CompileCompiler 3.52s ± 0% 3.52s ± 1% ~ (p=0.152 n=25+25) CompileSSA 8.06s ± 1% 8.05s ± 0% ~ (p=0.192 n=25+24) CompileFlate 132ms ± 1% 132ms ± 1% ~ (p=0.373 n=24+24) CompileGoParser 163ms ± 1% 164ms ± 1% +0.32% (p=0.003 n=24+25) CompileReflect 453ms ± 1% 455ms ± 1% +0.39% (p=0.000 n=22+22) CompileTar 181ms ± 1% 181ms ± 1% +0.20% (p=0.029 n=24+21) CompileXML 244ms ± 1% 244ms ± 1% ~ (p=0.065 n=24+24) CompileStdCmd 15.8s ± 2% 15.7s ± 2% ~ (p=0.059 n=23+24) FoglemanFauxGLRenderRotateBoat 13.4s ±11% 12.8s ± 0% ~ (p=0.377 n=25+24) FoglemanPathTraceRenderGopherIter1 18.6s ± 0% 18.6s ± 0% ~ (p=0.696 n=23+24) GopherLuaKNucleotide 28.7s ± 4% 28.6s ± 5% ~ (p=0.700 n=25+25) MarkdownRenderXHTML 250ms ± 1% 248ms ± 1% -1.01% (p=0.000 n=24+24) [Geo mean] 1.60s 1.60s -0.11% (https://perf.golang.org/search?q=upload:20200517.6) For #38702. Change-Id: I8af1fefd5fbf7b9cb665b98f9c4b73d1d08eea81 Reviewed-on: https://go-review.googlesource.com/c/go/+/234100 Run-TryBot: Austin Clements <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Cherry Zhang <[email protected]>
1 parent 9f4aeb3 commit ea2de33

File tree

3 files changed

+118
-0
lines changed

3 files changed

+118
-0
lines changed

src/runtime/gc_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"runtime"
1313
"runtime/debug"
1414
"sort"
15+
"strings"
1516
"sync"
1617
"sync/atomic"
1718
"testing"
@@ -192,6 +193,15 @@ func TestPeriodicGC(t *testing.T) {
192193
}
193194
}
194195

196+
func TestGcZombieReporting(t *testing.T) {
197+
// This test is somewhat sensitive to how the allocator works.
198+
got := runTestProg(t, "testprog", "GCZombie")
199+
want := "found pointer to free object"
200+
if !strings.Contains(got, want) {
201+
t.Fatalf("expected %q in output, but got %q", want, got)
202+
}
203+
}
204+
195205
func BenchmarkSetTypePtr(b *testing.B) {
196206
benchSetType(b, new(*byte))
197207
}

src/runtime/mgcsweep.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,10 +439,31 @@ func (s *mspan) sweep(preserve bool) bool {
439439
}
440440
}
441441

442+
// Check for zombie objects.
443+
if s.freeindex < s.nelems {
444+
// Everything < freeindex is allocated and hence
445+
// cannot be zombies.
446+
//
447+
// Check the first bitmap byte, where we have to be
448+
// careful with freeindex.
449+
obj := s.freeindex
450+
if (*s.gcmarkBits.bytep(obj / 8)&^*s.allocBits.bytep(obj / 8))>>(obj%8) != 0 {
451+
s.reportZombies()
452+
}
453+
// Check remaining bytes.
454+
for i := obj/8 + 1; i < divRoundUp(s.nelems, 8); i++ {
455+
if *s.gcmarkBits.bytep(i)&^*s.allocBits.bytep(i) != 0 {
456+
s.reportZombies()
457+
}
458+
}
459+
}
460+
442461
// Count the number of free objects in this span.
443462
nalloc := uint16(s.countAlloc())
444463
nfreed := s.allocCount - nalloc
445464
if nalloc > s.allocCount {
465+
// The zombie check above should have caught this in
466+
// more detail.
446467
print("runtime: nelems=", s.nelems, " nalloc=", nalloc, " previous allocCount=", s.allocCount, " nfreed=", nfreed, "\n")
447468
throw("sweep increased allocation count")
448469
}
@@ -755,6 +776,57 @@ func (s *mspan) oldSweep(preserve bool) bool {
755776
return res
756777
}
757778

779+
// reportZombies reports any marked but free objects in s and throws.
780+
//
781+
// This generally means one of the following:
782+
//
783+
// 1. User code converted a pointer to a uintptr and then back
784+
// unsafely, and a GC ran while the uintptr was the only reference to
785+
// an object.
786+
//
787+
// 2. User code (or a compiler bug) constructed a bad pointer that
788+
// points to a free slot, often a past-the-end pointer.
789+
//
790+
// 3. The GC two cycles ago missed a pointer and freed a live object,
791+
// but it was still live in the last cycle, so this GC cycle found a
792+
// pointer to that object and marked it.
793+
func (s *mspan) reportZombies() {
794+
printlock()
795+
print("runtime: marked free object in span ", s, ", elemsize=", s.elemsize, " freeindex=", s.freeindex, " (bad use of unsafe.Pointer? try -d=checkptr)\n")
796+
mbits := s.markBitsForBase()
797+
abits := s.allocBitsForIndex(0)
798+
for i := uintptr(0); i < s.nelems; i++ {
799+
addr := s.base() + i*s.elemsize
800+
print(hex(addr))
801+
alloc := i < s.freeindex || abits.isMarked()
802+
if alloc {
803+
print(" alloc")
804+
} else {
805+
print(" free ")
806+
}
807+
if mbits.isMarked() {
808+
print(" marked ")
809+
} else {
810+
print(" unmarked")
811+
}
812+
zombie := mbits.isMarked() && !alloc
813+
if zombie {
814+
print(" zombie")
815+
}
816+
print("\n")
817+
if zombie {
818+
length := s.elemsize
819+
if length > 1024 {
820+
length = 1024
821+
}
822+
hexdumpWords(addr, addr+length, nil)
823+
}
824+
mbits.advance()
825+
abits.advance()
826+
}
827+
throw("found pointer to free object")
828+
}
829+
758830
// deductSweepCredit deducts sweep credit for allocating a span of
759831
// size spanBytes. This must be performed *before* the span is
760832
// allocated to ensure the system has enough credit. If necessary, it

src/runtime/testdata/testprog/gc.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"runtime/debug"
1212
"sync/atomic"
1313
"time"
14+
"unsafe"
1415
)
1516

1617
func init() {
@@ -19,6 +20,7 @@ func init() {
1920
register("GCSys", GCSys)
2021
register("GCPhys", GCPhys)
2122
register("DeferLiveness", DeferLiveness)
23+
register("GCZombie", GCZombie)
2224
}
2325

2426
func GCSys() {
@@ -264,3 +266,37 @@ func DeferLiveness() {
264266
func escape(x interface{}) { sink2 = x; sink2 = nil }
265267

266268
var sink2 interface{}
269+
270+
// Test zombie object detection and reporting.
271+
func GCZombie() {
272+
// Allocate several objects of unusual size (so free slots are
273+
// unlikely to all be re-allocated by the runtime).
274+
const size = 190
275+
const count = 8192 / size
276+
keep := make([]*byte, 0, (count+1)/2)
277+
free := make([]uintptr, 0, (count+1)/2)
278+
zombies := make([]*byte, 0, len(free))
279+
for i := 0; i < count; i++ {
280+
obj := make([]byte, size)
281+
p := &obj[0]
282+
if i%2 == 0 {
283+
keep = append(keep, p)
284+
} else {
285+
free = append(free, uintptr(unsafe.Pointer(p)))
286+
}
287+
}
288+
289+
// Free the unreferenced objects.
290+
runtime.GC()
291+
292+
// Bring the free objects back to life.
293+
for _, p := range free {
294+
zombies = append(zombies, (*byte)(unsafe.Pointer(p)))
295+
}
296+
297+
// GC should detect the zombie objects.
298+
runtime.GC()
299+
println("failed")
300+
runtime.KeepAlive(keep)
301+
runtime.KeepAlive(zombies)
302+
}

0 commit comments

Comments
 (0)