Go version

1.24.4 and tip

Output of go env in your module/workspace:

playground

What did you do?

Add a local variable to slices.TestIssue68488 from #68488.

Modified version on playground: https://go.dev/play/p/HJC9xu8dz-E

func TestIssue68488ExtraVariable(t *testing.T) {
    // Test starts failing if we add these two lines declaring a local variable.
    x := 42
    _ = x

    s := make([]int, 3)
    clone := slices.Clone(s[1:1])
    switch unsafe.SliceData(clone) {
    case &s[0], &s[1], &s[2]:
        t.Error("clone keeps alive s due to array overlap")
    }
}

What did you see happen?

The test starts failing.

prog_test.go:28: clone keeps alive s due to array overlap

What did you expect to see?

It's not clear if the test is testing what it expects to test, but it appears sensitive to minor perturbations.

In my case, I first saw this while modifying the compiler on tip, but it seems to be reproducible manually, including in Go 1.24.

The s slice appears to be stack allocated. Maybe the test does not expect that? Forcing the slices to escape seems to jiggle the test to pass again, at least as observed via a couple of runs (see playground link above).

clone I think is a zero capacity slice passed to unsafe.SliceData. I will note that the unsafe.SliceData doc says:

SliceData returns a pointer to the underlying array of the argument slice.

- If cap(slice) > 0, SliceData returns &slice[:1][0].
- If slice == nil, SliceData returns nil.
- Otherwise, SliceData returns a non-nil pointer to an unspecified memory address.

Comment From: gabyhelp

Related Code Changes

(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.)

Comment From: dmitshur

CC @Jorropo, @randall77.

Comment From: Jorropo

The original bug is that the returned *[0]T would alias the passed in array leading to the GC not freeing the underlying *[X]T.

The fix were to allocate a new zero sized array. But for some reason the compiler decides to to allocate this *[0]T on the stack ¿

Image

Then after stack shenanigans the compiler randomly merges *[0]T to *[3]T together while they are both live.

So the test is "correctly" failing here.

However this not meaningful because the docs of unsafe.SliceData are pretty clear that this test is relying on fragile undocumented unsafe behavior:

Otherwise (the slice is non-nil and zero length), SliceData returns a non-nil pointer to an unspecified memory address.

according to the docs there does not exists a meaningful == use of whatever it returns here since it could literally be anything.

And in pure go there is no way to compare *[0]T with *T.


To fix this bug we could change this test to leak s, sure the compiler is recreating the spurious aliasing bug I've fixed in CL 598875 sometimes if the array is stack allocated; but it is clear that the original usecase is about GC and we don't really care if that bug happens on stack allocations.

I guess the test could be fixed to not rely on undocumented unsafe behavior using finalizers to test if s is kept alive, but I have no idea if they are reliable for something like this.

We could also "fix" the bug in the compiler and make sure *[0]T allocations are always &runtime.zerobase rather than be stack allocated. Beyond the promises of unsafe.SliceData it is intriguing that runtime.zerobase symbol is 1 byte big, while the stack allocations of zero sized elements is zero bytes big.

Comment From: gopherbot

Change https://go.dev/cl/684755 mentions this issue: slices: update TestIssue68488 to avoid false positives