Files
tailscale/util/zstdframe/synctest_test.go
Brad Fitzpatrick 3bf0149b29 util/zstdframe, util/testenv: don't pool coders within synctest bubbles
The zstd Encoder and Decoder types use channels internally, created
when the coder is constructed. A coder constructed by a goroutine
inside a testing/synctest bubble therefore owns bubble-associated
channels, and if it lands in zstdframe's process-wide pools and is
later reused outside that bubble, the Go runtime kills the process:

    fatal error: receive on synctest channel from outside bubble

This has been crashing test binaries that mix synctest-based tests
with regular tests exercising zstd compression in parallel, taking
out every other test in the package with it.

Add testenv.InSynctestBubble and use it in zstdframe to construct a
fresh coder per call within a bubble instead of using the pools.
Pooling behavior outside of bubbles (including in benchmarks) is
unchanged.

As of Go 1.26 there is no public API to query bubble membership, so
InSynctestBubble looks for the "synctest bubble N" annotation that
the runtime renders in the current goroutine's runtime.Stack header.
That annotation is not covered by the Go compatibility promise, so
tests fail loudly (in util/testenv directly, and in util/zstdframe by
reintroducing the pooled-coder crash) if a future Go release changes
it. The check costs ~2us and runs only in test binaries, detected by
an uncached flag.Lookup("test.v") rather than testenv.InTest: this
path is reachable from package init functions (before testing has
registered its flags), where InTest would permanently latch a false
result into its cache, breaking later InTest and AssertInTest calls.

Fixes tailscale/corp#45861

Change-Id: I7d89e2d0de51e30098ceda25c12d27918acc46e8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-07-30 12:28:22 -07:00

63 lines
1.9 KiB
Go

// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package zstdframe
import (
"bytes"
"testing"
"testing/synctest"
"time"
)
// TestSynctestBubbleIsolation verifies that coders used within a
// testing/synctest bubble do not crash the process when zstdframe is
// subsequently used outside the bubble (or in another bubble).
//
// The zstd Encoder and Decoder types use channels internally, so a pooled
// coder that was created within a synctest bubble must never be reused
// outside of it: the runtime kills the process with "fatal error: receive
// on synctest channel from outside bubble".
func TestSynctestBubbleIsolation(t *testing.T) {
src := []byte("hello, hello, hello, world, world, world")
// Use the coder pools within a bubble several times to make it very
// likely that a bubble-created coder would land in the package-level
// pools if pooling were (incorrectly) enabled here.
var frame []byte
synctest.Test(t, func(t *testing.T) {
for range 10 {
frame = AppendEncode(nil, src)
out, err := AppendDecode(nil, frame)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(out, src) {
t.Fatalf("roundtrip inside bubble = %q, want %q", out, src)
}
}
// Bubble detection must not depend on the fake clock still being
// near its 2000-01-01 start, so advance it past the real
// process start time and use the coders again.
time.Sleep(100 * 365 * 24 * time.Hour)
frame = AppendEncode(nil, src)
if _, err := AppendDecode(nil, frame); err != nil {
t.Fatal(err)
}
})
// Prior to pooling being disabled in tests, this crashed the process
// by reusing a pooled coder whose channels were created in the
// now-exited bubble above.
for range 10 {
got := AppendEncode(nil, src)
out, err := AppendDecode(nil, got)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(out, src) {
t.Fatalf("roundtrip outside bubble = %q, want %q", out, src)
}
}
}