Go version
go version go1.26.5 darwin/arm64
Also reproduced on go1.27rc2 darwin/arm64. The relevant code is unchanged at master.
Output of go env in your module/workspace:
GOARCH='arm64'
GOOS='darwin'
GOHOSTARCH='arm64'
GOHOSTOS='darwin'
GOVERSION='go1.26.5'
GOEXPERIMENT=''
CGO_ENABLED='1'
GOFLAGS=''
GODEBUG=''
GOTOOLCHAIN='auto'
(GOEXPERIMENT is empty, i.e. defaults — which on 1.26 means greenteagc is on.)
What did you do?
Ran the runtime's own zombie-object reproducer:
cd $GOROOT/src/runtime/testdata/testprog
go build -o /tmp/testprog .
GODEBUG=invalidptr=0 /tmp/testprog GCZombie
What did you see happen?
runtime: marked free object in span 0x127480968, elemsize=192 freeindex=0 (bad use of unsafe.Pointer or having race conditions? try -d=checkptr or -race)
0x3a1d01360000 alloc unmarked
0x3a1d013600c0 free unmarked
0x3a1d01360180 alloc unmarked
0x3a1d01360240 free unmarked
0x3a1d01360300 alloc unmarked
0x3a1d013603c0 free unmarked
[... 42 slots in total: 21 "alloc unmarked", 21 "free unmarked", nothing else ...]
fatal error: found pointer to free object
Every slot is reported unmarked, no slot is tagged zombie, and the hexdumpWords call is therefore never reached — even though the sweeper threw precisely because it found a marked-but-free object in this span. The report contradicts the error it accompanies, and the object contents that make the failure diagnosable are not printed at all.
Same program, same toolchain, Green Tea disabled:
GOEXPERIMENT=nogreenteagc go build -o /tmp/testprog .
GODEBUG=invalidptr=0 /tmp/testprog GCZombie
runtime: marked free object in span 0x10263a5e0, elemsize=192 freeindex=0 (bad use of unsafe.Pointer or having race conditions? try -d=checkptr or -race)
0x23a2e5f90000 alloc marked
0x23a2e5f900c0 free marked zombie
7 6 5 4 3 2 1 0 f e d c b a 9 8 0123456789abcdef
000023a2e5f900c0: 00000000 00000000 00000000 00000000 ................
000023a2e5f900d0: 00000000 00000000 00000000 00000000 ................
[...]
21 slots tagged zombie, each with its hex dump.
What did you expect to see?
The second output.
Analysis
(*sweepLocked).sweep merges the inline mark bits into gcmarkBits, clearing them, and then runs the zombie check against gcmarkBits:
// Copy over and clear the inline mark bits if necessary.
if gcUsesSpanInlineMarkBits(s.elemsize) {
s.moveInlineMarks(s.gcmarkBits)
}
// Check for zombie objects.
if s.freeindex < s.nelems {
...
if (*s.gcmarkBits.bytep(obj / 8)&^*s.allocBits.bytep(obj / 8))>>(obj%8) != 0 {
s.reportZombies()
}
moveInlineMarks ends with imb.init(s.spanclass, true), which resets the inline bits.
reportZombies then reads them back:
mbits := s.markBitsForBase()
and under goexperiment.greenteagc that returns the inline bits whenever gcUsesSpanInlineMarkBits(s.elemsize):
func (s *mspan) markBitsForBase() markBits {
if gcUsesSpanInlineMarkBits(s.elemsize) {
return markBits{&s.inlineMarkBits().marks[0], uint8(1), 0}
}
return markBits{&s.gcmarkBits.x, uint8(1), 0}
}
So mbits reads back all zero, zombie := mbits.isMarked() && !alloc is never true, and nothing is dumped.
Scope. gcUsesSpanInlineMarkBits(size) is heapBitsInSpan(size) && size >= 16, so this affects spans whose elemsize is in [16, 512] on 64-bit — size classes 2..26 of 67, which covers most Go allocations. Larger objects and the 8-byte class still report correctly; running the same reproducer with const size = 600 (elemsize 640) on stock go1.26.5 prints the zombie tags and hex dumps as expected.
Affected releases. Go 1.25 with GOEXPERIMENT=greenteagc, Go 1.26 and later where it is on by default, and master.
TestGcZombieReporting does not catch this because it only asserts that the output contains found pointer to free object, which is printed by the throw rather than by the loop.
Suggested fix
reportZombies should consult the same bits the zombie check that called it consulted:
- mbits := s.markBitsForBase()
+ // Not markBitsForBase: under Green Tea that returns the span's inline
+ // mark bits, which sweep has already drained into s.gcmarkBits and
+ // cleared by the time we get here.
+ mbits := markBits{&s.gcmarkBits.x, 1, 0}
The only other markBitsForBase in mgcsweep.go (in the trace / clobberfree / race / msan / asan loop) runs before moveInlineMarks and is correct as it stands.
Go version
go version go1.26.5 darwin/arm64Also reproduced on
go1.27rc2 darwin/arm64. The relevant code is unchanged at master.Output of
go envin your module/workspace:(
GOEXPERIMENTis empty, i.e. defaults — which on 1.26 meansgreenteagcis on.)What did you do?
Ran the runtime's own zombie-object reproducer:
What did you see happen?
Every slot is reported
unmarked, no slot is taggedzombie, and thehexdumpWordscall is therefore never reached — even though the sweeper threw precisely because it found a marked-but-free object in this span. The report contradicts the error it accompanies, and the object contents that make the failure diagnosable are not printed at all.Same program, same toolchain, Green Tea disabled:
21 slots tagged
zombie, each with its hex dump.What did you expect to see?
The second output.
Analysis
(*sweepLocked).sweepmerges the inline mark bits intogcmarkBits, clearing them, and then runs the zombie check againstgcmarkBits:moveInlineMarksends withimb.init(s.spanclass, true), which resets the inline bits.reportZombiesthen reads them back:and under
goexperiment.greenteagcthat returns the inline bits whenevergcUsesSpanInlineMarkBits(s.elemsize):So
mbitsreads back all zero,zombie := mbits.isMarked() && !allocis never true, and nothing is dumped.Scope.
gcUsesSpanInlineMarkBits(size)isheapBitsInSpan(size) && size >= 16, so this affects spans whoseelemsizeis in [16, 512] on 64-bit — size classes 2..26 of 67, which covers most Go allocations. Larger objects and the 8-byte class still report correctly; running the same reproducer withconst size = 600(elemsize 640) on stock go1.26.5 prints thezombietags and hex dumps as expected.Affected releases. Go 1.25 with
GOEXPERIMENT=greenteagc, Go 1.26 and later where it is on by default, and master.TestGcZombieReportingdoes not catch this because it only asserts that the output containsfound pointer to free object, which is printed by thethrowrather than by the loop.Suggested fix
reportZombiesshould consult the same bits the zombie check that called it consulted:The only other
markBitsForBaseinmgcsweep.go(in the trace / clobberfree / race / msan / asan loop) runs beforemoveInlineMarksand is correct as it stands.