Skip to content

runtime: keep workers alive after main Goexit - #2259

Merged
xushiwei merged 2 commits into
xgo-dev:mainfrom
cpunion:codex/goroot-goexit-20260801
Aug 3, 2026
Merged

runtime: keep workers alive after main Goexit#2259
xushiwei merged 2 commits into
xgo-dev:mainfrom
cpunion:codex/goroot-goexit-20260801

Conversation

@cpunion

@cpunion cpunion commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

Fixes the scheduler-liveness root cause behind GOROOT fixedbugs/issue5963.go.

Summary

  • track registered goroutine contexts in the native pthread scheduler
  • pack the live-context count and main-exited bit into one atomic state, so the last release makes one consistent deadlock decision
  • give lifecycle-owned main/foreign contexts and runtime-owned workers exactly one release/root-free owner
  • account for LLGo's LLVM-style atomic.Sub returning the pre-subtraction value
  • allow main runtime.Goexit to finish while another goroutine remains alive, while preserving the deadlock when main has no peer or the final worker returns
  • remove only the Go 1.24/1.25/1.26 native issue5963.go xfails

Bare metal keeps its existing single-context behavior through documented no-op/sentinel helpers. This PR does not modify CI configuration or timeouts.

Tests

  • review counterexample: a real LLGo subprocess waits until main's lifecycle context has completed mexit, then lets the peer return normally; it verifies the worker marker precedes the last-goroutine deadlock
  • go test -vet=off ./test/go -run '^TestMainGoexitLifecycleReleasedOnce$' -count=1
  • go run ./cmd/llgo test -run 'Goexit|GMP' ./test/std/runtime ./test/llgoext
  • go test ./ssa -run '^TestFromTestgo$' -count=1
  • upstream fixedbugs/issue5963.go passes without xfail on Go 1.24.11, 1.25.0, and 1.26.5
  • Go 1.26.5 fixedbugs/issue8158.go and fixedbugs/issue11256.go
  • runtime internal modules build successfully

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: keep workers alive after main Goexit

The reference-counting approach is sound in intent, and the NewProc error path and ordinary runtime-owned worker path are correctly balanced (exactly one retainG in initRuntimeContext, one releaseG in mexit). However, there is a deterministic double-free + gcount double-decrement on the exact scenario this PR targets — the main goroutine calling runtime.Goexit with a live peer — plus a cross-word race in the deadlock detection. See inline comments for the three concrete findings.

Why the double-free is deterministic (not a race): the main goroutine's g is created lazily via getg()setAutoG, which sets currentGHasLifecycle = true (g_tls.go:91-98) and installs the pthread destructor. It is never re-installed via setg before Goexit. So when main reaches mexit, setg(nil) (proc.go:149) takes the currentGHasLifecycle branch and calls destroyG(old) on the same context — which frees the root and calls releaseG() a second time. The included test cl/_testgo/goexitmain/in.go only passes because the peer races to os.Exit(0) before the corruption surfaces.

Additional (non-inline) note — deadlock check duplicated across three sites: the same fatal("no goroutines (main called runtime.Goexit) - deadlock!"); c.Exit(2) is emitted from z_default.go:39-42, proc.go:135-139, and g_tls.go:105-109, using two different predicates (gp.isMain && liveGCount() == 1 vs releaseG() == 0 && hasMainExited()). Consider extracting a single helper so the invariants cannot drift, and note that if fatal(...) already terminates, the trailing c.Exit(2) is dead code.

Test suggestion: after fixing ownership, add a deterministic variant of goexitmain where the peer signals completion and lets the process exit normally (rather than calling os.Exit), so the double-release/double-free path is actually exercised instead of raced past.

Comment thread runtime/internal/runtime/proc.go Outdated
Comment thread runtime/internal/runtime/z_default.go Outdated
Comment thread runtime/internal/runtime/proc_baremetal.go Outdated
@codecov

codecov Bot commented Aug 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown

LLGo baseline benchmarks

01395458d0c6 | workflow run | long-term charts

Program measurements

Platform Workload File size vs base Build vs base Run vs base
Linux cprintf 18456 B +0.6% (worse) 309.565 ms +2.7% (worse) 1.341 ms +2.0% (worse)
Linux fmtprintf 1831816 B +0.0% (worse) 3.061 s -1.8% (better) 3.251 ms +4.0% (worse)
Linux println 68008 B +0.4% (worse) 306.260 ms +1.2% (worse) 1.575 ms -2.4% (better)
macOS cprintf 84672 B +0.0% 548.462 ms +26.1% (worse) 4.049 ms +16.2% (worse)
macOS fmtprintf 1869328 B +0.0% 3.667 s +29.9% (worse) 13.935 ms +16.4% (worse)
macOS println 121200 B +0.0% 401.262 ms +3.9% (worse) 4.783 ms +18.6% (worse)
Core language and compiler benchmarks
Platform Benchmark ns/op vs base
Linux BenchmarkLookupPCRandom 13.310 ns/op +0.1% (worse)
Linux BenchmarkMergeCompilerFlags 151.100 ns/op -0.2% (better)
Linux BenchmarkMergeLinkerFlags 94.640 ns/op -2.9% (better)
Linux BenchmarkChannelBuffered 33.700 ns/op +0.1% (worse)
Linux BenchmarkChannelHandoff 27895 ns/op +5.0% (worse)
Linux BenchmarkDefer 49.710 ns/op +0.4% (worse)
Linux BenchmarkDirectCall 1.577 ns/op +1.3% (worse)
Linux BenchmarkGlobalRead 1.556 ns/op -0.1% (better)
Linux BenchmarkGlobalWrite 2.481 ns/op +0.0% (worse)
Linux BenchmarkGoroutine 41737 ns/op +30.1% (worse)
Linux BenchmarkInterfaceCall 8.093 ns/op -0.0% (better)
Linux BenchmarkRuntimeGetG 2.180 ns/op +0.0% (worse)
macOS BenchmarkLookupPCRandom 14.680 ns/op +16.8% (worse)
macOS BenchmarkMergeCompilerFlags 157.400 ns/op +32.0% (worse)
macOS BenchmarkMergeLinkerFlags 90.460 ns/op +2.6% (worse)
macOS BenchmarkChannelBuffered 29.680 ns/op -13.3% (better)
macOS BenchmarkChannelHandoff 7804 ns/op -2.4% (better)
macOS BenchmarkDefer 34.050 ns/op -31.4% (better)
macOS BenchmarkDirectCall 1.185 ns/op -5.7% (better)
macOS BenchmarkGlobalRead 1.149 ns/op -13.0% (better)
macOS BenchmarkGlobalWrite 1.217 ns/op -13.6% (better)
macOS BenchmarkGoroutine 44795 ns/op -16.5% (better)
macOS BenchmarkInterfaceCall 5.032 ns/op -21.8% (better)
macOS BenchmarkRuntimeGetG 2.709 ns/op +17.5% (worse)

Compared with 91d416bb12f2 measured in the same runner job.

@cpunion
cpunion force-pushed the codex/goroot-goexit-20260801 branch from 651c6e7 to 0139545 Compare August 3, 2026 13:54
@xushiwei
xushiwei merged commit d37108c into xgo-dev:main Aug 3, 2026
42 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants