Skip to content

runtime: reportZombies prints "unmarked" for every slot and dumps nothing under Green Tea GC #80799

Description

@RaduBerinde

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    FixPendingIssues that have a fix which has not yet been reviewed or submitted.compiler/runtimeIssues related to the Go compiler and/or runtime.

    Type

    No type

    Projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions