Skip to content

go/types: data race in Unalias during concurrent types.Identical calls on SSA-instantiated aliases #79035

@chabbimilind

Description

@chabbimilind

What version of Go are you using (go version)?

go version output
$ go version
go version devel go1.24-8191cd8868 Mon Apr 28 16:42:17 2026 +0000 darwin/arm64

Does this issue reproduce with the latest release?

Yes.

What operating system and processor architecture are you using (go env)?

go env output
GOOS="darwin"
GOARCH="arm64"

What did you do?

I encountered a data race when multiple goroutines concurrently call types.Identical on *types.Alias instances produced by golang.org/x/tools/go/ssa with InstantiateGenerics.

The issue occurs because SSA's substituter calls types.Instantiate (without a checker and without cleanup) when specializing generic types. The resulting *types.Alias keeps .actual = nil. When concurrent goroutines call types.Identical on these aliases, it triggers Unalias, which races on the path-compression write at $GOROOT/src/go/types/alias.go:121 versus the read at alias.go:112.

Minimal reproducer:

// Minimal reproducer: data race in go/types when concurrent goroutines
// call types.Identical on alias instantiations produced by
// golang.org/x/tools/go/ssa.
//
// This file is fully self-contained: it materializes the two GOPATH
// packages (p1, p2) from embedded source strings into a tempdir at
// startup, then loads them with golang.org/x/tools/go/packages, builds
// SSA with InstantiateGenerics, and races types.Identical on every
// alias the program touches. SSA's substituter calls types.Instantiate
// (no checker, no cleanup) when it specializes p1.New[T1]; the
// resulting *types.Alias for p1.A1[p2.T1] keeps .actual = nil. The
// concurrent Identical calls race on the path-compression write at
// $GOROOT/src/go/types/alias.go:121 vs the read at alias.go:112.
//
// Run:
//   GODEBUG=gotypesalias=1 go run -race ./main.go
package main

import (
	"go/types"
	"os"
	"path/filepath"
	"runtime"
	"sync"

	"golang.org/x/tools/go/packages"
	"golang.org/x/tools/go/ssa"
	"golang.org/x/tools/go/ssa/ssautil"
)

const p1Src = `package p1

type A1[T any] = func(T)
type S1[T any] struct{ F A1[T] }
func New[T any]() *S1[T] { return &S1[T]{} }
`

const p2Src = `package p2

import "p1"

type T1 struct{}
var V1 = p1.New[T1]()
`

func main() {
	gopath, err := os.MkdirTemp("", "alias-race-")
	if err != nil {
		panic(err)
	}
	defer os.RemoveAll(gopath)
	for _, f := range []struct{ rel, src string }{
		{"src/p1/p1.go", p1Src},
		{"src/p2/p2.go", p2Src},
	} {
		full := filepath.Join(gopath, f.rel)
		if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
			panic(err)
		}
		if err := os.WriteFile(full, []byte(f.src), 0o644); err != nil {
			panic(err)
		}
	}

	cfg := &packages.Config{
		Mode: packages.NeedName | packages.NeedFiles |
			packages.NeedImports | packages.NeedDeps |
			packages.NeedTypes | packages.NeedSyntax | packages.NeedTypesInfo,
		Dir: gopath,
		Env: append(os.Environ(), "GO111MODULE=off", "GOPATH="+gopath),
	}
	pkgs, _ := packages.Load(cfg, "p2")
	prog, _ := ssautil.AllPackages(pkgs, ssa.InstantiateGenerics)
	prog.Build()

	// Walk every SSA function signature and collect every *types.Alias
	// reachable from it.
	seen := map[types.Type]bool{}
	var aliases []*types.Alias
	var walk func(t types.Type)
	walk = func(t types.Type) {
		if t == nil || seen[t] {
			return
		}
		seen[t] = true
		switch x := t.(type) {
		case *types.Pointer:
			walk(x.Elem())
		case *types.Struct:
			for i := 0; i < x.NumFields(); i++ {
				walk(x.Field(i).Type())
			}
		case *types.Tuple:
			for i := 0; i < x.Len(); i++ {
				walk(x.At(i).Type())
			}
		case *types.Signature:
			walk(x.Params())
			walk(x.Results())
		case *types.Named:
			walk(x.Underlying())
		case *types.Alias:
			aliases = append(aliases, x)
			walk(x.Rhs())
		}
	}
	for fn := range ssautil.AllFunctions(prog) {
		walk(fn.Type())
	}

	// Workaround: single-threaded warm-up. Calling types.Unalias on each
	// *Alias from one goroutine populates .actual; after this pass every
	// subsequent Unalias call hits the a.actual!=nil early-return at
	// alias.go:113 and skips the racy write at alias.go:121.
	if os.Getenv("WARMUP") == "1" {
		for _, a := range aliases {
			_ = types.Unalias(a)
		}
	}

	// Race: many goroutines call types.Identical on every collected
	// alias. types.Identical calls Unalias on its operands; for aliases
	// whose .actual is still nil (those produced by SSA's substituter
	// via types.Instantiate without cleanup), the first call performs a
	// path-compression write at alias.go:121 while concurrent calls
	// read at alias.go:112 — a data race.
	var wg sync.WaitGroup
	for i := 0; i < runtime.GOMAXPROCS(0); i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for _, a := range aliases {
				_ = types.Identical(a, a)
			}
		}()
	}
	wg.Wait()
}

How to reproduce:

GODEBUG=gotypesalias=1 go run -race main.go

What did you expect to see?

No data race detected.

What did you see instead?

==================
WARNING: DATA RACE
Read at 0x... by goroutine ...:
  go/types.Unalias()
      $GOROOT/src/go/types/alias.go:112
  go/types.Identical()
      $GOROOT/src/go/types/predicates.go:...

Previous write at 0x... by goroutine ...:
  go/types.Unalias()
      $GOROOT/src/go/types/alias.go:121
  go/types.Identical()
      $GOROOT/src/go/types/predicates.go:...
==================

The race occurs because types.Instantiate (when called without cleanup by SSA) creates aliases with .actual = nil, and the path-compression logic in Unalias is not thread-safe for this case.

Workaround

Setting WARMUP=1 before running the reproducer populates .actual in a single-threaded pass, avoiding the race:

GODEBUG=gotypesalias=1 WARMUP=1 go run -race main.go

This demonstrates that pre-warming the .actual field eliminates the race.

Potential fixes

  1. Add synchronization (mutex) to the path-compression write in Unalias
  2. Ensure SSA (or other callers of types.Instantiate) always call cleanup to populate .actual
  3. Make the path-compression logic thread-safe via atomic operations

cc @golang/compiler @golang/types

Metadata

Metadata

Assignees

Labels

BugReportIssues describing a possible bug in the Go implementation.

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions