You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
go/types: data race between (*Named).unpack and (*Checker).isComplete under concurrent go/packages export-data loading on go1.27.0 #81122
Ran golang.org/x/tools/go/packages.Load (x/tools v0.48.0 and v0.49.0) over a large module (~200 packages, Tests: true) under go test -race, using the deprecated-but-supported mode constant packages.LoadSyntax, in which dependency packages are loaded from export data rather than from source:
The loader type-checks root packages concurrently (one goroutine per package in (*loader).loadPackage). Two concurrently-running go/types checkers touch the same lazily-unpacked *types.Named originating from shared export data.
Write: go/types/named.go:244 — (*Named).unpack (also via named.go:602) writing the same field
Both goroutines are x/tools go/packages.(*loader).loadPackage workers. Reproduces deterministically within ~20s on the large module. GOMAXPROCS=1 masks it.
What did you expect to see?
No data race: concurrent type-checking of distinct packages whose imports resolve to shared export-data-backed *types.Named values is the documented x/tools go/packages execution model, and go1.26.x is race-clean on the identical module and load configuration.
Notes:
A/B isolation on the same tree and toolchain: with the modern explicit mode (NeedName|NeedFiles|NeedCompiledGoFiles|NeedImports|NeedDeps|NeedTypes|NeedSyntax|NeedTypesInfo), where dependencies are type-checked from source, the identical load is race-clean. Only the export-data path (LoadSyntax-style modes) races, pointing at the go1.27 lazy unpack path for export-data-backed named types.
A small two-package module does NOT reproduce; a large dependency graph (hundreds of packages, Tests: true) reproduces reliably, consistent with a narrow first-unpack window.
Possibly related to the class in runtime: race detector crash on ppc64le #76051 ((*Named).unpack machinery) and the go/packages test disablement in commit 539cdc4 ("A change in go/types has uncovered a race in go/packages").
Update: reproduces with pure source-mode loading too (no export data) — via the instantiation branch of unpack.
The original report claimed export-data dependency loading triggers the race while full source loading appeared race-clean. At whole-tree scale that second claim turns out to be wrong. On the same repository, with the loader switched to full source mode:
the same (*Named).unpack / (*Checker).isComplete race fired during a -race run of the full test suite (~10 min of load-heavy testing to reproduce, vs ~17s in export-data mode). Nothing is shared across packages.Load calls in this setup, so the two racing checkers are workers of a single Load (x/tools/go/packages type-checks up to GOMAXPROCS packages concurrently via its cpuLimit semaphore).
In this configuration the write side is the instantiated type branch, not the lazy-loader branch:
write: go/types/named.go:244 — n.fromRHS = n.expandRHS() inside the n.inst != nil branch of unpack, holding n.mu
read: go/types/cycles.go:122 — rhs = t.fromRHS in isComplete (case *Named:), with no lock or atomic
Since instantiation expansion stays lazy after the declaring package finishes type-checking, two packages that both depend on a package containing a generic instantiation can race on its first Underlying() / isComplete touch.
So export-data loading is just the fastest trigger; the unsynchronized fromRHS read in isComplete appears reachable from any concurrent multi-package checking that shares a dependency with unexpanded instantiations.
full race trace (go1.27.0, x/tools v0.48.0, linux/amd64)
I hit the same report ((*Checker).isComplete at cycles.go:122 against (*Named).unpack at named.go:244, both under go/packages workers) in a go test -race gate with go1.27.1 and x/tools v0.50.0, on a test that loads a handful of packages with Tests: true and without NeedDeps. Some data that may help narrow it:
1. It reproduces without x/tools, reliably. The program below imports strings once with go/importer, then type-checks eight tiny packages concurrently against that shared *types.Package, which is the model go/packages uses for export-data dependencies. Each package ranges over strings.SplitSeq, whose result type iter.Seq[string] is an instance the importer leaves unexpanded.
go run -race ., linux/amd64, GOMAXPROCS=5, each run a fresh process:
Toolchain
Runs with a race report
Read side
go1.27.1
30 of 30
(*Checker).isComplete, cycles.go:122, from callExpr
go1.26.8
10 of 30
(*Checker).finiteSize, cycles.go:127, from pendingType
go1.25.14
0 of 30
none
The write side is in every case the instance branch of unpack shown in the trace above (named.go:250 in 1.26.8), reached from rangeKeyVal through commonUnder.
2. go1.26 is affected too, through a different read. The unsynchronized read of another package's Named.fromRHS arrived with the value-observance check of CL 726580 (1.26, finiteSize via pendingType) and was carried into isComplete by CL 734980 (1.27). go1.25 has neither read and reports nothing.
3. The read is the odd one out.hasVarSize in builtins.go reads the same field and says "Careful, we're inspecting t.fromRHS, so we need to unpack first." isComplete does not unpack. Named.cleanup deliberately leaves instances unexpanded ("they will lazily expand it as needed"), and the comment at the top of named.go says that lazy construction may happen concurrently after type checking or importing has finished, so an instance reachable from an imported package's API is exactly the case the mutex and atomic state exist for. fromRHS is an interface, two words, so beyond the report a torn read is at least possible in principle.
4. A fix is up as CL 841585 (#81871). isComplete returns true for a *Named that no Checker owns (t.check == nil) without reading its fromRHS, the ownership test CL 837605 uses in unalias. Such a type was created by an importer, through the API, or by a type-checking pass that has finished, so the walk from it cannot lead back to an object on this Checker's path and always ended in true. The CL message covers the alternatives: unpacking such a type first also removes the race but expands it only to reach that answer, and unpacking unconditionally breaks TestFixedbugs/issue57522.go ("nil typ" in subst).
It also covers the source-mode case in the update above. go/packages starts a package's Checker only after all its imports are loaded, which in source mode means their Checkers have returned, and a Checker's cleanup clears check on every instance it created without expanding the ones still unexpanded; an instance created later while expanding one of those gets a nil check as well. So every instance a dependent reaches in a dependency is unowned. An instance written as a type in the source (func F() Seq[string]) is expanded before Check returns and does not race; one created by substitution (var F = Of[string] with func Of[T any]() Seq[T], or one in the signature of a method that a dependent expands) stays unexpanded and does.
The CL's test type-checks four packages concurrently against a shared package built each way, by the importer and from source, as two subtests. Run on its own under -race on a toolchain built from master, each subtest raced in at least 99 of 100 runs without the change and in none of 200 with it, in both go/types and types2, and the reproducer above went from 20 of 20 to none. As far as I can tell this is the only unguarded read of a *Named's fromRHS: the other direct reads outside named.go (the *Alias case beside it, object.go, signature.go) are of an *Alias, whose fromRHS is set when it is created or declared and never lazily.
If this is fixed, a backport to 1.27 would help (1.26 has the same read in finiteSize, until it caches a result on the type): go/packages type-checks packages in parallel by design, so any -race test that loads several packages with it can hit this. The only workaround I know of is GOMAXPROCS=1 for the whole process, set before go/packages is initialized, since it sizes its type-checking pool from it then.
Activity
gabyhelp commentedon Aug 26, 2026
Related Issues
(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.)
buroa commentedon Aug 26, 2026
Update: reproduces with pure source-mode loading too (no export data) — via the instantiation branch of
unpack.The original report claimed export-data dependency loading triggers the race while full source loading appeared race-clean. At whole-tree scale that second claim turns out to be wrong. On the same repository, with the loader switched to full source mode:
the same
(*Named).unpack/(*Checker).isCompleterace fired during a-racerun of the full test suite (~10 min of load-heavy testing to reproduce, vs ~17s in export-data mode). Nothing is shared acrosspackages.Loadcalls in this setup, so the two racing checkers are workers of a single Load (x/tools/go/packagestype-checks up toGOMAXPROCSpackages concurrently via itscpuLimitsemaphore).In this configuration the write side is the instantiated type branch, not the lazy-loader branch:
go/types/named.go:244—n.fromRHS = n.expandRHS()inside then.inst != nilbranch ofunpack, holdingn.mugo/types/cycles.go:122—rhs = t.fromRHSinisComplete(case *Named:), with no lock or atomicSince instantiation expansion stays lazy after the declaring package finishes type-checking, two packages that both depend on a package containing a generic instantiation can race on its first
Underlying()/isCompletetouch.So export-data loading is just the fastest trigger; the unsynchronized
fromRHSread inisCompleteappears reachable from any concurrent multi-package checking that shares a dependency with unexpanded instantiations.full race trace (go1.27.0, x/tools v0.48.0, linux/amd64)
adonovan commentedon Aug 26, 2026
@mrkfrmn
ci(race): cite the matching upstream issue for the internal/lint excl…
build: keep go/types' type-check race out of the gate
gopherbot commentedon Sep 29, 2026
Change https://go.dev/cl/841585 mentions this issue:
go/types, types2: fix data race in isComplete on imported typesSerialize go/packages type-checking in race builds, working around go…
go/types, types2: fix data race in isComplete on imported types
jmrplens commentedon Sep 30, 2026
I hit the same report (
(*Checker).isCompleteat cycles.go:122 against(*Named).unpackat named.go:244, both undergo/packagesworkers) in ago test -racegate with go1.27.1 and x/tools v0.50.0, on a test that loads a handful of packages withTests: trueand withoutNeedDeps. Some data that may help narrow it:1. It reproduces without x/tools, reliably. The program below imports
stringsonce withgo/importer, then type-checks eight tiny packages concurrently against that shared*types.Package, which is the modelgo/packagesuses for export-data dependencies. Each package ranges overstrings.SplitSeq, whose result typeiter.Seq[string]is an instance the importer leaves unexpanded.main.go
go run -race ., linux/amd64, GOMAXPROCS=5, each run a fresh process:(*Checker).isComplete, cycles.go:122, fromcallExpr(*Checker).finiteSize, cycles.go:127, frompendingTypeThe write side is in every case the instance branch of
unpackshown in the trace above (named.go:250 in 1.26.8), reached fromrangeKeyValthroughcommonUnder.2. go1.26 is affected too, through a different read. The unsynchronized read of another package's
Named.fromRHSarrived with the value-observance check of CL 726580 (1.26,finiteSizeviapendingType) and was carried intoisCompleteby CL 734980 (1.27). go1.25 has neither read and reports nothing.3. The read is the odd one out.
hasVarSizein builtins.go reads the same field and says "Careful, we're inspecting t.fromRHS, so we need to unpack first."isCompletedoes not unpack.Named.cleanupdeliberately leaves instances unexpanded ("they will lazily expand it as needed"), and the comment at the top of named.go says that lazy construction may happen concurrently after type checking or importing has finished, so an instance reachable from an imported package's API is exactly the case the mutex and atomic state exist for.fromRHSis an interface, two words, so beyond the report a torn read is at least possible in principle.4. A fix is up as CL 841585 (#81871).
isCompletereturnstruefor a*Namedthat no Checker owns (t.check == nil) without reading itsfromRHS, the ownership test CL 837605 uses inunalias. Such a type was created by an importer, through the API, or by a type-checking pass that has finished, so the walk from it cannot lead back to an object on this Checker's path and always ended intrue. The CL message covers the alternatives: unpacking such a type first also removes the race but expands it only to reach that answer, and unpacking unconditionally breaksTestFixedbugs/issue57522.go("nil typ" insubst).It also covers the source-mode case in the update above.
go/packagesstarts a package's Checker only after all its imports are loaded, which in source mode means their Checkers have returned, and a Checker's cleanup clearscheckon every instance it created without expanding the ones still unexpanded; an instance created later while expanding one of those gets a nilcheckas well. So every instance a dependent reaches in a dependency is unowned. An instance written as a type in the source (func F() Seq[string]) is expanded beforeCheckreturns and does not race; one created by substitution (var F = Of[string]withfunc Of[T any]() Seq[T], or one in the signature of a method that a dependent expands) stays unexpanded and does.The CL's test type-checks four packages concurrently against a shared package built each way, by the importer and from source, as two subtests. Run on its own under
-raceon a toolchain built frommaster, each subtest raced in at least 99 of 100 runs without the change and in none of 200 with it, in bothgo/typesandtypes2, and the reproducer above went from 20 of 20 to none. As far as I can tell this is the only unguarded read of a*Named'sfromRHS: the other direct reads outside named.go (the*Aliascase beside it,object.go,signature.go) are of an*Alias, whosefromRHSis set when it is created or declared and never lazily.If this is fixed, a backport to 1.27 would help (1.26 has the same read in
finiteSize, until it caches a result on the type):go/packagestype-checks packages in parallel by design, so any-racetest that loads several packages with it can hit this. The only workaround I know of isGOMAXPROCS=1for the whole process, set beforego/packagesis initialized, since it sizes its type-checking pool from it then.zzffeggs commentedon Sep 30, 2026
Record the upstream state of 2026-09-30 in the register (#1083)
Stop shipping golang.org/x/crypto, gate the release binaries at modul…
Check the release binaries again when releasing, hold the image, bund…
Share one handler resolver and one client-go route reader, and move t…
Derive every action's requests and join them to the fine-grained perm…
Hold the fine-grained table to the live record in the 1:1 audit (R-GR…
test(decls): load each package alone to avoid a go/types data race