| // Copyright 2026 The Go Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file. | |
| package test | |
| import ( | |
| "internal/platform" | |
| "internal/testenv" | |
| "os" | |
| "path/filepath" | |
| "runtime" | |
| "strings" | |
| "testing" | |
| ) | |
| // Test that compiler-generated wrappers that end in a tail call still tell the | |
| // race detector that the wrapper has returned. This is a regression test for | |
| // #81478, where the missing racefuncexit call made every wrapper call leak an | |
| // entry in the race detector's shadow stack, which in turn made later | |
| // synchronization operations dramatically slower. | |
| func TestIssue81478(t *testing.T) { | |
| if !platform.RaceDetectorSupported(runtime.GOOS, runtime.GOARCH) { | |
| t.Skipf("race detector not supported on %s/%s", runtime.GOOS, runtime.GOARCH) | |
| } | |
| testenv.MustHaveGoBuild(t) | |
| dir := t.TempDir() | |
| src := filepath.Join(dir, "x.go") | |
| if err := os.WriteFile(src, []byte(issue81478src), 0644); err != nil { | |
| t.Fatalf("could not write file: %v", err) | |
| } | |
| cmd := testenv.Command(t, testenv.GoToolPath(t), "tool", "compile", "-race", "-p=main", "-S", "-o", filepath.Join(dir, "x.o"), src) | |
| out, err := cmd.CombinedOutput() | |
| if err != nil { | |
| t.Fatalf("compile failed: %v\n%s", err, out) | |
| } | |
| // W.F tail calls through an interface, S.G tail calls a static function. | |
| // Whether the tail call is actually emitted is architecture-dependent, but | |
| // either way the enter/exit instrumentation must be balanced. | |
| for _, wrapper := range []string{"main.(*W).F", "main.(*S).G"} { | |
| body, ok := funcBody(string(out), wrapper) | |
| if !ok { | |
| t.Errorf("no assembly found for %s", wrapper) | |
| continue | |
| } | |
| enter := strings.Count(body, "runtime.racefuncenter") | |
| exit := strings.Count(body, "runtime.racefuncexit") | |
| if enter == 0 { | |
| t.Errorf("%s: not instrumented for the race detector\n%s", wrapper, body) | |
| } else if enter != exit { | |
| t.Errorf("%s: unbalanced race instrumentation: %d racefuncenter, %d racefuncexit\n%s", wrapper, enter, exit, body) | |
| } | |
| } | |
| } | |
| // funcBody returns the part of the -S output that belongs to the symbol fn: | |
| // its header line plus the indented instruction and relocation lines below it. | |
| func funcBody(out, fn string) (string, bool) { | |
| lines := strings.Split(out, "\n") | |
| for i, line := range lines { | |
| if !strings.HasPrefix(line, fn+" STEXT") { | |
| continue | |
| } | |
| end := i + 1 | |
| for end < len(lines) && strings.HasPrefix(lines[end], "\t") { | |
| end++ | |
| } | |
| return strings.Join(lines[i:end], "\n"), true | |
| } | |
| return "", false | |
| } | |
| var issue81478src = ` | |
| package main | |
| type I interface{ F() } | |
| type T struct{ n int } | |
| func (t *T) F() { t.n++ } | |
| //go:noinline | |
| func (t *T) G() { t.n++ } | |
| // W.F is a wrapper around an embedded interface method. | |
| type W struct{ I } | |
| // S.G is a wrapper around an embedded pointer's method. | |
| type S struct{ *T } | |
| var _ I = &W{} | |
| var _ = (&S{}).G | |
| func main() {} | |
| ` |