Skip to content

Commit 3df078f

Browse files
mknyszekgopherbot
authored andcommitted
runtime: add new GODEBUG checkfinalizer
This new debug mode detects cleanup/finalizer leaks using checkmark mode. It runs a partial GC using only specials as roots. If the GC can find a path from one of these roots back to the object the special is attached to, then the object might never be reclaimed. (The cycle could be broken in the future, but it's almost certainly a bug.) This debug mode is very barebones. It contains no type information and no stack location for where the finalizer or cleanup was created. For #72949. Change-Id: Ibffd64c1380b51f281950e4cfe61f677385d42a5 Reviewed-on: https://go-review.googlesource.com/c/go/+/634599 Reviewed-by: Carlos Amedee <[email protected]> Reviewed-by: Michael Pratt <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Auto-Submit: Michael Knyszek <[email protected]>
1 parent 2a65100 commit 3df078f

File tree

7 files changed

+237
-52
lines changed

7 files changed

+237
-52
lines changed

src/runtime/gc_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1073,3 +1073,17 @@ func TestMSpanQueue(t *testing.T) {
10731073
expectMSpan(t, p.Pop(), nil, "pop")
10741074
})
10751075
}
1076+
1077+
func TestDetectFinalizerAndCleanupLeaks(t *testing.T) {
1078+
got := runTestProg(t, "testprog", "DetectFinalizerAndCleanupLeaks", "GODEBUG=checkfinalizers=1")
1079+
sp := strings.SplitN(got, "runtime: detected", 2)
1080+
if len(sp) != 2 {
1081+
t.Fatalf("expected the runtime to throw, got:\n%s", got)
1082+
}
1083+
if strings.Count(sp[0], "finalizer") != 1 {
1084+
t.Fatalf("expected exactly one leaked finalizer, got:\n%s", got)
1085+
}
1086+
if strings.Count(sp[0], "cleanup") != 1 {
1087+
t.Fatalf("expected exactly one leaked finalizer, got:\n%s", got)
1088+
}
1089+
}

src/runtime/mcheckmark.go

Lines changed: 134 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -92,23 +92,150 @@ func setCheckmark(obj, base, off uintptr, mbits markBits) bool {
9292
getg().m.traceback = 2
9393
throw("checkmark found unmarked object")
9494
}
95+
bytep, mask := getCheckmark(obj)
96+
if bytep == nil {
97+
return false
98+
}
99+
if atomic.Load8(bytep)&mask != 0 {
100+
// Already checkmarked.
101+
return true
102+
}
103+
atomic.Or8(bytep, mask)
104+
return false
105+
}
95106

107+
func getCheckmark(obj uintptr) (bytep *byte, mask uint8) {
96108
ai := arenaIndex(obj)
97109
arena := mheap_.arenas[ai.l1()][ai.l2()]
98110
if arena == nil {
99111
// Non-heap pointer.
100-
return false
112+
return nil, 0
101113
}
102114
wordIdx := (obj - alignDown(obj, heapArenaBytes)) / goarch.PtrSize
103115
arenaWord := wordIdx / 8
104-
mask := byte(1 << (wordIdx % 8))
105-
bytep := &arena.checkmarks.b[arenaWord]
116+
mask = byte(1 << (wordIdx % 8))
117+
bytep = &arena.checkmarks.b[arenaWord]
118+
return bytep, mask
119+
}
106120

107-
if atomic.Load8(bytep)&mask != 0 {
108-
// Already checkmarked.
121+
// runCheckmark runs a full non-parallel, stop-the-world mark using
122+
// checkmark bits, to check that we didn't forget to mark anything
123+
// during the concurrent mark process.
124+
//
125+
// The world must be stopped to call runCheckmark.
126+
func runCheckmark(prepareRootSet func(*gcWork)) {
127+
assertWorldStopped()
128+
129+
// Turn off gcwaiting because that will force
130+
// gcDrain to return early if this goroutine
131+
// happens to have its preemption flag set.
132+
// This is fine because the world is stopped.
133+
// Restore it after we're done just to be safe.
134+
sched.gcwaiting.Store(false)
135+
startCheckmarks()
136+
gcResetMarkState()
137+
gcw := &getg().m.p.ptr().gcw
138+
prepareRootSet(gcw)
139+
gcDrain(gcw, 0)
140+
wbBufFlush1(getg().m.p.ptr())
141+
gcw.dispose()
142+
endCheckmarks()
143+
sched.gcwaiting.Store(true)
144+
}
145+
146+
// checkFinalizersAndCleanups uses checkmarks to check for potential issues
147+
// with the program's use of cleanups and finalizers.
148+
func checkFinalizersAndCleanups() {
149+
assertWorldStopped()
150+
151+
failed := false
152+
forEachSpecial(func(p uintptr, s *mspan, sp *special) bool {
153+
// We only care about finalizers and cleanups.
154+
if sp.kind != _KindSpecialFinalizer && sp.kind != _KindSpecialCleanup {
155+
return true
156+
}
157+
158+
// Run a checkmark GC using this cleanup and/or finalizer as a root.
159+
runCheckmark(func(gcw *gcWork) {
160+
switch sp.kind {
161+
case _KindSpecialFinalizer:
162+
gcScanFinalizer((*specialfinalizer)(unsafe.Pointer(sp)), s, gcw)
163+
case _KindSpecialCleanup:
164+
gcScanCleanup((*specialCleanup)(unsafe.Pointer(sp)), gcw)
165+
}
166+
})
167+
168+
// Now check to see if the object the special is attached to was marked.
169+
// The roots above do not directly mark p, so if it is marked, then p
170+
// must be reachable from the finalizer and/or cleanup, preventing
171+
// reclamation.
172+
bytep, mask := getCheckmark(p)
173+
if bytep == nil {
174+
return true
175+
}
176+
if atomic.Load8(bytep)&mask != 0 {
177+
if !failed {
178+
println("runtime: found possibly unreclaimable objects:")
179+
}
180+
failed = true
181+
kind := "cleanup"
182+
if sp.kind == _KindSpecialFinalizer {
183+
kind = "finalizer"
184+
}
185+
print("\t0x", hex(p), " leaked due to a ", kind)
186+
if sp.kind == _KindSpecialFinalizer {
187+
spf := (*specialfinalizer)(unsafe.Pointer(sp))
188+
print(" (", (rtype{spf.fint}).string(), ")\n")
189+
} else {
190+
println()
191+
}
192+
}
109193
return true
194+
})
195+
if failed {
196+
throw("runtime: detected possible cleanup and/or finalizer leak")
110197
}
198+
}
111199

112-
atomic.Or8(bytep, mask)
113-
return false
200+
// forEachSpecial is an iterator over all specials.
201+
//
202+
// Used by debug.checkfinalizers.
203+
//
204+
// The world must be stopped.
205+
func forEachSpecial(yield func(p uintptr, s *mspan, sp *special) bool) {
206+
assertWorldStopped()
207+
208+
// Find the arena and page index into that arena for this shard.
209+
for _, ai := range mheap_.markArenas {
210+
ha := mheap_.arenas[ai.l1()][ai.l2()]
211+
212+
// Construct slice of bitmap which we'll iterate over.
213+
for i := range ha.pageSpecials[:] {
214+
// Find set bits, which correspond to spans with specials.
215+
specials := atomic.Load8(&ha.pageSpecials[i])
216+
if specials == 0 {
217+
continue
218+
}
219+
for j := uint(0); j < 8; j++ {
220+
if specials&(1<<j) == 0 {
221+
continue
222+
}
223+
// Find the span for this bit.
224+
//
225+
// This value is guaranteed to be non-nil because having
226+
// specials implies that the span is in-use, and since we're
227+
// currently marking we can be sure that we don't have to worry
228+
// about the span being freed and re-used.
229+
s := ha.spans[uint(i)*8+j]
230+
231+
// Lock the specials to prevent a special from being
232+
// removed from the list while we're traversing it.
233+
for sp := s.specials; sp != nil; sp = sp.next {
234+
if !yield(s.base()+sp.offset, s, sp) {
235+
return
236+
}
237+
}
238+
}
239+
}
240+
}
114241
}

src/runtime/mgc.go

Lines changed: 7 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -370,13 +370,13 @@ type workType struct {
370370
tstart int64
371371
nwait uint32
372372

373-
// Number of roots of various root types. Set by gcMarkRootPrepare.
373+
// Number of roots of various root types. Set by gcPrepareMarkRoots.
374374
//
375375
// nStackRoots == len(stackRoots), but we have nStackRoots for
376376
// consistency.
377377
nDataRoots, nBSSRoots, nSpanRoots, nStackRoots int
378378

379-
// Base indexes of each root type. Set by gcMarkRootPrepare.
379+
// Base indexes of each root type. Set by gcPrepareMarkRoots.
380380
baseData, baseBSS, baseSpans, baseStacks, baseEnd uint32
381381

382382
// stackRoots is a snapshot of all of the Gs that existed
@@ -788,7 +788,7 @@ func gcStart(trigger gcTrigger) {
788788
setGCPhase(_GCmark)
789789

790790
gcBgMarkPrepare() // Must happen before assists are enabled.
791-
gcMarkRootPrepare()
791+
gcPrepareMarkRoots()
792792

793793
// Mark all active tinyalloc blocks. Since we're
794794
// allocating from these, they need to be black like
@@ -1069,26 +1069,10 @@ func gcMarkTermination(stw worldStop) {
10691069
systemstack(func() {
10701070
work.heap2 = work.bytesMarked
10711071
if debug.gccheckmark > 0 {
1072-
// Run a full non-parallel, stop-the-world
1073-
// mark using checkmark bits, to check that we
1074-
// didn't forget to mark anything during the
1075-
// concurrent mark process.
1076-
//
1077-
// Turn off gcwaiting because that will force
1078-
// gcDrain to return early if this goroutine
1079-
// happens to have its preemption flag set.
1080-
// This is fine because the world is stopped.
1081-
// Restore it after we're done just to be safe.
1082-
sched.gcwaiting.Store(false)
1083-
startCheckmarks()
1084-
gcResetMarkState()
1085-
gcMarkRootPrepare()
1086-
gcw := &getg().m.p.ptr().gcw
1087-
gcDrain(gcw, 0)
1088-
wbBufFlush1(getg().m.p.ptr())
1089-
gcw.dispose()
1090-
endCheckmarks()
1091-
sched.gcwaiting.Store(true)
1072+
runCheckmark(func(_ *gcWork) { gcPrepareMarkRoots() })
1073+
}
1074+
if debug.checkfinalizers > 0 {
1075+
checkFinalizersAndCleanups()
10921076
}
10931077

10941078
// marking is complete so we can turn the write barrier off

src/runtime/mgcmark.go

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,11 @@ const (
5353
pagesPerSpanRoot = 512
5454
)
5555

56-
// gcMarkRootPrepare queues root scanning jobs (stacks, globals, and
56+
// gcPrepareMarkRoots queues root scanning jobs (stacks, globals, and
5757
// some miscellany) and initializes scanning-related state.
5858
//
5959
// The world must be stopped.
60-
func gcMarkRootPrepare() {
60+
func gcPrepareMarkRoots() {
6161
assertWorldStopped()
6262

6363
// Compute how many data and BSS root blocks there are.
@@ -128,7 +128,7 @@ func gcMarkRootCheck() {
128128
//
129129
// We only check the first nStackRoots Gs that we should have scanned.
130130
// Since we don't care about newer Gs (see comment in
131-
// gcMarkRootPrepare), no locking is required.
131+
// gcPrepareMarkRoots), no locking is required.
132132
i := 0
133133
forEachGRace(func(gp *g) {
134134
if i >= work.nStackRoots {
@@ -392,36 +392,44 @@ func markrootSpans(gcw *gcWork, shard int) {
392392
for sp := s.specials; sp != nil; sp = sp.next {
393393
switch sp.kind {
394394
case _KindSpecialFinalizer:
395-
// don't mark finalized object, but scan it so we
396-
// retain everything it points to.
397-
spf := (*specialfinalizer)(unsafe.Pointer(sp))
398-
// A finalizer can be set for an inner byte of an object, find object beginning.
399-
p := s.base() + uintptr(spf.special.offset)/s.elemsize*s.elemsize
400-
401-
// Mark everything that can be reached from
402-
// the object (but *not* the object itself or
403-
// we'll never collect it).
404-
if !s.spanclass.noscan() {
405-
scanobject(p, gcw)
406-
}
407-
408-
// The special itself is a root.
409-
scanblock(uintptr(unsafe.Pointer(&spf.fn)), goarch.PtrSize, &oneptrmask[0], gcw, nil)
395+
gcScanFinalizer((*specialfinalizer)(unsafe.Pointer(sp)), s, gcw)
410396
case _KindSpecialWeakHandle:
411397
// The special itself is a root.
412398
spw := (*specialWeakHandle)(unsafe.Pointer(sp))
413399
scanblock(uintptr(unsafe.Pointer(&spw.handle)), goarch.PtrSize, &oneptrmask[0], gcw, nil)
414400
case _KindSpecialCleanup:
415-
spc := (*specialCleanup)(unsafe.Pointer(sp))
416-
// The special itself is a root.
417-
scanblock(uintptr(unsafe.Pointer(&spc.fn)), goarch.PtrSize, &oneptrmask[0], gcw, nil)
401+
gcScanCleanup((*specialCleanup)(unsafe.Pointer(sp)), gcw)
418402
}
419403
}
420404
unlock(&s.speciallock)
421405
}
422406
}
423407
}
424408

409+
// gcScanFinalizer scans the relevant parts of a finalizer special as a root.
410+
func gcScanFinalizer(spf *specialfinalizer, s *mspan, gcw *gcWork) {
411+
// Don't mark finalized object, but scan it so we retain everything it points to.
412+
413+
// A finalizer can be set for an inner byte of an object, find object beginning.
414+
p := s.base() + uintptr(spf.special.offset)/s.elemsize*s.elemsize
415+
416+
// Mark everything that can be reached from
417+
// the object (but *not* the object itself or
418+
// we'll never collect it).
419+
if !s.spanclass.noscan() {
420+
scanobject(p, gcw)
421+
}
422+
423+
// The special itself is also a root.
424+
scanblock(uintptr(unsafe.Pointer(&spf.fn)), goarch.PtrSize, &oneptrmask[0], gcw, nil)
425+
}
426+
427+
// gcScanCleanup scans the relevant parts of a cleanup special as a root.
428+
func gcScanCleanup(spc *specialCleanup, gcw *gcWork) {
429+
// The special itself is a root.
430+
scanblock(uintptr(unsafe.Pointer(&spc.fn)), goarch.PtrSize, &oneptrmask[0], gcw, nil)
431+
}
432+
425433
// gcAssistAlloc performs GC work to make gp's assist debt positive.
426434
// gp must be the calling user goroutine.
427435
//

src/runtime/mheap.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,7 @@ type heapArena struct {
314314
pageUseSpanInlineMarkBits [pagesPerArena / 8]uint8
315315

316316
// checkmarks stores the debug.gccheckmark state. It is only
317-
// used if debug.gccheckmark > 0.
317+
// used if debug.gccheckmark > 0 or debug.checkfinalizers > 0.
318318
checkmarks *checkmarksMap
319319

320320
// zeroedBase marks the first byte of the first page in this

src/runtime/runtime1.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,7 @@ var debug struct {
333333
traceCheckStackOwnership int32
334334
profstackdepth int32
335335
dataindependenttiming int32
336+
checkfinalizers int32
336337

337338
// debug.malloc is used as a combined debug check
338339
// in the malloc function and should be set
@@ -373,6 +374,7 @@ var dbgvars = []*dbgVar{
373374
{name: "decoratemappings", value: &debug.decoratemappings, def: 1},
374375
{name: "disablethp", value: &debug.disablethp},
375376
{name: "dontfreezetheworld", value: &debug.dontfreezetheworld},
377+
{name: "checkfinalizers", value: &debug.checkfinalizers},
376378
{name: "efence", value: &debug.efence},
377379
{name: "gccheckmark", value: &debug.gccheckmark},
378380
{name: "gcpacertrace", value: &debug.gcpacertrace},
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright 2024 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package main
6+
7+
import (
8+
"runtime"
9+
)
10+
11+
func init() {
12+
register("DetectFinalizerAndCleanupLeaks", DetectFinalizerAndCleanupLeaks)
13+
}
14+
15+
// Intended to be run only with `GODEBUG=checkfinalizers=1`.
16+
func DetectFinalizerAndCleanupLeaks() {
17+
type T *int
18+
19+
// Leak a cleanup.
20+
cLeak := new(T)
21+
runtime.AddCleanup(cLeak, func(x int) {
22+
**cLeak = x
23+
}, int(0))
24+
25+
// Have a regular cleanup to make sure it doesn't trip the detector.
26+
cNoLeak := new(T)
27+
runtime.AddCleanup(cNoLeak, func(_ int) {}, int(0))
28+
29+
// Leak a finalizer.
30+
fLeak := new(T)
31+
runtime.SetFinalizer(fLeak, func(_ *T) {
32+
**fLeak = 12
33+
})
34+
35+
// Have a regular finalizer to make sure it doesn't trip the detector.
36+
fNoLeak := new(T)
37+
runtime.SetFinalizer(fNoLeak, func(x *T) {
38+
**x = 51
39+
})
40+
41+
// runtime.GC here should crash.
42+
runtime.GC()
43+
println("OK")
44+
45+
// Keep everything alive.
46+
runtime.KeepAlive(cLeak)
47+
runtime.KeepAlive(cNoLeak)
48+
runtime.KeepAlive(fLeak)
49+
runtime.KeepAlive(fNoLeak)
50+
}

0 commit comments

Comments
 (0)