Triage and debug go.dev/issue/78576
#78576 (HTTP content-length flake)
/home/claude/golang_go_78576/go · Tue 25 Aug 2026 15:11:09 · 8 messages10d2d89d-010c-44e4-8cf8-1c57aa7d3172
You Tue 25 Aug 2026 08:10:29
Assistant Tue 25 Aug 2026 08:10:31
I'll start by pulling up the issue.
Ran tools (7 Bash, 1 ToolSearch, 2 WebFetch)
Bash ls -a && echo "---" && cat *.md 2>/dev/null | head -50
ls -a && echo "---" && cat *.md 2>/dev/null | head -50
Inspect working directory
. .. env.sh go ---
ToolSearch select:WebFetch,WebSearch
select:WebFetch,WebSearch
5
WebFetch https://go.dev/issue/78576
https://go.dev/issue/78576
Reproduce the full issue text: title, labels, author, the complete body including any code, and all comments verbatim.
REDIRECT DETECTED: The URL redirects to a location that was not fetched automatically. Original URL: https://go.dev/issue/78576 Redirect URL (from the server's Location header — server-supplied, not verified): https://github.com/golang/go/issues/78576 Status: 302 Found To complete your request, I need to fetch content from the redirected URL. Please use WebFetch again with these parameters: - url: "https://github.com/golang/go/issues/78576" - prompt: "Reproduce the full issue text: title, labels, author, the complete body including any code, and all comments verbatim."
Bash cat /home/claude/golang_go_78576/env.sh; echo "---"; ls /home/claude/golang_go_78576/go | head -40
cat /home/claude/golang_go_78576/env.sh; echo "---"; ls /home/claude/golang_go_78576/go | head -40
Check env and go tree
export WORK=/home/claude/golang_go_78576 export GOROOT=$WORK/go export GOPATH=$WORK export GOBIN=$WORK/bin export PATH=$GOBIN:/home/claude/.local/bin:/home/claude/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin --- api bin codereview.cfg CONTRIBUTING.md doc go.env lib LICENSE misc PATENTS pkg README.md SECURITY.md src test VERSION.cache
WebFetch https://github.com/golang/go/issues/78576
https://github.com/golang/go/issues/78576
Reproduce the full issue text: title, labels, author, the complete body including any code, and all comments verbatim.
# GitHub Issue Summary **Title:** net/http: TestErrorContentLength/h1 failures #78576 **Author:** @gopherbot **Status:** Open **Labels:** - Automation - NeedsInvestigation **Body:** The issue was created automatically by watchflakes to track test failures. The watchflakes configuration specifies: "default <- pkg == 'net/http' && test == 'TestErrorContentLength/h1'" An example failure log is referenced from a Chromium CI build (ID: 8685090845973527137), showing the test entering a PAUSE and CONT state without completing successfully. **Key Details:** - Repository: golang/go - Package affected: net/http - Test name: TestErrorContentLength/h1 - Issue creation date: April 8, 2026 - Issue type: Test flake (intermittent failure) - Project: Test Flakes **Current Status:** The issue awaits investigation to determine whether this represents a genuine bug or a duplicate of an existing issue. No assignee, milestone, or type has been set.
Bash cd /home/claude/golang_go_78576/go && cat VERSION 2>/dev/null; cat VERSION.cache; git log --oneline -3 2>&1 | head; ls s...
cd /home/claude/golang_go_78576/go && cat VERSION 2>/dev/null; cat VERSION.cache; git log --oneline -3 2>&1 | head; ls src | head -20
Check go version
go1.28-devel_13b477edc7 Thu Aug 20 11:16:25 2026 -040013b477edc7 runtime: fix flaky refill detection in TestScanAllocIssue77573 72755de33c os, internal/poll: use splice for copies involving pipes 3e6ad2cd0a cmd/compile: always do rewriteCondSelectIntoMath for powers of two all.bash all.bat all.rc archive arena bootstrap.bash bufio buildall.bash builtin bytes clean.bash clean.bat clean.rc cmd cmp cmp.bash compress container context crypto
Bash curl -sS "https://api.github.com/repos/golang/go/issues/78576" | python3 -c "import json,sys; d=json.load(sys.stdin); pr...
curl -sS "https://api.github.com/repos/golang/go/issues/78576" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d['title']); print(d['created_at'], d['state']); print([l['name'] for l in d['labels']]); print('---BODY---'); print(d['body'])"Fetch issue via API
net/http: TestErrorContentLength/h1 failures
2026-04-08T13:28:15Z open
['NeedsInvestigation', 'Automation']
---BODY---
```
#!watchflakes
default <- pkg == "net/http" && test == "TestErrorContentLength/h1"
```
Issue created automatically to collect these failures.
Example ([log](https://ci.chromium.org/b/8685090845973527137)):
=== RUN TestErrorContentLength/h1
=== PAUSE TestErrorContentLength/h1
=== CONT TestErrorContentLength/h1
— [watchflakes](https://go.dev/wiki/Watchflakes)Bash grep -rn "TestErrorContentLength" src/net/http/
grep -rn "TestErrorContentLength" src/net/http/
Find the test
src/net/http/serve_test.go:7680:func TestErrorContentLength(t *testing.T) { run(t, testErrorContentLength) }Bash curl -sS "https://api.github.com/repos/golang/go/issues/78576/comments" | python3 -c " ...
curl -sS "https://api.github.com/repos/golang/go/issues/78576/comments" | python3 -c "
import json,sys
for c in json.load(sys.stdin):
print('====', c['user']['login'], c['created_at'])
print(c['body'])
"Fetch issue comments
==== gopherbot 2026-04-08T13:28:16Z
Found new dashboard test flakes for:
#!watchflakes
default <- pkg == "net/http" && test == "TestErrorContentLength/h1"
<details><summary>2026-04-08 10:51 gotip-linux-ppc64le_power10 go@b1c8857f net/http.TestErrorContentLength/h1 [SKIP] (<a href="https://ci.chromium.org/b/8685090845973527137">log</a>)</summary>
=== RUN TestErrorContentLength/h1
=== PAUSE TestErrorContentLength/h1
=== CONT TestErrorContentLength/h1
</details>
— [watchflakes](https://go.dev/wiki/Watchflakes)
==== dr2chase 2026-04-13T11:58:54Z
@neild @nicholashusin
another http flake with minimal information about the failureBash sed -n '7660,7740p' src/net/http/serve_test.go
sed -n '7660,7740p' src/net/http/serve_test.go
Read the test
t.Fatal(err)
}
withCL := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) {
fmt.Fprintf(w, "OK")
}))
res, err = withCL.c.Get(withCL.ts.URL)
if err != nil {
t.Fatal(err)
}
// HTTP/3 does not automatically set ContentLength. This is intentional.
if got := res.Header.Get("Content-Length"); got != "2" && mode != http3Mode {
t.Errorf("Content-Length: %q; want 2", got)
}
if err := res.Body.Close(); err != nil {
t.Fatal(err)
}
}
func TestErrorContentLength(t *testing.T) { run(t, testErrorContentLength) }
func testErrorContentLength(t *testing.T, mode testMode) {
const errorBody = "an error occurred"
cst := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) {
w.Header().Set("Content-Length", "1000")
Error(w, errorBody, 400)
}))
res, err := cst.c.Get(cst.ts.URL)
if err != nil {
t.Fatalf("Get(%q) = %v", cst.ts.URL, err)
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
t.Fatalf("io.ReadAll(res.Body) = %v", err)
}
if string(body) != errorBody+"\n" {
t.Fatalf("read body: %q, want %q", string(body), errorBody)
}
}
func TestError(t *testing.T) {
w := httptest.NewRecorder()
w.Header().Set("Content-Length", "1")
w.Header().Set("X-Content-Type-Options", "scratch and sniff")
w.Header().Set("Other", "foo")
Error(w, "oops", 432)
h := w.Header()
for _, hdr := range []string{"Content-Length"} {
if v, ok := h[hdr]; ok {
t.Errorf("%s: %q, want not present", hdr, v)
}
}
if v := h.Get("Content-Type"); v != "text/plain; charset=utf-8" {
t.Errorf("Content-Type: %q, want %q", v, "text/plain; charset=utf-8")
}
if v := h.Get("X-Content-Type-Options"); v != "nosniff" {
t.Errorf("X-Content-Type-Options: %q, want %q", v, "nosniff")
}
}
func TestServerReadAfterWriteHeader100Continue(t *testing.T) {
run(t, testServerReadAfterWriteHeader100Continue)
}
func testServerReadAfterWriteHeader100Continue(t *testing.T, mode testMode) {
t.Skip("https://go.dev/issue/67555")
body := []byte("body")
cst := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) {
w.WriteHeader(200)
NewResponseController(w).Flush()
io.ReadAll(r.Body)
w.Write(body)
}), func(tr *Transport) {
tr.ExpectContinueTimeout = 24 * time.Hour // forever
})
req, _ := NewRequest("GET", cst.ts.URL, strings.NewReader("body"))
req.Header.Set("Expect", "100-continue")
res, err := cst.c.Do(req)
if err != nil {The report is unusual — it's marked [SKIP], not a failure. Let me pull the actual build log.
Ran tools (7 Bash)
Bash curl -sS -L "https://ci.chromium.org/b/8685090845973527137" -o /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-01...
curl -sS -L "https://ci.chromium.org/b/8685090845973527137" -o /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/b.html -w "%{http_code} %{url_effective}\n" 2>&1 | tail -2Fetch build page
200 https://ci.chromium.org/ui/p/golang/builders/ci/gotip-linux-ppc64le_power10/b8685090845973527137
Bash grep -rn "func run(" src/net/http/clientserver_test.go | head; sed -n '1,120p' src/net/http/clientserver_test.go
grep -rn "func run(" src/net/http/clientserver_test.go | head; sed -n '1,120p' src/net/http/clientserver_test.goRead clientserver test harness
// Copyright 2015 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.
// Tests that use both the client & server, in both HTTP/1 and HTTP/2 mode.
package http_test
import (
"bytes"
"compress/gzip"
"context"
"crypto/rand"
"crypto/sha1"
"crypto/tls"
"fmt"
"hash"
"internal/nettest"
"io"
"log"
"maps"
"net"
"net/http"
. "net/http"
"net/http/httptest"
"net/http/httptrace"
"net/http/httputil"
"net/netip"
"net/textproto"
"net/url"
"os"
"reflect"
"runtime"
"slices"
"strings"
"sync"
"sync/atomic"
"testing"
"testing/synctest"
"time"
_ "unsafe" // for linkname
_ "golang.org/x/net/http3"
)
//go:linkname registerHTTP3Transport
func registerHTTP3Transport(*http.Transport, any) error
//go:linkname registerHTTP3Server
func registerHTTP3Server(*http.Server, any) error
type testMode string
const (
http1Mode = testMode("h1") // HTTP/1.1
https1Mode = testMode("https1") // HTTPS/1.1
http2Mode = testMode("h2") // HTTP/2
http2UnencryptedMode = testMode("h2unencrypted") // HTTP/2
http3Mode = testMode("h3") // HTTP/3
)
type (
testAddMode []testMode // default, plus these
testSkipMode []testMode // default, minus these
)
// http3SkippedMode is a convenient alias for []testMode{http1Mode, http2Mode},
// which was the default test mode used by run and runSynctest prior to HTTP/3
// development.
// As we work on getting net/http tests to pass for our x/net HTTP/3
// implementation, tests that still use http3SkippedMode are essentially a list
// of TODOs on what work needs to be done for our HTTP/3 implementation to
// reach basic feature parity with our HTTP/1 and HTTP/2 implementations
var http3SkippedMode = []testMode{http1Mode, http2Mode}
func (m testMode) Scheme() string {
switch m {
case http1Mode, http2UnencryptedMode:
return "http"
case https1Mode, http2Mode, http3Mode:
return "https"
}
panic("unknown testMode")
}
type (
testNotParallelOpt struct{}
testFakeNetOpt struct{}
)
var (
testNotParallel = testNotParallelOpt{}
optFakeNet = testFakeNetOpt{}
)
type TBRun[T any] interface {
testing.TB
Run(string, func(T)) bool
}
// run runs a client/server test in a variety of test configurations.
//
// Tests execute in HTTP/1.1 and HTTP/2 modes by default.
// To run in a different set of configurations, pass a []testMode option.
//
// Tests call t.Parallel() by default.
// To disable parallel execution, pass the testNotParallel option.
func run[T TBRun[T]](t T, f func(t T, mode testMode), opts ...any) {
t.Helper()
modes := []testMode{http1Mode, http2Mode, http3Mode}
parallel := true
for _, opt := range opts {
switch opt := opt.(type) {
case testAddMode:
for _, m := range opt {
if !slices.Contains(modes, m) {
modes = append(modes, m)
}
}Bash cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad && curl -sS -X POST "ht...
cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad && curl -sS -X POST "https://cr-buildbucket.appspot.com/prpc/buildbucket.v2.Builds/GetBuild" -H "Content-Type: application/json" -H "Accept: application/json" -d '{"id":"8685090845973527137","mask":{"fields":"id,builder,status,summary_markdown,steps,output,infra.swarming,start_time,end_time,input"}}' -o gb.json -w "%{http_code}\n"; head -c 300 gb.jsonQuery buildbucket for build details
200
)]}'
{"id":"8685090845973527137","builder":{"project":"golang","bucket":"ci","builder":"gotip-linux-ppc64le_power10"},"startTime":"2026-04-08T11:50:49.060065526Z","endTime":"2026-04-08T12:11:48.548754513Z","status":"FAILURE","summaryMarkdown":"* go on master ([commit b1c8857](https://go.googlesource
Shell cwd was reset to /home/claude/golang_go_78576Bash cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad && tail -c +6 gb.json >...
cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad && tail -c +6 gb.json > gb2.json && python3 -c "
import json
d=json.load(open('gb2.json'))
print('SUMMARY:', d.get('summaryMarkdown'))
for s in d.get('steps',[]):
if s.get('status') not in ('SUCCESS',):
print('--- STEP', s['name'], s['status'])
print((s.get('summaryMarkdown') or '')[:2000])
for l in s.get('logs',[]): print(' log:', l['name'], l.get('viewUrl'))
"Inspect failing steps
SUMMARY: * go on master ([commit b1c8857](https://go.googlesource.com/go/+/b1c8857f95581ef3cb3daa0767985bba9f72320f)) To reproduce, try `gomote repro 8685090845973527137`. Additional links for debugging: * [go tool dist test -json output](https://logs.chromium.org/logs/golang/buildbucket/cr-buildbucket/8685090845973527137/+/u/step/11/log/2) --- STEP run tests FAILURE --- STEP run tests|go tool dist test -json FAILURE log: command https://logs.chromium.org/logs/golang/buildbucket/cr-buildbucket/8685090845973527137/+/u/step/11/log/1 log: output https://logs.chromium.org/logs/golang/buildbucket/cr-buildbucket/8685090845973527137/+/u/step/11/log/2 log: file: dist.testjson https://logs.chromium.org/logs/golang/buildbucket/cr-buildbucket/8685090845973527137/+/u/step/11/log/3 Shell cwd was reset to /home/claude/golang_go_78576
Bash cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad && curl -sS "https://lo...
cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad && curl -sS "https://logs.chromium.org/logs/golang/buildbucket/cr-buildbucket/8685090845973527137/+/u/step/11/log/2?format=raw" -o out.txt -w "%{http_code} %{size_download}\n" && tail -60 out.txtFetch dist test output log
200 12915 ok cmd/go/internal/mvs 0.029s ok cmd/go/internal/str 0.006s ok cmd/go/internal/test 0.081s ok cmd/go/internal/toolchain 0.013s ok cmd/go/internal/vcs 0.009s ok cmd/go/internal/vcweb 0.050s ok cmd/go/internal/vcweb/vcstest 2.914s ok cmd/go/internal/verylongtest 0.015s ok cmd/go/internal/web 0.004s ok cmd/go/internal/work 0.073s ok cmd/gofmt 0.076s ok cmd/internal/archive 1.470s ok cmd/internal/bootstrap_test 0.005s ok cmd/internal/buildid 0.361s ok cmd/internal/cov 0.506s ok cmd/internal/dwarf 0.019s ok cmd/internal/edit 0.006s ok cmd/internal/fuzztest 0.642s ok cmd/internal/goobj 0.009s ok cmd/internal/moddeps 2.431s ok cmd/internal/obj 0.943s ok cmd/internal/obj/arm64 0.508s ok cmd/internal/obj/loong64 0.083s ok cmd/internal/obj/ppc64 0.115s ok cmd/internal/obj/riscv 0.084s ok cmd/internal/obj/s390x 0.002s ok cmd/internal/obj/x86 6.164s ok cmd/internal/objabi 0.012s ok cmd/internal/osinfo 0.006s ok cmd/internal/par 0.016s ok cmd/internal/pgo 0.006s ok cmd/internal/pkgpath 0.036s ok cmd/internal/pkgpattern 0.006s ok cmd/internal/quoted 0.013s ok cmd/internal/script 0.003s ok cmd/internal/src 0.006s ok cmd/internal/sys 0.013s ok cmd/internal/test2json 0.953s ok cmd/link 24.615s ok cmd/link/internal/benchmark 0.018s ok cmd/link/internal/ld 20.061s ok cmd/link/internal/loader 0.037s ok cmd/nm 2.601s ok cmd/objdump 3.740s ok cmd/pack 0.890s ok cmd/pprof 1.796s ok cmd/relnote 0.012s ok cmd/trace 0.184s ok cmd/vet 10.005s Warning: no '=' in invocation-link-artifacts pair: "", ignoring [W2026-04-08T12:11:12.933654Z 551356 0 cmd_stream.go:589] rdb-stream: test process exited with error: exit status 2 [I2026-04-08T12:11:12.933733Z 551356 0 cmd_stream.go:573] rdb-stream: the test process terminated [I2026-04-08T12:11:12.933816Z 551356 0 sink.go:452] SinkServer: shutdown started [I2026-04-08T12:11:12.934015Z 551356 0 sink.go:430] SinkServer: HTTP server stopped with "http: Server closed" [I2026-04-08T12:11:12.934031Z 551356 0 sink_server.go:96] SinkServer: draining TestResult channel started [I2026-04-08T12:11:13.872711Z 551356 0 sink_server.go:98] SinkServer: draining TestResult channel ended [I2026-04-08T12:11:13.872769Z 551356 0 sink_server.go:100] SinkServer: draining Artifact channel started [I2026-04-08T12:11:14.092927Z 551356 0 sink_server.go:102] SinkServer: draining Artifact channel ended [I2026-04-08T12:11:14.093002Z 551356 0 sink.go:455] SinkServer: shutdown completed successfully [I2026-04-08T12:11:14.093019Z 551356 0 cmd_stream.go:498] rdb-stream: exiting with 2 Shell cwd was reset to /home/claude/golang_go_78576
Bash cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad && grep -n -i "FAIL\|pa...
cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad && grep -n -i "FAIL\|panic\|net/http\|--- " out.txt | head -60
Grep for failures in output log
7:go tool dist: Failed: exit status 1 200:FAIL net/http 1080.019s 201:ok net/http/cgi 0.156s 202:ok net/http/cookiejar 0.029s 203:ok net/http/fcgi 0.222s 204:ok net/http/httptest 0.096s 205:ok net/http/httptrace 0.012s 206:ok net/http/httputil 0.770s 207:ok net/http/internal 0.359s 208:ok net/http/internal/ascii 0.006s 209:ok net/http/internal/http2 7.267s 210:ok net/http/pprof 5.099s Shell cwd was reset to /home/claude/golang_go_78576
Bash cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad && curl -sS "https://lo...
cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad && curl -sS "https://logs.chromium.org/logs/golang/buildbucket/cr-buildbucket/8685090845973527137/+/u/step/11/log/3?format=raw" -o dist.testjson -w "%{http_code} %{size_download}\n"Fetch dist.testjson
200 105404917 Shell cwd was reset to /home/claude/golang_go_78576
FAIL net/http 1080.019s — that's a test binary timeout, not an assertion failure. Let me find the panic in the 105MB JSON log.
Ran tools (3 Bash)
Bash cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad && python3 - <<'EOF' ...
cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad && python3 - <<'EOF'
import json
out=open('nethttp.txt','w')
n=0
for line in open('dist.testjson',errors='replace'):
try: d=json.loads(line)
except: continue
if d.get('Package')!='net/http': continue
if d.get('Action')=='output':
out.write(d.get('Output',''))
n+=1
out.close()
print('output lines:',n)
EOF
wc -l nethttp.txtExtract net/http output
output lines: 5385 5385 nethttp.txt Shell cwd was reset to /home/claude/golang_go_78576
Bash cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad && grep -n "panic\|FAIL...
cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad && grep -n "panic\|FAIL\|--- FAIL\|test timed out\|running tests:" nethttp.txt | head -40
Find panic in net/http output
5102:panic: test timed out after 18m0s
5103: running tests:
5107:panic({0x969180?, 0x5517303f4030?})
5108: /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/panic.go:878 +0x184 fp=0x55172f384ee0 sp=0x55172f384e28 pc=0xa4c84
5385:FAIL net/http 1080.019s
Shell cwd was reset to /home/claude/golang_go_78576Bash cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad && sed -n '5100,5112p' ...
cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad && sed -n '5100,5112p' nethttp.txt
Show the timeout panic header
--- PASS: TestServerNoWriteTimeout (0.00s)
--- PASS: TestOmitHTTP2Vet (6.43s)
panic: test timed out after 18m0s
running tests:
TestErrorContentLength/h1 (17m58s)
goroutine 18454 gp=0x55172f0048c0 m=9 mp=0x55172f400808 [running]:
panic({0x969180?, 0x5517303f4030?})
/home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/panic.go:878 +0x184 fp=0x55172f384ee0 sp=0x55172f384e28 pc=0xa4c84
testing.(*M).startAlarm.func1()
/home/swarming/.swarming/w/ir/x/w/goroot/src/testing/testing.go:2955 +0x30c fp=0x55172f384fc0 sp=0x55172f384ee0 pc=0x14e98c
runtime.goexit({})
/home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/asm_ppc64x.s:1067 +0x4 fp=0x55172f384fc0 sp=0x55172f384fc0 pc=0xad8c4
Shell cwd was reset to /home/claude/golang_go_78576This is a genuine hang — TestErrorContentLength/h1 is the only running test, stuck 17m58s. Let me find its goroutine.
Ran tools (2 Bash)
Bash cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad && sed -n '5106,5385p' ...
cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad && sed -n '5106,5385p' nethttp.txt > stack.txt && grep -n "^goroutine" stack.txt | wc -l && grep -n "^goroutine" stack.txt
Extract and enumerate goroutines
17 1:goroutine 18454 gp=0x55172f0048c0 m=9 mp=0x55172f400808 [running]: 11:goroutine 1 gp=0x55172ef061c0 m=nil [chan receive, 17 minutes]: 35:goroutine 2 gp=0x55172ef06c40 m=nil [force gc (idle)]: 47:goroutine 3 gp=0x55172ef07180 m=nil [GC sweep wait]: 61:goroutine 4 gp=0x55172ef07340 m=nil [GC scavenge wait]: 77:goroutine 5 gp=0x55172ef07880 m=nil [GOMAXPROCS updater (idle), 17 minutes]: 89:goroutine 6 gp=0x55172efae000 m=nil [finalizer wait, 17 minutes]: 99:goroutine 7 gp=0x55172efae1c0 m=nil [cleanup wait, 17 minutes]: 113:goroutine 12 gp=0x55172efae540 m=nil [GC worker (idle), 3 minutes]: 125:goroutine 13 gp=0x55172efae700 m=nil [GC worker (idle), 17 minutes]: 137:goroutine 14 gp=0x55172efae8c0 m=nil [GC worker (idle), 17 minutes]: 149:goroutine 15 gp=0x55172efaea80 m=nil [GC worker (idle), 17 minutes]: 161:goroutine 16 gp=0x55172efaec40 m=nil [GC worker (idle), 17 minutes]: 173:goroutine 10231 gp=0x55172f294000 m=nil [IO wait, 17 minutes]: 201:goroutine 1875 gp=0x55172f1056c0 m=nil [chan receive, 17 minutes]: 219:goroutine 5171 gp=0x55173025b180 m=nil [select, 17 minutes]: 253:goroutine 10232 gp=0x55173025b500 m=nil [runnable]: Shell cwd was reset to /home/claude/golang_go_78576
Bash cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad && sed -n '173,280p' st...
cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad && sed -n '173,280p' stack.txt
Print the interesting goroutines
goroutine 10231 gp=0x55172f294000 m=nil [IO wait, 17 minutes]:
runtime.gopark(0x0?, 0x0?, 0x0?, 0x0?, 0x0?)
/home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/proc.go:466 +0x118 fp=0x55172f3a8af0 sp=0x55172f3a8ab8 pc=0xa51d8
runtime.netpollblock(0x0?, 0x0?, 0x0?)
/home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/netpoll.go:575 +0x188 fp=0x55172f3a8b38 sp=0x55172f3a8af0 pc=0x614c8
internal/poll.runtime_pollWait(0x64f410557800, 0x72)
/home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/netpoll.go:351 +0xf4 fp=0x55172f3a8b78 sp=0x55172f3a8b38 pc=0xa4204
internal/poll.(*pollDesc).wait(0x55172f326f80?, 0x100?, 0x0)
/home/swarming/.swarming/w/ir/x/w/goroot/src/internal/poll/fd_poll_runtime.go:84 +0x4c fp=0x55172f3a8bd0 sp=0x55172f3a8b78 pc=0xe37cc
internal/poll.(*pollDesc).waitRead(...)
/home/swarming/.swarming/w/ir/x/w/goroot/src/internal/poll/fd_poll_runtime.go:89
internal/poll.(*FD).Accept(0x55172f326f80)
/home/swarming/.swarming/w/ir/x/w/goroot/src/internal/poll/fd_unix.go:613 +0x288 fp=0x55172f3a8c88 sp=0x55172f3a8bd0 pc=0xe50a8
net.(*netFD).accept(0x55172f326f80)
/home/swarming/.swarming/w/ir/x/w/goroot/src/net/fd_unix.go:150 +0x38 fp=0x55172f3a8d60 sp=0x55172f3a8c88 pc=0x20c578
net.(*TCPListener).accept(0x55172f5839c0)
/home/swarming/.swarming/w/ir/x/w/goroot/src/net/tcpsock_posix.go:159 +0x34 fp=0x55172f3a8dc0 sp=0x55172f3a8d60 pc=0x21f774
net.(*TCPListener).Accept(0x55172f5839c0)
/home/swarming/.swarming/w/ir/x/w/goroot/src/net/tcpsock.go:387 +0x44 fp=0x55172f3a8e20 sp=0x55172f3a8dc0 pc=0x21e8e4
net/http.(*Server).Serve(0x55172f6ccb40, {0x9da598, 0x55172f5839c0})
/home/swarming/.swarming/w/ir/x/w/goroot/src/net/http/server.go:3476 +0x300 fp=0x55172f3a8f68 sp=0x55172f3a8e20 pc=0x346600
net/http/httptest.(*Server).goServe.func1()
/home/swarming/.swarming/w/ir/x/w/goroot/src/net/http/httptest/server.go:341 +0x64 fp=0x55172f3a8fc0 sp=0x55172f3a8f68 pc=0x3975a4
runtime.goexit({})
/home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/asm_ppc64x.s:1067 +0x4 fp=0x55172f3a8fc0 sp=0x55172f3a8fc0 pc=0xad8c4
created by net/http/httptest.(*Server).goServe in goroutine 5171
/home/swarming/.swarming/w/ir/x/w/goroot/src/net/http/httptest/server.go:339 +0x84
goroutine 1875 gp=0x55172f1056c0 m=nil [chan receive, 17 minutes]:
runtime.gopark(0x1?, 0x0?, 0x8?, 0xa7?, 0x299f4?)
/home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/proc.go:466 +0x118 fp=0x55172f25fcc0 sp=0x55172f25fc88 pc=0xa51d8
runtime.chanrecv(0x55172f6a7d80, 0x0, 0x1)
/home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/chan.go:667 +0x55c fp=0x55172f25fd58 sp=0x55172f25fcc0 pc=0x2a11c
runtime.chanrecv1(0x55172f116ee0?, 0x557f00?)
/home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/chan.go:509 +0x10 fp=0x55172f25fd90 sp=0x55172f25fd58 pc=0x29b80
testing.tRunner.func1()
/home/swarming/.swarming/w/ir/x/w/goroot/src/testing/testing.go:2146 +0x3fc fp=0x55172f25ff30 sp=0x55172f25fd90 pc=0x14a73c
testing.tRunner(0x55172f3f9208, 0x9dd298)
/home/swarming/.swarming/w/ir/x/w/goroot/src/testing/testing.go:2195 +0x144 fp=0x55172f25ff90 sp=0x55172f25ff30 pc=0x14a2c4
testing.(*T).Run.gowrap1()
/home/swarming/.swarming/w/ir/x/w/goroot/src/testing/testing.go:2254 +0x2c fp=0x55172f25ffc0 sp=0x55172f25ff90 pc=0x14b2fc
runtime.goexit({})
/home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/asm_ppc64x.s:1067 +0x4 fp=0x55172f25ffc0 sp=0x55172f25ffc0 pc=0xad8c4
created by testing.(*T).Run in goroutine 1
/home/swarming/.swarming/w/ir/x/w/goroot/src/testing/testing.go:2254 +0x494
goroutine 5171 gp=0x55173025b180 m=nil [select, 17 minutes]:
runtime.gopark(0x55172f3b3620?, 0x2?, 0x0?, 0x0?, 0x55172f3b3554?)
/home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/proc.go:466 +0x118 fp=0x55172f3b3378 sp=0x55172f3b3340 pc=0xa51d8
runtime.selectgo(0x55172f3b3620, 0x55172f3b3550, 0xf?, 0x0, 0xf?, 0x1)
/home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/select.go:351 +0xa24 fp=0x55172f3b34f8 sp=0x55172f3b3378 pc=0x7f3e4
net/http.(*Transport).getConn(0x55172f802b60, 0x55172f70d630, {{}, 0x0, {0x55172f311800, 0x4}, {0x55172f1076d0, 0xf}, 0x0})
/home/swarming/.swarming/w/ir/x/w/goroot/src/net/http/transport.go:1590 +0x3ec fp=0x55172f3b36e0 sp=0x55172f3b34f8 pc=0x3582ac
net/http.(*Transport).roundTrip(0x55172f802b60, 0x55172f6d5b80)
/home/swarming/.swarming/w/ir/x/w/goroot/src/net/http/transport.go:708 +0x980 fp=0x55172f3b3898 sp=0x55172f3b36e0 pc=0x353940
net/http.(*Transport).RoundTrip(0x228f10?, 0x9d6c20?)
/home/swarming/.swarming/w/ir/x/w/goroot/src/net/http/roundtrip.go:33 +0x3c fp=0x55172f3b38c8 sp=0x55172f3b3898 pc=0x388dfc
net/http.send(0x55172f6d5b80, {0x9d6c20, 0x55172f802b60}, {0x331d0?, 0x33160?, 0x0?})
/home/swarming/.swarming/w/ir/x/w/goroot/src/net/http/client.go:264 +0x5b0 fp=0x55172f3b3aa0 sp=0x55172f3b38c8 pc=0x319130
net/http.(*Client).send(0x55172faac270, 0x55172f6d5b80, {0x33d4c?, 0x228110?, 0x0?})
/home/swarming/.swarming/w/ir/x/w/goroot/src/net/http/client.go:185 +0x24c fp=0x55172f3b3b40 sp=0x55172f3b3aa0 pc=0x318a0c
net/http.(*Client).do(0x55172faac270, 0x55172f6d5b80)
/home/swarming/.swarming/w/ir/x/w/goroot/src/net/http/client.go:736 +0x884 fp=0x55172f3b3d60 sp=0x55172f3b3b40 pc=0x31ae44
net/http.(*Client).Do(...)
/home/swarming/.swarming/w/ir/x/w/goroot/src/net/http/client.go:595
net/http.(*Client).Get(0x55172faac270, {0x55172f311800?, 0x5000a3?})
/home/swarming/.swarming/w/ir/x/w/goroot/src/net/http/client.go:489 +0x9c fp=0x55172f3b3dc0 sp=0x55172f3b3d60 pc=0x31a49c
net/http_test.testErrorContentLength(0x5517305fa908, {0x5000a3?, 0x69d6419b?})
/home/swarming/.swarming/w/ir/x/w/goroot/src/net/http/serve_test.go:7415 +0x9c fp=0x55172f3b3eb8 sp=0x55172f3b3dc0 pc=0x43c49c
net/http_test.run[...].func1()
/home/swarming/.swarming/w/ir/x/w/goroot/src/net/http/clientserver_test.go:103 +0x124 fp=0x55172f3b3f30 sp=0x55172f3b3eb8 pc=0x49b684
testing.tRunner(0x5517305fa908, 0x55172f2c2ba0)
/home/swarming/.swarming/w/ir/x/w/goroot/src/testing/testing.go:2189 +0x100 fp=0x55172f3b3f90 sp=0x55172f3b3f30 pc=0x14a280
testing.(*T).Run.gowrap1()
/home/swarming/.swarming/w/ir/x/w/goroot/src/testing/testing.go:2254 +0x2c fp=0x55172f3b3fc0 sp=0x55172f3b3f90 pc=0x14b2fc
runtime.goexit({})
/home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/asm_ppc64x.s:1067 +0x4 fp=0x55172f3b3fc0 sp=0x55172f3b3fc0 pc=0xad8c4
created by testing.(*T).Run in goroutine 1875
/home/swarming/.swarming/w/ir/x/w/goroot/src/testing/testing.go:2254 +0x494
goroutine 10232 gp=0x55173025b500 m=nil [runnable]:
runtime.asyncPreempt2()
/home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/preempt.go:320 +0x14 fp=0x55172f655410 sp=0x55172f6553e8 pc=0x67a64
runtime.asyncPreempt()
/home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/preempt_ppc64x.s:75 +0x10c fp=0x55172f655618 sp=0x55172f655410 pc=0xae3dc
context.(*cancelCtx).Deadline(0x69d6419c?)
<autogenerated>:1 +0x10 fp=0x55172f655638 sp=0x55172f655638 pc=0x112c00
net.(*Dialer).deadline(0x55172f6bf320, {0x9dac18?, 0x55172f70d680?}, {0x55172f655640?, 0x388dfc?, 0xa675a0?})
/home/swarming/.swarming/w/ir/x/w/goroot/src/net/dial.go:255 +0xc8 fp=0x55172f6556a0 sp=0x55172f655638 pc=0x2010f8
net.(*Dialer).dialCtx(0x55172f6bf320, {0x9dac18, 0x55172f70d680})
/home/swarming/.swarming/w/ir/x/w/goroot/src/net/dial.go:564 +0x78 fp=0x55172f655748 sp=0x55172f6556a0 pc=0x202bb8
net.(*Dialer).DialContext(0x55172f6bf320, {0x9dac18?, 0x55172f70d680?}, {0x500270, 0x3}, {0x55172f1076d0, 0xf})
/home/swarming/.swarming/w/ir/x/w/goroot/src/net/dial.go:527 +0x94 fp=0x55172f655898 sp=0x55172f655748 pc=0x202514
net/http/httptest.(*Server).Start.func1({0x9dac18, 0x55172f70d680}, {0x500270, 0x3}, {0x55172f1076d0?, 0xf?})
/home/swarming/.swarming/w/ir/x/w/goroot/src/net/http/httptest/server.go:144 +0x188 fp=0x55172f655900 sp=0x55172f655898 pc=0x395a28
net/http.(*Transport).dial(0x34f44?, {0x9dac18?, 0x55172f70d680?}, {0x500270?, 0x88930?}, {0x55172f1076d0?, 0x55172f1076b0?})
/home/swarming/.swarming/w/ir/x/w/goroot/src/net/http/transport.go:1345 +0x140 fp=0x55172f655978 sp=0x55172f655900 pc=0x357340
net/http.(*Transport).dialConn(0x55172f802b60, {0x9dac18, 0x55172f70d680}, {{}, 0x0, {0x55172f311800, 0x4}, {0x55172f1076d0, 0xf}, 0x0}, ...)
/home/swarming/.swarming/w/ir/x/w/goroot/src/net/http/transport.go:1875 +0x774 fp=0x55172f655ec8 sp=0x55172f655978 pc=0x35a134
net/http.(*Transport).dialConnFor(0x55172f802b60, 0x55172f707080)
/home/swarming/.swarming/w/ir/x/w/goroot/src/net/http/transport.go:1686 +0x98 fp=0x55172f655f60 sp=0x55172f655ec8 pc=0x358d38
net/http.(*Transport).startDialConnForLocked.func1()
/home/swarming/.swarming/w/ir/x/w/goroot/src/net/http/transport.go:1667 +0x3c fp=0x55172f655fc0 sp=0x55172f655f60 pc=0x358b6c
runtime.goexit({})
/home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/asm_ppc64x.s:1067 +0x4 fp=0x55172f655fc0 sp=0x55172f655fc0 pc=0xad8c4
created by net/http.(*Transport).startDialConnForLocked in goroutine 5171
/home/swarming/.swarming/w/ir/x/w/goroot/src/net/http/transport.go:1666 +0x140
FAIL net/http 1080.019s
Shell cwd was reset to /home/claude/golang_go_78576Now let me examine the code. The dial goroutine is [runnable] — actively running, not blocked — after 17 minutes.
Ran tools (5 Bash)
Bash cd /home/claude/golang_go_78576/go && grep -n "func (d \*Dialer) dialCtx\|func (d \*Dialer) deadline\|func (d \*Dialer) ...
cd /home/claude/golang_go_78576/go && grep -n "func (d \*Dialer) dialCtx\|func (d \*Dialer) deadline\|func (d \*Dialer) DialContext" src/net/dial.go
Locate dialCtx in current tree
254:func (d *Dialer) deadline(ctx context.Context, now time.Time) (earliest time.Time) {
529:func (d *Dialer) DialContext(ctx context.Context, network, address string) (Conn, error) {
563:func (d *Dialer) dialCtx(ctx context.Context) (context.Context, context.CancelFunc) {Bash git log --oneline -5 b1c8857f 2>&1 | head
git log --oneline -5 b1c8857f 2>&1 | head
Check if failure commit is present locally
b1c8857f95 cmd/internal/obj/arm64: add ASIMD arithmetic instructions 9111d85e2f cmd/internal/obj/arm64: add ASIMD shift instructions 4dffc57944 cmd/internal/obj/arm64: add remaining ASIMD compare instructions f91403c2a0 cmd/internal/obj: refactor arm64 ASIMD instruction encoding 497ec0d4e1 cmd/go/internal/tool: only forward SIGHUP, SIGINT, SIGQUIT, and SIGTERM
Bash sed -n '240,300p' src/net/dial.go && echo "===== dialCtx =====" && sed -n '520,610p' src/net/dial.go
sed -n '240,300p' src/net/dial.go && echo "===== dialCtx =====" && sed -n '520,610p' src/net/dial.go
Read Dialer.deadline and dialCtx
return b
}
if b.IsZero() || a.Before(b) {
return a
}
return b
}
// deadline returns the earliest of:
// - now+Timeout
// - d.Deadline
// - the context's deadline
//
// Or zero, if none of Timeout, Deadline, or context's deadline is set.
func (d *Dialer) deadline(ctx context.Context, now time.Time) (earliest time.Time) {
if d.Timeout != 0 { // including negative, for historical reasons
earliest = now.Add(d.Timeout)
}
if d, ok := ctx.Deadline(); ok {
earliest = minNonzeroTime(earliest, d)
}
return minNonzeroTime(earliest, d.Deadline)
}
func (d *Dialer) resolver() *Resolver {
if d.Resolver != nil {
return d.Resolver
}
return DefaultResolver
}
// partialDeadline returns the deadline to use for a single address,
// when multiple addresses are pending.
func partialDeadline(now, deadline time.Time, addrsRemaining int) (time.Time, error) {
if deadline.IsZero() {
return deadline, nil
}
timeRemaining := deadline.Sub(now)
if timeRemaining <= 0 {
return time.Time{}, errTimeout
}
// Tentatively allocate equal time to each remaining address.
timeout := timeRemaining / time.Duration(addrsRemaining)
// If the time per address is too short, steal from the end of the list.
const saneMinimum = 2 * time.Second
if timeout < saneMinimum {
if timeRemaining < saneMinimum {
timeout = timeRemaining
} else {
timeout = saneMinimum
}
}
return now.Add(timeout), nil
}
func (d *Dialer) fallbackDelay() time.Duration {
if d.FallbackDelay > 0 {
return d.FallbackDelay
} else {
return 300 * time.Millisecond
}
===== dialCtx =====
// network addresses, any dial timeout (from d.Timeout or ctx) is spread
// over each consecutive dial, such that each is given an appropriate
// fraction of the time to connect.
// For example, if a host has 4 IP addresses and the timeout is 1 minute,
// the connect to each single address will be given 15 seconds to complete
// before trying the next one.
//
// See func [Dial] for a description of the network and address
// parameters.
func (d *Dialer) DialContext(ctx context.Context, network, address string) (Conn, error) {
ctx, cancel := d.dialCtx(ctx)
defer cancel()
// Shadow the nettrace (if any) during resolve so Connect events don't fire for DNS lookups.
resolveCtx := ctx
if trace, _ := ctx.Value(nettrace.TraceKey{}).(*nettrace.Trace); trace != nil {
shadow := *trace
shadow.ConnectStart = nil
shadow.ConnectDone = nil
resolveCtx = context.WithValue(resolveCtx, nettrace.TraceKey{}, &shadow)
}
addrs, err := d.resolver().resolveAddrList(resolveCtx, "dial", network, address, d.LocalAddr)
if err != nil {
return nil, &OpError{Op: "dial", Net: network, Source: nil, Addr: nil, Err: err}
}
sd := &sysDialer{
Dialer: *d,
network: network,
address: address,
}
var primaries, fallbacks addrList
if d.dualStack() && network == "tcp" {
primaries, fallbacks = addrs.partition(isIPv4)
} else {
primaries = addrs
}
return sd.dialParallel(ctx, primaries, fallbacks)
}
func (d *Dialer) dialCtx(ctx context.Context) (context.Context, context.CancelFunc) {
if ctx == nil {
panic("nil context")
}
deadline := d.deadline(ctx, time.Now())
var cancel1, cancel2 context.CancelFunc
if !deadline.IsZero() {
testHookStepTime()
if d, ok := ctx.Deadline(); !ok || deadline.Before(d) {
var subCtx context.Context
subCtx, cancel1 = context.WithDeadline(ctx, deadline)
ctx = subCtx
}
}
if oldCancel := d.Cancel; oldCancel != nil {
var subCtx context.Context
subCtx, cancel2 = context.WithCancel(ctx)
go func() {
select {
case <-oldCancel:
cancel2()
case <-subCtx.Done():
}
}()
ctx = subCtx
}
return ctx, func() {
if cancel1 != nil {
cancel1()
}
if cancel2 != nil {
cancel2()
}
}
}
// DialTCP acts like Dial for TCP networks using the provided context.
//
// The provided Context must be non-nil. If the context expires before
// the connection is complete, an error is returned. Once successfully
// connected, any expiration of the context will not affect the
// connection.
//
// The network must be a TCP network name; see func Dial for details.
func (d *Dialer) DialTCP(ctx context.Context, network string, laddr netip.AddrPort, raddr netip.AddrPort) (*TCPConn, error) {
ctx, cancel := d.dialCtx(ctx)
defer cancel()
return dialTCP(ctx, d, network, TCPAddrFromAddrPort(laddr), TCPAddrFromAddrPort(raddr))Bash grep -rn "testHookStepTime" src/ | head -20
grep -rn "testHookStepTime" src/ | head -20
Find testHookStepTime
src/net/timeout_test.go:26: testHookStepTime = func() {
src/net/dial.go:570: testHookStepTime()
src/net/hook.go:26: // testHookStepTime sleeps until time has moved forward by a nonzero amount.
src/net/hook.go:30: testHookStepTime = func() {}Bash cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad && head -20 nethttp.txt...
cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad && head -20 nethttp.txt && echo "=== count of tests run ===" && grep -c "^--- PASS\|^--- FAIL\|^--- SKIP" nethttp.txt; grep -n "ErrorContentLength" nethttp.txt
Inspect net/http log head
=== RUN TestWriteSetCookies
--- PASS: TestWriteSetCookies (0.00s)
=== RUN TestSetCookie
--- PASS: TestSetCookie (0.00s)
=== RUN TestAddCookie
--- PASS: TestAddCookie (0.00s)
=== RUN TestReadSetCookies
--- PASS: TestReadSetCookies (0.02s)
=== RUN TestReadCookies
--- PASS: TestReadCookies (0.02s)
=== RUN TestSetCookieDoubleQuotes
--- PASS: TestSetCookieDoubleQuotes (0.00s)
=== RUN TestCookieSanitizeValue
--- PASS: TestCookieSanitizeValue (0.00s)
=== RUN TestCookieSanitizePath
--- PASS: TestCookieSanitizePath (0.00s)
=== RUN TestCookieValid
--- PASS: TestCookieValid (0.00s)
=== RUN TestParseCookie
--- PASS: TestParseCookie (0.02s)
=== count of tests run ===
1528
1340:=== RUN TestErrorContentLength
1341:=== PAUSE TestErrorContentLength
2445:=== CONT TestErrorContentLength
2446:=== RUN TestErrorContentLength/h1
2447:=== PAUSE TestErrorContentLength/h1
2448:=== RUN TestErrorContentLength/h2
2449:=== PAUSE TestErrorContentLength/h2
3971:=== CONT TestErrorContentLength/h1
3985:=== CONT TestErrorContentLength/h2
3986:--- PASS: TestErrorContentLength/h2 (0.00s)
5104: TestErrorContentLength/h1 (17m58s)
5345:net/http_test.testErrorContentLength(0x5517305fa908, {0x5000a3?, 0x69d6419b?})
Shell cwd was reset to /home/claude/golang_go_78576Strong signal. Let me verify the timeline and check the test isn't inherently flaky.
Ran tools (4 Bash)
Bash cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad && python3 - <<'EOF' ...
cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad && python3 - <<'EOF'
import json
first=None
ev=[]
for line in open('dist.testjson',errors='replace'):
try: d=json.loads(line)
except: continue
if d.get('Package')!='net/http': continue
t=d.get('Time')
if first is None: first=t
if 'ErrorContentLength' in (d.get('Test') or ''):
ev.append((t,d.get('Action'),d.get('Test'),(d.get('Output') or '').strip()[:80]))
print('first net/http event:',first)
for e in ev[:20]: print(e)
EOFTimeline of the subtest events
first net/http event: 2026-04-08T11:52:57.907310858Z
('2026-04-08T11:52:58.770025376Z', 'run', 'TestErrorContentLength', '')
('2026-04-08T11:52:58.770028458Z', 'output', 'TestErrorContentLength', '=== RUN TestErrorContentLength')
('2026-04-08T11:52:58.770050309Z', 'output', 'TestErrorContentLength', '=== PAUSE TestErrorContentLength')
('2026-04-08T11:52:58.770053589Z', 'pause', 'TestErrorContentLength', '')
('2026-04-08T11:52:59.16637058Z', 'cont', 'TestErrorContentLength', '')
('2026-04-08T11:52:59.166373324Z', 'output', 'TestErrorContentLength', '=== CONT TestErrorContentLength')
('2026-04-08T11:52:59.166376453Z', 'run', 'TestErrorContentLength/h1', '')
('2026-04-08T11:52:59.166379238Z', 'output', 'TestErrorContentLength/h1', '=== RUN TestErrorContentLength/h1')
('2026-04-08T11:52:59.166382508Z', 'output', 'TestErrorContentLength/h1', '=== PAUSE TestErrorContentLength/h1')
('2026-04-08T11:52:59.166385547Z', 'pause', 'TestErrorContentLength/h1', '')
('2026-04-08T11:52:59.166388604Z', 'run', 'TestErrorContentLength/h2', '')
('2026-04-08T11:52:59.166391301Z', 'output', 'TestErrorContentLength/h2', '=== RUN TestErrorContentLength/h2')
('2026-04-08T11:52:59.166394574Z', 'output', 'TestErrorContentLength/h2', '=== PAUSE TestErrorContentLength/h2')
('2026-04-08T11:52:59.166399172Z', 'pause', 'TestErrorContentLength/h2', '')
('2026-04-08T11:53:00.290662686Z', 'cont', 'TestErrorContentLength/h1', '')
('2026-04-08T11:53:00.290665254Z', 'output', 'TestErrorContentLength/h1', '=== CONT TestErrorContentLength/h1')
('2026-04-08T11:53:00.310586408Z', 'cont', 'TestErrorContentLength/h2', '')
('2026-04-08T11:53:00.310589806Z', 'output', 'TestErrorContentLength/h2', '=== CONT TestErrorContentLength/h2')
('2026-04-08T11:53:00.314819748Z', 'output', 'TestErrorContentLength/h2', '--- PASS: TestErrorContentLength/h2 (0.00s)')
('2026-04-08T11:53:00.315033246Z', 'pass', 'TestErrorContentLength/h2', '')
Shell cwd was reset to /home/claude/golang_go_78576Bash cd /home/claude/golang_go_78576/go && git log --oneline -8 --since=2026-01-01 -- src/net/dial.go
cd /home/claude/golang_go_78576/go && git log --oneline -8 --since=2026-01-01 -- src/net/dial.go
Recent changes to net/dial.go
a4d587ad78 std: address ptrtoerror analyzer diagnostics cb39d7aa5c net: clarify documentation for Dialer.FallbackDelay 136332571b net: cancel2 should not be shadow
Bash cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad && python3 - <<'EOF' ...
cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad && python3 - <<'EOF'
import json
last=[]
for line in open('dist.testjson',errors='replace'):
try: d=json.loads(line)
except: continue
if d.get('Package')!='net/http': continue
if d.get('Action') in ('pass','fail','skip') and d.get('Test'):
last.append((d['Time'],d['Action'],d['Test']))
last.sort()
print('total test results:',len(last))
for e in last[-6:]: print(e)
EOFFind last completed net/http test
total test results: 1528
('2026-04-08T11:53:02.914801021Z', 'pass', 'TestClientRedirectNoLocation/h2/308')
('2026-04-08T11:53:02.914814146Z', 'pass', 'TestClientRedirectNoLocation/h2')
('2026-04-08T11:53:03.264230858Z', 'pass', 'TestClientRedirectNoLocation')
('2026-04-08T11:53:03.264278026Z', 'pass', 'TestServerNoWriteTimeout/h2')
('2026-04-08T11:53:05.733205497Z', 'pass', 'TestServerNoWriteTimeout')
('2026-04-08T12:10:57.925951878Z', 'pass', 'TestOmitHTTP2Vet')
Shell cwd was reset to /home/claude/golang_go_78576Bash git log -1 --format="%H %ad %s" 136332571b && git show --stat 136332571b | tail -5
git log -1 --format="%H %ad %s" 136332571b && git show --stat 136332571b | tail -5
Check cancel2 shadow fix
Exit code 128 fatal: not a git repository (or any of the parent directories): .git
The timeline is odd — let me check exactly which tests were still outstanding.
Ran tools (2 Bash)
Bash cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad && python3 - <<'EOF' ...
cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad && python3 - <<'EOF'
import json
run={}; done=set(); order=[]
for line in open('dist.testjson',errors='replace'):
try: d=json.loads(line)
except: continue
if d.get('Package')!='net/http': continue
t=d.get('Test'); a=d.get('Action')
if not t: continue
if a=='run': run[t]=d['Time']
elif a in ('pass','fail','skip'): done.add(t)
outstanding=[(v,k) for k,v in run.items() if k not in done]
outstanding.sort()
print('never completed:', len(outstanding))
for e in outstanding: print(e)
EOFList tests that never completed
never completed: 2
('2026-04-08T11:52:58.770025376Z', 'TestErrorContentLength')
('2026-04-08T11:52:59.166376453Z', 'TestErrorContentLength/h1')
Shell cwd was reset to /home/claude/golang_go_78576Bash cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad && python3 - <<'EOF' ...
cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad && python3 - <<'EOF'
import json, collections
h=collections.Counter()
for line in open('dist.testjson',errors='replace'):
try: d=json.loads(line)
except: continue
if d.get('Package')!='net/http': continue
h[d['Time'][11:16]]+=1
if d.get('Test')=='TestOmitHTTP2Vet': print('VET:', d['Time'], d['Action'], (d.get('Output') or '').strip()[:60])
print('--- events per minute (net/http) ---')
for k in sorted(h): print(k, h[k])
EOFEvent rate histogram + TestOmitHTTP2Vet timing
VET: 2026-04-08T11:52:57.982333727Z run
VET: 2026-04-08T11:52:57.982336485Z output === RUN TestOmitHTTP2Vet
VET: 2026-04-08T11:52:57.982340366Z output === PAUSE TestOmitHTTP2Vet
VET: 2026-04-08T11:52:57.982343202Z pause
VET: 2026-04-08T11:52:59.308104911Z cont
VET: 2026-04-08T11:52:59.308109032Z output === CONT TestOmitHTTP2Vet
VET: 2026-04-08T11:53:05.733249305Z output --- PASS: TestOmitHTTP2Vet (6.43s)
VET: 2026-04-08T12:10:57.923052696Z output panic: test timed out after 18m0s
VET: 2026-04-08T12:10:57.923182104Z output running tests:
VET: 2026-04-08T12:10:57.923188006Z output TestErrorContentLength/h1 (17m58s)
VET: 2026-04-08T12:10:57.923191323Z output
VET: 2026-04-08T12:10:57.923194825Z output goroutine 18454 gp=0x55172f0048c0 m=9 mp=0x55172f400808 [run
VET: 2026-04-08T12:10:57.923199565Z output panic({0x969180?, 0x5517303f4030?})
VET: 2026-04-08T12:10:57.923202825Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/panic.g
VET: 2026-04-08T12:10:57.923206037Z output testing.(*M).startAlarm.func1()
VET: 2026-04-08T12:10:57.923209262Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/testing/testing
VET: 2026-04-08T12:10:57.9232122Z output runtime.goexit({})
VET: 2026-04-08T12:10:57.923215608Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/asm_ppc
VET: 2026-04-08T12:10:57.923218569Z output created by time.goFunc
VET: 2026-04-08T12:10:57.923221594Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/time/sleep.go:2
VET: 2026-04-08T12:10:57.923224541Z output
VET: 2026-04-08T12:10:57.923244824Z output goroutine 1 gp=0x55172ef061c0 m=nil [chan receive, 17 minute
VET: 2026-04-08T12:10:57.92324894Z output runtime.gopark(0x55172ef90000?, 0x55172f12d8a0?, 0x8?, 0xa7?
VET: 2026-04-08T12:10:57.923252438Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/proc.go
VET: 2026-04-08T12:10:57.923264387Z output runtime.chanrecv(0x55172f1a2500, 0x0, 0x1)
VET: 2026-04-08T12:10:57.923268787Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/chan.go
VET: 2026-04-08T12:10:57.923272127Z output runtime.chanrecv1(0x55172ef9a2a0?, 0x557f00?)
VET: 2026-04-08T12:10:57.923275356Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/chan.go
VET: 2026-04-08T12:10:57.923278764Z output testing.tRunner.func1()
VET: 2026-04-08T12:10:57.923282125Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/testing/testing
VET: 2026-04-08T12:10:57.923285293Z output testing.tRunner(0x55172efd2248, 0x55172ef75be8)
VET: 2026-04-08T12:10:57.923288635Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/testing/testing
VET: 2026-04-08T12:10:57.923292071Z output testing.runTests({0x0, 0x0}, {0x501e94, 0x8}, 0x55172ef141c8
VET: 2026-04-08T12:10:57.923295535Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/testing/testing
VET: 2026-04-08T12:10:57.923298533Z output testing.(*M).Run(0x55172efa65a0)
VET: 2026-04-08T12:10:57.923301846Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/testing/testing
VET: 2026-04-08T12:10:57.923304885Z output net/http_test.TestMain(0xa8f4c0?)
VET: 2026-04-08T12:10:57.923308264Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/net/http/main_t
VET: 2026-04-08T12:10:57.923311264Z output main.main()
VET: 2026-04-08T12:10:57.923314319Z output _testmain.go:1342 +0xb0 fp=0x55172f12df30 sp=0x55172f12de90
VET: 2026-04-08T12:10:57.923318194Z output runtime.main()
VET: 2026-04-08T12:10:57.923321637Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/proc.go
VET: 2026-04-08T12:10:57.923324639Z output runtime.goexit({})
VET: 2026-04-08T12:10:57.923327951Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/asm_ppc
VET: 2026-04-08T12:10:57.923331135Z output
VET: 2026-04-08T12:10:57.923334129Z output goroutine 2 gp=0x55172ef06c40 m=nil [force gc (idle)]:
VET: 2026-04-08T12:10:57.923337219Z output runtime.gopark(0x87c2fc7c9889d?, 0x0?, 0x0?, 0x0?, 0x0?)
VET: 2026-04-08T12:10:57.923342291Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/proc.go
VET: 2026-04-08T12:10:57.923345444Z output runtime.goparkunlock(...)
VET: 2026-04-08T12:10:57.923348541Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/proc.go
VET: 2026-04-08T12:10:57.923351449Z output runtime.forcegchelper()
VET: 2026-04-08T12:10:57.923354766Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/proc.go
VET: 2026-04-08T12:10:57.923357785Z output runtime.goexit({})
VET: 2026-04-08T12:10:57.923361063Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/asm_ppc
VET: 2026-04-08T12:10:57.923364438Z output created by runtime.init.6 in goroutine 1
VET: 2026-04-08T12:10:57.923384506Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/proc.go
VET: 2026-04-08T12:10:57.923402571Z output
VET: 2026-04-08T12:10:57.923405906Z output goroutine 3 gp=0x55172ef07180 m=nil [GC sweep wait]:
VET: 2026-04-08T12:10:57.923408951Z output runtime.gopark(0xa66b01?, 0x0?, 0x0?, 0x0?, 0x0?)
VET: 2026-04-08T12:10:57.923412535Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/proc.go
VET: 2026-04-08T12:10:57.923417287Z output runtime.goparkunlock(...)
VET: 2026-04-08T12:10:57.923420668Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/proc.go
VET: 2026-04-08T12:10:57.923423701Z output runtime.bgsweep(0x55172ef8c000)
VET: 2026-04-08T12:10:57.923426973Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/mgcswee
VET: 2026-04-08T12:10:57.92343026Z output runtime.gcenable.gowrap1()
VET: 2026-04-08T12:10:57.923433668Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/mgc.go:
VET: 2026-04-08T12:10:57.92343685Z output runtime.goexit({})
VET: 2026-04-08T12:10:57.923440231Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/asm_ppc
VET: 2026-04-08T12:10:57.923443543Z output created by runtime.gcenable in goroutine 1
VET: 2026-04-08T12:10:57.923446856Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/mgc.go:
VET: 2026-04-08T12:10:57.923449602Z output
VET: 2026-04-08T12:10:57.923468037Z output goroutine 4 gp=0x55172ef07340 m=nil [GC scavenge wait]:
VET: 2026-04-08T12:10:57.923471285Z output runtime.gopark(0x10000?, 0x106ef24?, 0x0?, 0x0?, 0x0?)
VET: 2026-04-08T12:10:57.923474658Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/proc.go
VET: 2026-04-08T12:10:57.923477883Z output runtime.goparkunlock(...)
VET: 2026-04-08T12:10:57.923480938Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/proc.go
VET: 2026-04-08T12:10:57.92348394Z output runtime.(*scavengerState).park(0xa676c0)
VET: 2026-04-08T12:10:57.923487701Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/mgcscav
VET: 2026-04-08T12:10:57.923490877Z output runtime.bgscavenge(0x55172ef8c000)
VET: 2026-04-08T12:10:57.923494354Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/mgcscav
VET: 2026-04-08T12:10:57.9234975Z output runtime.gcenable.gowrap2()
VET: 2026-04-08T12:10:57.923502035Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/mgc.go:
VET: 2026-04-08T12:10:57.923504887Z output runtime.goexit({})
VET: 2026-04-08T12:10:57.923508256Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/asm_ppc
VET: 2026-04-08T12:10:57.92351135Z output created by runtime.gcenable in goroutine 1
VET: 2026-04-08T12:10:57.92351441Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/mgc.go:
VET: 2026-04-08T12:10:57.923517285Z output
VET: 2026-04-08T12:10:57.923520473Z output goroutine 5 gp=0x55172ef07880 m=nil [GOMAXPROCS updater (idl
VET: 2026-04-08T12:10:57.923523627Z output runtime.gopark(0x0?, 0x0?, 0x0?, 0x0?, 0x1?)
VET: 2026-04-08T12:10:57.923526848Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/proc.go
VET: 2026-04-08T12:10:57.923530008Z output runtime.goparkunlock(...)
VET: 2026-04-08T12:10:57.923533262Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/proc.go
VET: 2026-04-08T12:10:57.923536319Z output runtime.updateMaxProcsGoroutine()
VET: 2026-04-08T12:10:57.923539545Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/proc.go
VET: 2026-04-08T12:10:57.923542602Z output runtime.goexit({})
VET: 2026-04-08T12:10:57.923545889Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/asm_ppc
VET: 2026-04-08T12:10:57.923549041Z output created by runtime.defaultGOMAXPROCSUpdateEnable in goroutin
VET: 2026-04-08T12:10:57.923552104Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/proc.go
VET: 2026-04-08T12:10:57.92355566Z output
VET: 2026-04-08T12:10:57.923573346Z output goroutine 6 gp=0x55172efae000 m=nil [finalizer wait, 17 minu
VET: 2026-04-08T12:10:57.923576723Z output runtime.gopark(0x0?, 0x9dfe68?, 0x20?, 0x40?, 0x2000000020?)
VET: 2026-04-08T12:10:57.923580096Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/proc.go
VET: 2026-04-08T12:10:57.92358335Z output runtime.runFinalizers()
VET: 2026-04-08T12:10:57.923586668Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/mfinal.
VET: 2026-04-08T12:10:57.923589854Z output runtime.goexit({})
VET: 2026-04-08T12:10:57.923593074Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/asm_ppc
VET: 2026-04-08T12:10:57.923596227Z output created by runtime.createfing in goroutine 1
VET: 2026-04-08T12:10:57.923599444Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/mfinal.
VET: 2026-04-08T12:10:57.923602414Z output
VET: 2026-04-08T12:10:57.923605235Z output goroutine 7 gp=0x55172efae1c0 m=nil [cleanup wait, 17 minute
VET: 2026-04-08T12:10:57.92360841Z output runtime.gopark(0x64f410525800?, 0x393c4?, 0x0?, 0x0?, 0x0?)
VET: 2026-04-08T12:10:57.923611854Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/proc.go
VET: 2026-04-08T12:10:57.923615354Z output runtime.goparkunlock(...)
VET: 2026-04-08T12:10:57.923618414Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/proc.go
VET: 2026-04-08T12:10:57.923621475Z output runtime.(*cleanupQueue).dequeue(0xa67c40)
VET: 2026-04-08T12:10:57.92362485Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/mcleanu
VET: 2026-04-08T12:10:57.923627854Z output runtime.runCleanups()
VET: 2026-04-08T12:10:57.923631069Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/mcleanu
VET: 2026-04-08T12:10:57.923634096Z output runtime.goexit({})
VET: 2026-04-08T12:10:57.92363744Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/asm_ppc
VET: 2026-04-08T12:10:57.923640789Z output created by runtime.(*cleanupQueue).createGs in goroutine 1
VET: 2026-04-08T12:10:57.923644203Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/mcleanu
VET: 2026-04-08T12:10:57.92364726Z output
VET: 2026-04-08T12:10:57.923650252Z output goroutine 12 gp=0x55172efae540 m=nil [GC worker (idle), 3 mi
VET: 2026-04-08T12:10:57.923653533Z output runtime.gopark(0x87c2fc884f14c?, 0x55172f398000?, 0x1c?, 0xa
VET: 2026-04-08T12:10:57.923656916Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/proc.go
VET: 2026-04-08T12:10:57.923660193Z output runtime.gcBgMarkWorker(0x55172ef9a4d0)
VET: 2026-04-08T12:10:57.923663565Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/mgc.go:
VET: 2026-04-08T12:10:57.923666918Z output runtime.gcBgMarkStartWorkers.gowrap1()
VET: 2026-04-08T12:10:57.923670129Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/mgc.go:
VET: 2026-04-08T12:10:57.923674852Z output runtime.goexit({})
VET: 2026-04-08T12:10:57.923678346Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/asm_ppc
VET: 2026-04-08T12:10:57.923683129Z output created by runtime.gcBgMarkStartWorkers in goroutine 11
VET: 2026-04-08T12:10:57.923686502Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/mgc.go:
VET: 2026-04-08T12:10:57.923689385Z output
VET: 2026-04-08T12:10:57.923692447Z output goroutine 13 gp=0x55172efae700 m=nil [GC worker (idle), 17 m
VET: 2026-04-08T12:10:57.923695758Z output runtime.gopark(0x87b5036f8e0d8?, 0x55172f398200?, 0x1c?, 0xa
VET: 2026-04-08T12:10:57.92369941Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/proc.go
VET: 2026-04-08T12:10:57.923702631Z output runtime.gcBgMarkWorker(0x55172ef9a4d0)
VET: 2026-04-08T12:10:57.923706199Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/mgc.go:
VET: 2026-04-08T12:10:57.923709656Z output runtime.gcBgMarkStartWorkers.gowrap1()
VET: 2026-04-08T12:10:57.923713133Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/mgc.go:
VET: 2026-04-08T12:10:57.923716137Z output runtime.goexit({})
VET: 2026-04-08T12:10:57.92371944Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/asm_ppc
VET: 2026-04-08T12:10:57.923723381Z output created by runtime.gcBgMarkStartWorkers in goroutine 11
VET: 2026-04-08T12:10:57.923727692Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/mgc.go:
VET: 2026-04-08T12:10:57.923730594Z output
VET: 2026-04-08T12:10:57.923733545Z output goroutine 14 gp=0x55172efae8c0 m=nil [GC worker (idle), 17 m
VET: 2026-04-08T12:10:57.923736797Z output runtime.gopark(0x87b50052bd095?, 0x1?, 0xb6?, 0x4e?, 0x0?)
VET: 2026-04-08T12:10:57.923740172Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/proc.go
VET: 2026-04-08T12:10:57.923743385Z output runtime.gcBgMarkWorker(0x55172ef9a4d0)
VET: 2026-04-08T12:10:57.923748733Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/mgc.go:
VET: 2026-04-08T12:10:57.923752162Z output runtime.gcBgMarkStartWorkers.gowrap1()
VET: 2026-04-08T12:10:57.923755918Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/mgc.go:
VET: 2026-04-08T12:10:57.923759012Z output runtime.goexit({})
VET: 2026-04-08T12:10:57.923762412Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/asm_ppc
VET: 2026-04-08T12:10:57.923766195Z output created by runtime.gcBgMarkStartWorkers in goroutine 11
VET: 2026-04-08T12:10:57.923769504Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/mgc.go:
VET: 2026-04-08T12:10:57.923772221Z output
VET: 2026-04-08T12:10:57.923775135Z output goroutine 15 gp=0x55172efaea80 m=nil [GC worker (idle), 17 m
VET: 2026-04-08T12:10:57.92377826Z output runtime.gopark(0x87b5020ccaec0?, 0x1?, 0xe6?, 0xb7?, 0x0?)
VET: 2026-04-08T12:10:57.923781789Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/proc.go
VET: 2026-04-08T12:10:57.9237851Z output runtime.gcBgMarkWorker(0x55172ef9a4d0)
VET: 2026-04-08T12:10:57.923788576Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/mgc.go:
VET: 2026-04-08T12:10:57.923791785Z output runtime.gcBgMarkStartWorkers.gowrap1()
VET: 2026-04-08T12:10:57.923796629Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/mgc.go:
VET: 2026-04-08T12:10:57.923801318Z output runtime.goexit({})
VET: 2026-04-08T12:10:57.923805002Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/asm_ppc
VET: 2026-04-08T12:10:57.923808289Z output created by runtime.gcBgMarkStartWorkers in goroutine 11
VET: 2026-04-08T12:10:57.923811565Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/mgc.go:
VET: 2026-04-08T12:10:57.92381442Z output
VET: 2026-04-08T12:10:57.923817941Z output goroutine 16 gp=0x55172efaec40 m=nil [GC worker (idle), 17 m
VET: 2026-04-08T12:10:57.923821166Z output runtime.gopark(0x87b503799186c?, 0x3?, 0x42?, 0x3f?, 0x0?)
VET: 2026-04-08T12:10:57.923824414Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/proc.go
VET: 2026-04-08T12:10:57.923829445Z output runtime.gcBgMarkWorker(0x55172ef9a4d0)
VET: 2026-04-08T12:10:57.92383282Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/mgc.go:
VET: 2026-04-08T12:10:57.923835979Z output runtime.gcBgMarkStartWorkers.gowrap1()
VET: 2026-04-08T12:10:57.923841074Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/mgc.go:
VET: 2026-04-08T12:10:57.923844231Z output runtime.goexit({})
VET: 2026-04-08T12:10:57.923847604Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/asm_ppc
VET: 2026-04-08T12:10:57.923850856Z output created by runtime.gcBgMarkStartWorkers in goroutine 11
VET: 2026-04-08T12:10:57.923854014Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/mgc.go:
VET: 2026-04-08T12:10:57.923856945Z output
VET: 2026-04-08T12:10:57.923859887Z output goroutine 10231 gp=0x55172f294000 m=nil [IO wait, 17 minutes
VET: 2026-04-08T12:10:57.923864227Z output runtime.gopark(0x0?, 0x0?, 0x0?, 0x0?, 0x0?)
VET: 2026-04-08T12:10:57.923867539Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/proc.go
VET: 2026-04-08T12:10:57.923870879Z output runtime.netpollblock(0x0?, 0x0?, 0x0?)
VET: 2026-04-08T12:10:57.923875389Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/netpoll
VET: 2026-04-08T12:10:57.923878754Z output internal/poll.runtime_pollWait(0x64f410557800, 0x72)
VET: 2026-04-08T12:10:57.923883699Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/netpoll
VET: 2026-04-08T12:10:57.92388951Z output internal/poll.(*pollDesc).wait(0x55172f326f80?, 0x100?, 0x0)
VET: 2026-04-08T12:10:57.923894197Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/internal/poll/f
VET: 2026-04-08T12:10:57.923899381Z output internal/poll.(*pollDesc).waitRead(...)
VET: 2026-04-08T12:10:57.92390391Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/internal/poll/f
VET: 2026-04-08T12:10:57.923907191Z output internal/poll.(*FD).Accept(0x55172f326f80)
VET: 2026-04-08T12:10:57.923911596Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/internal/poll/f
VET: 2026-04-08T12:10:57.924107943Z output net.(*netFD).accept(0x55172f326f80)
VET: 2026-04-08T12:10:57.924121537Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/net/fd_unix.go:
VET: 2026-04-08T12:10:57.924133158Z output net.(*TCPListener).accept(0x55172f5839c0)
VET: 2026-04-08T12:10:57.924145014Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/net/tcpsock_pos
VET: 2026-04-08T12:10:57.924169947Z output net.(*TCPListener).Accept(0x55172f5839c0)
VET: 2026-04-08T12:10:57.92418566Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/net/tcpsock.go:
VET: 2026-04-08T12:10:57.924197285Z output net/http.(*Server).Serve(0x55172f6ccb40, {0x9da598, 0x55172f
VET: 2026-04-08T12:10:57.92420882Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/net/http/server
VET: 2026-04-08T12:10:57.924220695Z output net/http/httptest.(*Server).goServe.func1()
VET: 2026-04-08T12:10:57.924232072Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/net/http/httpte
VET: 2026-04-08T12:10:57.924261699Z output runtime.goexit({})
VET: 2026-04-08T12:10:57.924273355Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/asm_ppc
VET: 2026-04-08T12:10:57.924284687Z output created by net/http/httptest.(*Server).goServe in goroutine
VET: 2026-04-08T12:10:57.924295877Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/net/http/httpte
VET: 2026-04-08T12:10:57.924306973Z output
VET: 2026-04-08T12:10:57.924334916Z output goroutine 1875 gp=0x55172f1056c0 m=nil [chan receive, 17 min
VET: 2026-04-08T12:10:57.924346072Z output runtime.gopark(0x1?, 0x0?, 0x8?, 0xa7?, 0x299f4?)
VET: 2026-04-08T12:10:57.924357473Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/proc.go
VET: 2026-04-08T12:10:57.924369818Z output runtime.chanrecv(0x55172f6a7d80, 0x0, 0x1)
VET: 2026-04-08T12:10:57.924381348Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/chan.go
VET: 2026-04-08T12:10:57.924443441Z output runtime.chanrecv1(0x55172f116ee0?, 0x557f00?)
VET: 2026-04-08T12:10:57.924455133Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/chan.go
VET: 2026-04-08T12:10:57.924466201Z output testing.tRunner.func1()
VET: 2026-04-08T12:10:57.924485039Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/testing/testing
VET: 2026-04-08T12:10:57.924496439Z output testing.tRunner(0x55172f3f9208, 0x9dd298)
VET: 2026-04-08T12:10:57.924507814Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/testing/testing
VET: 2026-04-08T12:10:57.924518883Z output testing.(*T).Run.gowrap1()
VET: 2026-04-08T12:10:57.924532822Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/testing/testing
VET: 2026-04-08T12:10:57.924544127Z output runtime.goexit({})
VET: 2026-04-08T12:10:57.924571695Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/asm_ppc
VET: 2026-04-08T12:10:57.924583326Z output created by testing.(*T).Run in goroutine 1
VET: 2026-04-08T12:10:57.924588689Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/testing/testing
VET: 2026-04-08T12:10:57.924591631Z output
VET: 2026-04-08T12:10:57.924594512Z output goroutine 5171 gp=0x55173025b180 m=nil [select, 17 minutes]:
VET: 2026-04-08T12:10:57.924597441Z output runtime.gopark(0x55172f3b3620?, 0x2?, 0x0?, 0x0?, 0x55172f3b
VET: 2026-04-08T12:10:57.924600635Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/proc.go
VET: 2026-04-08T12:10:57.924604004Z output runtime.selectgo(0x55172f3b3620, 0x55172f3b3550, 0xf?, 0x0,
VET: 2026-04-08T12:10:57.924607189Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/select.
VET: 2026-04-08T12:10:57.924610664Z output net/http.(*Transport).getConn(0x55172f802b60, 0x55172f70d630
VET: 2026-04-08T12:10:57.924614064Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/net/http/transp
VET: 2026-04-08T12:10:57.924617258Z output net/http.(*Transport).roundTrip(0x55172f802b60, 0x55172f6d5b
VET: 2026-04-08T12:10:57.924620478Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/net/http/transp
VET: 2026-04-08T12:10:57.924664574Z output net/http.(*Transport).RoundTrip(0x228f10?, 0x9d6c20?)
VET: 2026-04-08T12:10:57.924877972Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/net/http/roundt
VET: 2026-04-08T12:10:57.924908918Z output net/http.send(0x55172f6d5b80, {0x9d6c20, 0x55172f802b60}, {0
VET: 2026-04-08T12:10:57.924915699Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/net/http/client
VET: 2026-04-08T12:10:57.924921756Z output net/http.(*Client).send(0x55172faac270, 0x55172f6d5b80, {0x3
VET: 2026-04-08T12:10:57.924927918Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/net/http/client
VET: 2026-04-08T12:10:57.924933605Z output net/http.(*Client).do(0x55172faac270, 0x55172f6d5b80)
VET: 2026-04-08T12:10:57.924940187Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/net/http/client
VET: 2026-04-08T12:10:57.92494542Z output net/http.(*Client).Do(...)
VET: 2026-04-08T12:10:57.924951664Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/net/http/client
VET: 2026-04-08T12:10:57.924961107Z output net/http.(*Client).Get(0x55172faac270, {0x55172f311800?, 0x5
VET: 2026-04-08T12:10:57.924967511Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/net/http/client
VET: 2026-04-08T12:10:57.924997318Z output net/http_test.testErrorContentLength(0x5517305fa908, {0x5000
VET: 2026-04-08T12:10:57.925014127Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/net/http/serve_
VET: 2026-04-08T12:10:57.92502898Z output net/http_test.run[...].func1()
VET: 2026-04-08T12:10:57.925046285Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/net/http/client
VET: 2026-04-08T12:10:57.92508573Z output testing.tRunner(0x5517305fa908, 0x55172f2c2ba0)
VET: 2026-04-08T12:10:57.925101316Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/testing/testing
VET: 2026-04-08T12:10:57.925116826Z output testing.(*T).Run.gowrap1()
VET: 2026-04-08T12:10:57.925148828Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/testing/testing
VET: 2026-04-08T12:10:57.925162918Z output runtime.goexit({})
VET: 2026-04-08T12:10:57.925178095Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/asm_ppc
VET: 2026-04-08T12:10:57.92519416Z output created by testing.(*T).Run in goroutine 1875
VET: 2026-04-08T12:10:57.925316162Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/testing/testing
VET: 2026-04-08T12:10:57.925332357Z output
VET: 2026-04-08T12:10:57.925346908Z output goroutine 10232 gp=0x55173025b500 m=nil [runnable]:
VET: 2026-04-08T12:10:57.925361355Z output runtime.asyncPreempt2()
VET: 2026-04-08T12:10:57.925386156Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/preempt
VET: 2026-04-08T12:10:57.92541432Z output runtime.asyncPreempt()
VET: 2026-04-08T12:10:57.925430222Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/preempt
VET: 2026-04-08T12:10:57.925444441Z output context.(*cancelCtx).Deadline(0x69d6419c?)
VET: 2026-04-08T12:10:57.925469193Z output <autogenerated>:1 +0x10 fp=0x55172f655638 sp=0x55172f655638
VET: 2026-04-08T12:10:57.925485033Z output net.(*Dialer).deadline(0x55172f6bf320, {0x9dac18?, 0x55172f7
VET: 2026-04-08T12:10:57.925500564Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/net/dial.go:255
VET: 2026-04-08T12:10:57.925517699Z output net.(*Dialer).dialCtx(0x55172f6bf320, {0x9dac18, 0x55172f70d
VET: 2026-04-08T12:10:57.925550445Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/net/dial.go:564
VET: 2026-04-08T12:10:57.925567294Z output net.(*Dialer).DialContext(0x55172f6bf320, {0x9dac18?, 0x5517
VET: 2026-04-08T12:10:57.925583761Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/net/dial.go:527
VET: 2026-04-08T12:10:57.925591513Z output net/http/httptest.(*Server).Start.func1({0x9dac18, 0x55172f7
VET: 2026-04-08T12:10:57.925597101Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/net/http/httpte
VET: 2026-04-08T12:10:57.925639042Z output net/http.(*Transport).dial(0x34f44?, {0x9dac18?, 0x55172f70d
VET: 2026-04-08T12:10:57.925802249Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/net/http/transp
VET: 2026-04-08T12:10:57.925807353Z output net/http.(*Transport).dialConn(0x55172f802b60, {0x9dac18, 0x
VET: 2026-04-08T12:10:57.925812345Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/net/http/transp
VET: 2026-04-08T12:10:57.925815695Z output net/http.(*Transport).dialConnFor(0x55172f802b60, 0x55172f70
VET: 2026-04-08T12:10:57.925819193Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/net/http/transp
VET: 2026-04-08T12:10:57.925822294Z output net/http.(*Transport).startDialConnForLocked.func1()
VET: 2026-04-08T12:10:57.925825728Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/net/http/transp
VET: 2026-04-08T12:10:57.925829413Z output runtime.goexit({})
VET: 2026-04-08T12:10:57.92583282Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/asm_ppc
VET: 2026-04-08T12:10:57.925836005Z output created by net/http.(*Transport).startDialConnForLocked in g
VET: 2026-04-08T12:10:57.925839156Z output /home/swarming/.swarming/w/ir/x/w/goroot/src/net/http/transp
VET: 2026-04-08T12:10:57.925951878Z pass
--- events per minute (net/http) ---
11:52 7370
11:53 2499
12:10 286
Shell cwd was reset to /home/claude/golang_go_78576Only 2 tests never completed, and the process emitted zero output for 17m52s — it was fully idle. The stranded goroutine is stopped inside asyncPreempt2. Let me look at that.
Ran tools (4 Bash)
Bash sed -n '290,340p' src/runtime/preempt.go
sed -n '290,340p' src/runtime/preempt.go
Read asyncPreempt2
Exit code 2 sed: can't read src/runtime/preempt.go: No such file or directory
Bash grep -n "func gopreempt_m\|func goschedImpl\|func goschedguarded_m\|func gosched_m" src/runtime/proc.go; echo ---; sed -...
grep -n "func gopreempt_m\|func goschedImpl\|func goschedguarded_m\|func gosched_m" src/runtime/proc.go; echo ---; sed -n "$(grep -n 'func goschedImpl' src/runtime/proc.go | cut -d: -f1),+30p" src/runtime/proc.go
Read goschedImpl
Exit code 1 ugrep: warning: src/runtime/proc.go: No such file or directory --- ugrep: warning: src/runtime/proc.go: No such file or directory sed: -e expression #1, char 1: unknown command: `,'
Bash cd /home/claude/golang_go_78576/go && sed -n '300,340p' src/runtime/preempt.go && echo "===== goschedImpl =====" && sed ...
cd /home/claude/golang_go_78576/go && sed -n '300,340p' src/runtime/preempt.go && echo "===== goschedImpl =====" && sed -n "$(grep -n 'func goschedImpl' src/runtime/proc.go | cut -d: -f1),+25p" src/runtime/proc.go
Read asyncPreempt2 and goschedImpl
//
// On some platforms, it saves large additional scalar-only register state such
// as vector registers to an "extended register state" on the P.
//
// asyncPreempt is implemented in assembly.
func asyncPreempt()
// asyncPreempt2 is the Go continuation of asyncPreempt.
//
// It must be deeply nosplit because there's untyped data on the stack from
// asyncPreempt.
//
// It must not have any write barriers because we need to limit the amount of
// stack it uses.
//
//go:nosplit
//go:nowritebarrierrec
func asyncPreempt2() {
// We can't grow the stack with untyped data from asyncPreempt, so switch to
// the system stack right away.
mcall(func(gp *g) {
gp.asyncSafePoint = true
// Move the extended register state from the P to the G. We do this now that
// we're on the system stack to avoid stack splits.
xRegSave(gp)
if gp.preemptStop {
preemptPark(gp)
} else {
gopreempt_m(gp)
}
// The above functions never return.
})
// Do not grow the stack below here!
gp := getg()
// Put the extended register state back on the M so resumption can find it.
// We can't do this in asyncPreemptM because the park calls never return.
===== goschedImpl =====
func goschedImpl(gp *g, preempted bool) {
pp := gp.m.p.ptr()
trace := traceAcquire()
status := readgstatus(gp)
if status&^_Gscan != _Grunning {
dumpgstatus(gp)
throw("bad g status")
}
if trace.ok() {
// Trace the event before the transition. It may take a
// stack trace, but we won't own the stack after the
// transition anymore.
if preempted {
trace.GoPreempt()
} else {
trace.GoSched()
}
}
casgstatus(gp, _Grunning, _Grunnable)
if trace.ok() {
traceRelease(trace)
}
dropg()
if preempted && sched.gcwaiting.Load() {
// If preempted for STW, keep the G on the local P in runnextBash sed -n "$(grep -n 'func goschedImpl' src/runtime/proc.go | cut -d: -f1),+60p" src/runtime/proc.go | tail -40
sed -n "$(grep -n 'func goschedImpl' src/runtime/proc.go | cut -d: -f1),+60p" src/runtime/proc.go | tail -40
Rest of goschedImpl
}
dropg()
if preempted && sched.gcwaiting.Load() {
// If preempted for STW, keep the G on the local P in runnext
// so it can keep running immediately after the STW.
runqput(pp, gp, true)
} else {
lock(&sched.lock)
globrunqput(gp)
unlock(&sched.lock)
}
if mainStarted {
wakep()
}
schedule()
}
// Gosched continuation on g0.
func gosched_m(gp *g) {
goschedImpl(gp, false)
}
// goschedguarded is a forbidden-states-avoided version of gosched_m.
func goschedguarded_m(gp *g) {
if !canPreemptM(gp.m) {
gogo(&gp.sched) // never return
}
goschedImpl(gp, false)
}
func gopreempt_m(gp *g) {
goschedImpl(gp, true)
}
// preemptPark parks gp and puts it in _Gpreempted.
//
//go:systemstackThis points at a specific interaction. Let me check procresize — whether it considers runnext when deciding a P has work.
Ran tools (2 Bash)
Bash sed -n "$(grep -n '^func procresize' src/runtime/proc.go | cut -d: -f1),+130p" src/runtime/proc.go | grep -n "runnext\|r...
sed -n "$(grep -n '^func procresize' src/runtime/proc.go | cut -d: -f1),+130p" src/runtime/proc.go | grep -n "runnext\|runqempty\|runq\|_Pidle\|_Prunning\|_Pgcstop\|runnablePs\|link"
Inspect procresize work detection
56: gp.m.p.ptr().status = _Prunning
79: pp.status = _Pidle
109: var runnablePs *p
110: var runnablePsNeedM *p
117: pp.status = _Pidle
118: if runqempty(pp) {
119: pp.link.set(idlePs)Bash sed -n "$(grep -n '^func runqempty' src/runtime/proc.go | cut -d: -f1),+30p" src/runtime/proc.go; echo "=== startTheWorl...
sed -n "$(grep -n '^func runqempty' src/runtime/proc.go | cut -d: -f1),+30p" src/runtime/proc.go; echo "=== startTheWorld wake ==="; sed -n "$(grep -n 'func startTheWorldWithSema' src/runtime/proc.go | cut -d: -f1),+70p" src/runtime/proc.go | grep -n "wakep\|npidle\|nmspinning\|newm\|notewakeup\|idlePs\|runnablePs"
Read runqempty and startTheWorld wakeup logic
func runqempty(pp *p) bool {
// Defend against a race where 1) pp has G1 in runqnext but runqhead == runqtail,
// 2) runqput on pp kicks G1 to the runq, 3) runqget on pp empties runqnext.
// Simply observing that runqhead == runqtail and then observing that runqnext == nil
// does not mean the queue is empty.
for {
head := atomic.Load(&pp.runqhead)
tail := atomic.Load(&pp.runqtail)
runnext := atomic.Loaduintptr((*uintptr)(unsafe.Pointer(&pp.runnext)))
if tail == atomic.Load(&pp.runqtail) {
return head == tail && runnext == 0
}
}
}
// To shake out latent assumptions about scheduling order,
// we introduce some randomness into scheduling decisions
// when running with the race detector.
// The need for this was made obvious by changing the
// (deterministic) scheduling order in Go 1.5 and breaking
// many poorly-written tests.
// With the randomness here, as long as the tests pass
// consistently with -race, they shouldn't have latent scheduling
// assumptions.
const randomizeScheduler = raceenabled
// runqput tries to put g on the local runnable queue.
// If next is false, runqput adds g to the tail of the runnable queue.
// If next is true, runqput puts g in the pp.runnext slot.
// If the run queue is full, runnext puts g on the global queue.
// Executed only by the owner P.
=== startTheWorld wake ===
21: notewakeup(&sched.sysmonnote)
37: notewakeup(&mp.park)
40: newm(nil, p, -1)
63: wakep()The runtime paths look correct. Let me check whether this signature appears in other flakes — especially on the same builder.
Ran tools (2 Bash)
Bash for q in "runtime+goroutine+stuck+runnable+ppc64le" "ppc64le+test+timed+out+after+18m0s" "runtime%3A+stranded+runnable+g...
for q in "runtime+goroutine+stuck+runnable+ppc64le" "ppc64le+test+timed+out+after+18m0s" "runtime%3A+stranded+runnable+goroutine"; do
echo "=== $q"
curl -sS "https://api.github.com/search/issues?q=repo:golang/go+is:issue+$q&sort=created&order=desc&per_page=8" | python3 -c "
import json,sys
d=json.load(sys.stdin)
for i in d.get('items',[]): print(i['number'], i['state'], i['created_at'][:10], i['title'])
"
doneSearch Go tracker for related issues
=== runtime+goroutine+stuck+runnable+ppc64le 75744 closed 2025-10-03 runtime: deadlock in internal/runtime/exithook.Run 74045 closed 2025-06-09 runtime: apparent deadlock after building with PGO 69855 closed 2024-10-12 runtime: memory corruption crashes since Go 1.9 54461 open 2022-08-15 x/tools/gopls: regtest flakes due to hanging go commands 45867 closed 2021-04-30 runtime: broken work conservation due to CL 310850 43930 open 2021-01-26 Go compiler and runtime meeting notes 42237 closed 2020-10-27 runtime: test timeouts / deadlocks on OpenBSD after CL 232298 37126 closed 2020-02-07 runtime: gdb stepping behavior when debugging Go program on ppc64le changed when split-stack prologue marked non-preemptible === ppc64le+test+timed+out+after+18m0s 65311 open 2024-01-26 x/build: timeout on LUCI openbsd-amd64 builder 65171 open 2024-01-19 x/build: time out on LUCI Linux/PPC64x builder === runtime%3A+stranded+runnable+goroutine
Bash for q in "gotip-linux-ppc64le_power10+created:2026-01-01..2026-08-25" "%22test+timed+out%22+ppc64le+created:2026-01-01.....
for q in "gotip-linux-ppc64le_power10+created:2026-01-01..2026-08-25" "%22test+timed+out%22+ppc64le+created:2026-01-01..2026-08-25" "runtime%3A+preempt+ppc64+created:2025-06-01..2026-08-25"; do
echo "=== $q"
curl -sS "https://api.github.com/search/issues?q=repo:golang/go+is:issue+$q&sort=created&order=desc&per_page=15" | python3 -c "
import json,sys
d=json.load(sys.stdin)
print('total',d.get('total_count'))
for i in d.get('items',[]): print(i['number'], i['state'], i['created_at'][:10], i['title'])
"; sleep 2; doneSearch for ppc64le power10 flake issues in 2026
=== gotip-linux-ppc64le_power10+created:2026-01-01..2026-08-25 total 16 81087 open 2026-08-24 net/http:nethttpomithttp2: TestServerEmptyBodyRace/h3 failures 79528 open 2026-05-20 runtime/pprof: TestMemoryProfiler/debug=1 failures 79290 open 2026-05-09 x/net/http2: TestLegacy failures 78915 open 2026-04-22 net/http: TestServerContext_ServerContextKey/h2 failures 78897 open 2026-04-21 net/http: TestTransportRespectRequestWantsClose/h1/DisableKeepAlive=true,RequestClose=false failures 78821 open 2026-04-17 net/http: TestTransportRespectRequestWantsClose/h1/DisableKeepAlive=false,RequestClose=false failures 78576 open 2026-04-08 net/http: TestErrorContentLength/h1 failures 78558 closed 2026-04-07 cmd/internal/testdir:0_10: Test/codegen/condmove.go failures [consistent failure] 78403 closed 2026-03-27 cmd/internal/testdir:1_10: Test/codegen/bits.go failures [consistent failure] 78268 open 2026-03-21 cmd/link/internal/ld: TestLargeTextSectionSplitting failures 78266 open 2026-03-21 cmd/link: TestMachOBuildVersion failures 78210 open 2026-03-18 x/build: ppc64le builders missing curl 78199 open 2026-03-17 net/http: TestServerContentType/h1 failures 78026 open 2026-03-09 x/build: add LUCI linux-riscv64-rva22u64 builder 77742 open 2026-02-23 runtime/trace: TestFlightRecorderGenerationCount/MaxBytes failures === %22test+timed+out%22+ppc64le+created:2026-01-01..2026-08-25 total 0 === runtime%3A+preempt+ppc64+created:2025-06-01..2026-08-25 total 2 78189 open 2026-03-17 proposal: runtime/jit: user frame support for JIT compilers and VMs 78021 closed 2026-03-08 cmd/internal/obj/arm: arm32 tailcall doesn't set spadj
A cluster of net/http h1 flakes on the same builder. Let me check whether they share the signature.
Ran tools (1 Bash)
Bash for n in 78199 78821 78897 78915 81087; do ...
for n in 78199 78821 78897 78915 81087; do echo "===== $n" curl -sS "https://api.github.com/repos/golang/go/issues/$n" | python3 -c " import json,sys d=json.load(sys.stdin) print(d['title']) print(d['body'][:900]) " sleep 1 done
Fetch related flake issue bodies
===== 78199
net/http: TestServerContentType/h1 failures
```
#!watchflakes
default <- pkg == "net/http" && test == "TestServerContentType/h1"
```
Issue created automatically to collect these failures.
Example ([log](https://ci.chromium.org/b/8687055014685645281)):
=== RUN TestServerContentType/h1
=== PAUSE TestServerContentType/h1
=== CONT TestServerContentType/h1
— [watchflakes](https://go.dev/wiki/Watchflakes)
===== 78821
net/http: TestTransportRespectRequestWantsClose/h1/DisableKeepAlive=false,RequestClose=false failures
```
#!watchflakes
default <- pkg == "net/http" && test == "TestTransportRespectRequestWantsClose/h1/DisableKeepAlive=false,RequestClose=false"
```
Issue created automatically to collect these failures.
Example ([log](https://ci.chromium.org/b/8684319785964673617)):
=== RUN TestTransportRespectRequestWantsClose/h1/DisableKeepAlive=false,RequestClose=false
— [watchflakes](https://go.dev/wiki/Watchflakes)
===== 78897
net/http: TestTransportRespectRequestWantsClose/h1/DisableKeepAlive=true,RequestClose=false failures
```
#!watchflakes
default <- pkg == "net/http" && test == "TestTransportRespectRequestWantsClose/h1/DisableKeepAlive=true,RequestClose=false"
```
Issue created automatically to collect these failures.
Example ([log](https://ci.chromium.org/b/8683892575461664369)):
=== RUN TestTransportRespectRequestWantsClose/h1/DisableKeepAlive=true,RequestClose=false
— [watchflakes](https://go.dev/wiki/Watchflakes)
===== 78915
net/http: TestServerContext_ServerContextKey/h2 failures
```
#!watchflakes
default <- pkg == "net/http" && test == "TestServerContext_ServerContextKey/h2"
```
Issue created automatically to collect these failures.
Example ([log](https://ci.chromium.org/b/8683799428027205777)):
=== RUN TestServerContext_ServerContextKey/h2
=== PAUSE TestServerContext_ServerContextKey/h2
=== CONT TestServerContext_ServerContextKey/h2
— [watchflakes](https://go.dev/wiki/Watchflakes)
===== 81087
net/http:nethttpomithttp2: TestServerEmptyBodyRace/h3 failures
```
#!watchflakes
default <- pkg == "net/http:nethttpomithttp2" && test == "TestServerEmptyBodyRace/h3"
```
Issue created automatically to collect these failures.
Example ([log](https://ci.chromium.org/b/8672549833097208977)):
=== RUN TestServerEmptyBodyRace/h3
=== PAUSE TestServerEmptyBodyRace/h3
=== CONT TestServerEmptyBodyRace/h3
serve_test.go:4947: handler ran 21 times; want 20
--- FAIL: TestServerEmptyBodyRace/h3 (1.29s)
— [watchflakes](https://go.dev/wiki/Watchflakes)Let me verify two of the sibling flakes have the identical signature.
Ran tools (3 Bash)
Bash cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad && cat > probe.sh <<'EO...
cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad && cat > probe.sh <<'EOF'
#!/bin/bash
bid=$1
url=$(curl -sS -X POST "https://cr-buildbucket.appspot.com/prpc/buildbucket.v2.Builds/GetBuild" \
-H "Content-Type: application/json" -d "{\"id\":\"$bid\",\"mask\":{\"fields\":\"steps\"}}" \
| tail -c +6 | python3 -c "
import json,sys
d=json.load(sys.stdin)
for s in d.get('steps',[]):
for l in s.get('logs',[]):
if l['name'].startswith('file: dist.testjson'): print(l['viewUrl'])
")
echo "BUILD $bid -> $url"
[ -z "$url" ] && exit 0
curl -sS "$url?format=raw" | python3 -c "
import json,sys
hit=0
for line in sys.stdin:
try: d=json.loads(line)
except: continue
o=d.get('Output') or ''
if 'test timed out' in o:
hit=1; print('PKG',d.get('Package')); print(o.rstrip())
continue
if hit:
if o.startswith('goroutine ') and ('runnable' in o or 'running' in o):
print(o.rstrip()); hit=2
elif hit==2:
print(' ',o.rstrip())
if o.strip()=='' : hit=1
elif 'running tests' in o or o.startswith('\t'):
print(o.rstrip())
" 2>/dev/null | head -60
EOF
chmod +x probe.sh && ./probe.sh 8687055014685645281Probe issue 78199 build for same signature
Traceback (most recent call last):
File "<string>", line 3, in <module>
File "/usr/lib/python3.12/json/__init__.py", line 293, in load
return loads(fp.read(),
^^^^^^^^^
File "<frozen codecs>", line 322, in decode
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xa1 in position 17: invalid start byte
BUILD 8687055014685645281 ->
Shell cwd was reset to /home/claude/golang_go_78576Bash cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad && sed -i 's|-d "{\\"id...
cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad && sed -i 's|-d "{\\"id|-H "Accept: application/json" -d "{\\"id|' probe.sh && head -6 probe.sh && ./probe.sh 8687055014685645281Fix accept header and retry probe
#!/bin/bash
bid=$1
url=$(curl -sS -X POST "https://cr-buildbucket.appspot.com/prpc/buildbucket.v2.Builds/GetBuild" \
-H "Content-Type: application/json" -H "Accept: application/json" -d "{\"id\":\"$bid\",\"mask\":{\"fields\":\"steps\"}}" \
| tail -c +6 | python3 -c "
import json,sys
BUILD 8687055014685645281 -> https://logs.chromium.org/logs/golang/buildbucket/cr-buildbucket/8687055014685645281/+/u/step/11/log/3
PKG testing
panic: test timed out after 10ms
running tests:
PKG testing
panic: test timed out after 10ms
running tests:
/home/swarming/.swarming/w/ir/x/w/goroot/test/235.go
/home/swarming/.swarming/w/ir/x/w/goroot/test/64bit.go
/home/swarming/.swarming/w/ir/x/w/goroot/test/alg.go
/home/swarming/.swarming/w/ir/x/w/goroot/test/alias.go
/home/swarming/.swarming/w/ir/x/w/goroot/test/alias1.go
/home/swarming/.swarming/w/ir/x/w/goroot/test/alias2.go
/home/swarming/.swarming/w/ir/x/w/goroot/test/alias3.go
/home/swarming/.swarming/w/ir/x/w/goroot/test/align.go
/home/swarming/.swarming/w/ir/x/w/goroot/test/append.go
/home/swarming/.swarming/w/ir/x/w/goroot/test/append1.go
/home/swarming/.swarming/w/ir/x/w/goroot/test/args.go
/home/swarming/.swarming/w/ir/x/w/goroot/test/armimm.go
/home/swarming/.swarming/w/ir/x/w/goroot/test/asmhdr.go
/home/swarming/.swarming/w/ir/x/w/goroot/test/assign.go
/home/swarming/.swarming/w/ir/x/w/goroot/test/assign1.go
/home/swarming/.swarming/w/ir/x/w/goroot/test/atomicload.go
/home/swarming/.swarming/w/ir/x/w/goroot/test/bigalg.go
/home/swarming/.swarming/w/ir/x/w/goroot/test/bigmap.go
/home/swarming/.swarming/w/ir/x/w/goroot/test/blank.go
/home/swarming/.swarming/w/ir/x/w/goroot/test/blank1.go
/home/swarming/.swarming/w/ir/x/w/goroot/test/bloop.go
/home/swarming/.swarming/w/ir/x/w/goroot/test/bom.go
/home/swarming/.swarming/w/ir/x/w/goroot/test/bombad.go
/home/swarming/.swarming/w/ir/x/w/goroot/test/bounds.go
/home/swarming/.swarming/w/ir/x/w/goroot/test/cannotassign.go
/home/swarming/.swarming/w/ir/x/w/goroot/test/chancap.go
/home/swarming/.swarming/w/ir/x/w/goroot/test/char_lit.go
/home/swarming/.swarming/w/ir/x/w/goroot/test/char_lit1.go
/home/swarming/.swarming/w/ir/x/w/goroot/test/clear.go
/home/swarming/.swarming/w/ir/x/w/goroot/test/clearfat.go
/home/swarming/.swarming/w/ir/x/w/goroot/test/closedchan.go
/home/swarming/.swarming/w/ir/x/w/goroot/test/closure.go
/home/swarming/.swarming/w/ir/x/w/goroot/test/closure1.go
/home/swarming/.swarming/w/ir/x/w/goroot/test/closure2.go
/home/swarming/.swarming/w/ir/x/w/goroot/test/closure4.go
/home/swarming/.swarming/w/ir/x/w/goroot/test/closure6.go
/home/swarming/.swarming/w/ir/x/w/goroot/test/closure7.go
/home/swarming/.swarming/w/ir/x/w/goroot/test/cmp.go
/home/swarming/.swarming/w/ir/x/w/goroot/test/cmp6.go
/home/swarming/.swarming/w/ir/x/w/goroot/test/cmplx.go
/home/swarming/.swarming/w/ir/x/w/goroot/test/complit.go
/home/swarming/.swarming/w/ir/x/w/goroot/test/complit1.go
/home/swarming/.swarming/w/ir/x/w/goroot/test/compos.go
/home/swarming/.swarming/w/ir/x/w/goroot/test/const.go
/home/swarming/.swarming/w/ir/x/w/goroot/test/const1.go
/home/swarming/.swarming/w/ir/x/w/goroot/test/const2.go
/home/swarming/.swarming/w/ir/x/w/goroot/test/const3.go
/home/swarming/.swarming/w/ir/x/w/goroot/test/const4.go
/home/swarming/.swarming/w/ir/x/w/goroot/test/const5.go
/home/swarming/.swarming/w/ir/x/w/goroot/test/const6.go
/home/swarming/.swarming/w/ir/x/w/goroot/test/const7.go
/home/swarming/.swarming/w/ir/x/w/goroot/test/const8.go
/home/swarming/.swarming/w/ir/x/w/goroot/test/convT2X.go
/home/swarming/.swarming/w/ir/x/w/goroot/test/convert.go
curl: (23) Failure writing output to destination
Shell cwd was reset to /home/claude/golang_go_78576Bash cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad && cat > probe2.py <<'P...
cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad && cat > probe2.py <<'PY'
import json,sys,subprocess,re
bid=sys.argv[1]
r=subprocess.run(["curl","-sS","-X","POST","https://cr-buildbucket.appspot.com/prpc/buildbucket.v2.Builds/GetBuild",
"-H","Content-Type: application/json","-H","Accept: application/json",
"-d",json.dumps({"id":bid,"mask":{"fields":"steps"}})],capture_output=True)
d=json.loads(r.stdout[5:])
url=None
for s in d.get('steps',[]):
for l in s.get('logs',[]):
if l['name'].startswith('file: dist.testjson'): url=l['viewUrl']
print("BUILD",bid,url)
p=subprocess.Popen(["curl","-sS",url+"?format=raw"],stdout=subprocess.PIPE)
cap=[]; state=0
for raw in p.stdout:
try: d=json.loads(raw.decode('utf8','replace'))
except: continue
if not (d.get('Package') or '').startswith('net/http'): continue
o=d.get('Output') or ''
if 'test timed out after' in o and '18m' in o: state=1
if state:
cap.append(o.rstrip())
if len(cap)>4000: break
p.kill()
txt="\n".join(cap)
print("\n".join(cap[:5]))
# print goroutines that are runnable/running non-panic
blocks=re.split(r'\n(?=goroutine )',txt)
for b in blocks:
h=b.split('\n')[0]
if 'runnable' in h or ('running' in h and 'panic' not in b[:200]):
print("\n>>>",'\n'.join(b.split('\n')[:22]))
PY
python3 probe2.py 8687055014685645281Probe 78199 build properly
BUILD 8687055014685645281 https://logs.chromium.org/logs/golang/buildbucket/cr-buildbucket/8687055014685645281/+/u/step/11/log/3
panic: test timed out after 18m0s
running tests:
TestServerContentType/h1 (17m57s)
goroutine 18727 gp=0x71ba4749bc00 m=4 mp=0x71ba46c11808 [running]:
>>> goroutine 9898 gp=0x71ba47704380 m=nil [runnable]:
runtime.asyncPreempt2()
/home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/preempt.go:320 +0x14 fp=0x71ba47608410 sp=0x71ba476083e8 pc=0x679a4
runtime.asyncPreempt()
/home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/preempt_ppc64x.s:75 +0x10c fp=0x71ba47608618 sp=0x71ba47608410 pc=0xae85c
context.(*cancelCtx).Deadline(0x69b9ac35?)
<autogenerated>:1 +0x10 fp=0x71ba47608638 sp=0x71ba47608638 pc=0x113380
net.(*Dialer).deadline(0x71ba47814240, {0x9d9618?, 0x71ba47ae60f0?}, {0x0?, 0x3892fc?, 0xa67520?})
/home/swarming/.swarming/w/ir/x/w/goroot/src/net/dial.go:255 +0xc8 fp=0x71ba476086a0 sp=0x71ba47608638 pc=0x2020b8
net.(*Dialer).dialCtx(0x71ba47814240, {0x9d9618, 0x71ba47ae60f0})
/home/swarming/.swarming/w/ir/x/w/goroot/src/net/dial.go:564 +0x78 fp=0x71ba47608748 sp=0x71ba476086a0 pc=0x203b78
net.(*Dialer).DialContext(0x71ba47814240, {0x9d9618?, 0x71ba47ae60f0?}, {0x500270, 0x3}, {0x71ba4898bb70, 0xf})
/home/swarming/.swarming/w/ir/x/w/goroot/src/net/dial.go:527 +0x94 fp=0x71ba47608898 sp=0x71ba47608748 pc=0x2034d4
net/http/httptest.(*Server).Start.func1({0x9d9618, 0x71ba47ae60f0}, {0x500270, 0x3}, {0x71ba4898bb70?, 0xf?})
/home/swarming/.swarming/w/ir/x/w/goroot/src/net/http/httptest/server.go:144 +0x188 fp=0x71ba47608900 sp=0x71ba47608898 pc=0x395e28
net/http.(*Transport).dial(0x60900?, {0x9d9618?, 0x71ba47ae60f0?}, {0x500270?, 0x350b8?}, {0x71ba4898bb70?, 0x9?})
/home/swarming/.swarming/w/ir/x/w/goroot/src/net/http/transport.go:1345 +0x140 fp=0x71ba47608978 sp=0x71ba47608900 pc=0x357840
net/http.(*Transport).dialConn(0x71ba4746c9c0, {0x9d9618, 0x71ba47ae60f0}, {{}, 0x0, {0x71ba46d54ea0, 0x4}, {0x71ba4898bb70, 0xf}, 0x0}, ...)
/home/swarming/.swarming/w/ir/x/w/goroot/src/net/http/transport.go:1875 +0x774 fp=0x71ba47608ec8 sp=0x71ba47608978 pc=0x35a634
net/http.(*Transport).dialConnFor(0x71ba4746c9c0, 0x71ba474a0580)
/home/swarming/.swarming/w/ir/x/w/goroot/src/net/http/transport.go:1686 +0x98 fp=0x71ba47608f60 sp=0x71ba47608ec8 pc=0x359238
net/http.(*Transport).startDialConnForLocked.func1()
Shell cwd was reset to /home/claude/golang_go_78576Identical signature on a different build/date/test. Let me read the rest of asyncPreempt2 and check the other siblings.
Ran tools (2 Bash)
Bash cd /home/claude/golang_go_78576/go && sed -n '316,360p' src/runtime/preempt.go && echo "=== xRegState arch support ===" ...
cd /home/claude/golang_go_78576/go && sed -n '316,360p' src/runtime/preempt.go && echo "=== xRegState arch support ===" && grep -rn "xRegState\|xRegSave\|xRegRestore" src/runtime/*.go | grep -v "_test" | head -20 && ls src/runtime/preempt_*.s
Read asyncPreempt2 tail and xReg code
//go:nowritebarrierrec
func asyncPreempt2() {
// We can't grow the stack with untyped data from asyncPreempt, so switch to
// the system stack right away.
mcall(func(gp *g) {
gp.asyncSafePoint = true
// Move the extended register state from the P to the G. We do this now that
// we're on the system stack to avoid stack splits.
xRegSave(gp)
if gp.preemptStop {
preemptPark(gp)
} else {
gopreempt_m(gp)
}
// The above functions never return.
})
// Do not grow the stack below here!
gp := getg()
// Put the extended register state back on the M so resumption can find it.
// We can't do this in asyncPreemptM because the park calls never return.
xRegRestore(gp)
gp.asyncSafePoint = false
}
// asyncPreemptStack is the bytes of stack space required to inject an
// asyncPreempt call.
var asyncPreemptStack = ^uintptr(0)
func init() {
f := findfunc(abi.FuncPCABI0(asyncPreempt))
total := funcMaxSPDelta(f)
f = findfunc(abi.FuncPCABIInternal(asyncPreempt2))
total += funcMaxSPDelta(f)
f = findfunc(abi.FuncPCABIInternal(xRegRestore))
total += funcMaxSPDelta(f)
// Add some overhead for return PCs, etc.
asyncPreemptStack = uintptr(total) + 8*goarch.PtrSize
if asyncPreemptStack > stackNosplit {
// We need more than the nosplit limit. This isn't unsafe, but it may
=== xRegState arch support ===
src/runtime/mkpreempt.go:388: const xReg = "AX" // *xRegState
src/runtime/mkpreempt.go:564: const vReg = "R0" // *xRegState
src/runtime/mkpreempt.go:774: const xReg = "R4" // *xRegState
src/runtime/mklockrank.go:201:# xRegState allocator
src/runtime/preempt_noxreg.go:16:// xRegState is defined only so the build fails if we try to define a real
src/runtime/preempt_noxreg.go:17:// xRegState on a noxreg architecture.
src/runtime/preempt_noxreg.go:18:type xRegState struct{}
src/runtime/preempt_noxreg.go:22:func xRegSave(gp *g) {}
src/runtime/preempt_noxreg.go:25:func xRegRestore(gp *g) {}
src/runtime/preempt_xreg.go:17:// definition of an xRegState type for storing the state, and its asyncPreempt
src/runtime/preempt_xreg.go:28:// xRegState is long-lived extended register state. It is allocated off-heap and
src/runtime/preempt_xreg.go:30:type xRegState struct {
src/runtime/preempt_xreg.go:37:// xRegState objects.
src/runtime/preempt_xreg.go:39: state *xRegState
src/runtime/preempt_xreg.go:49: // allocation cache. On exit from preemption, the G's xRegState is always
src/runtime/preempt_xreg.go:52: // delay-freeing the allocated xRegState until after we've definitely
src/runtime/preempt_xreg.go:54: cache *xRegState
src/runtime/preempt_xreg.go:57:// xRegAlloc allocates xRegState objects.
src/runtime/preempt_xreg.go:65: xRegAlloc.alloc.init(unsafe.Sizeof(xRegState{}), nil, nil, &memstats.other_sys)
src/runtime/preempt_xreg.go:68:// xRegSave saves the extended register state on this P to gp.
src/runtime/preempt_386.s
src/runtime/preempt_amd64.s
src/runtime/preempt_arm64.s
src/runtime/preempt_arm.s
src/runtime/preempt_loong64.s
src/runtime/preempt_mips64x.s
src/runtime/preempt_mipsx.s
src/runtime/preempt_ppc64x.s
src/runtime/preempt_riscv64.s
src/runtime/preempt_s390x.s
src/runtime/preempt_wasm.sBash cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad && for b in 86843197859...
cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad && for b in 8684319785964673617 8683892575461664369 8683799428027205777; do python3 probe2.py $b 2>/dev/null | head -14; echo; done
Probe remaining sibling builds
BUILD 8684319785964673617 https://logs.chromium.org/logs/golang/buildbucket/cr-buildbucket/8684319785964673617/+/u/step/11/log/3 panic: test timed out after 18m0s running tests: TestTransportRespectRequestWantsClose/h1 (17m58s) TestTransportRespectRequestWantsClose/h1/DisableKeepAlive=false,RequestClose=false (17m58s) >>> goroutine 6772 gp=0x6b69b06ec380 m=nil [runnable]: runtime.asyncPreempt2() /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/preempt.go:320 +0x14 fp=0x6b69b0b56478 sp=0x6b69b0b56450 pc=0x67b64 runtime.asyncPreempt() /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/preempt_ppc64x.s:75 +0x10c fp=0x6b69b0b56680 sp=0x6b69b0b56478 pc=0xae65c context.(*valueCtx).Done(0x635ea817f330?) <autogenerated>:1 +0x10 fp=0x6b69b0b566a0 sp=0x6b69b0b566a0 pc=0x117e00 BUILD 8683892575461664369 https://logs.chromium.org/logs/golang/buildbucket/cr-buildbucket/8683892575461664369/+/u/step/11/log/3 panic: test timed out after 18m0s running tests: TestTransportRespectRequestWantsClose/h1 (17m58s) TestTransportRespectRequestWantsClose/h1/DisableKeepAlive=true,RequestClose=false (17m58s) >>> goroutine 8472 gp=0x3f6d9c78cc40 m=nil [runnable]: runtime.asyncPreempt2() /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/preempt.go:320 +0x14 fp=0x3f6d9baf6808 sp=0x3f6d9baf67e0 pc=0x67b64 runtime.asyncPreempt() /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/preempt_ppc64x.s:75 +0x10c fp=0x3f6d9baf6a10 sp=0x3f6d9baf6808 pc=0xae65c context.(*valueCtx).Done(0x6b3f2843bfc0?) <autogenerated>:1 +0x10 fp=0x3f6d9baf6a30 sp=0x3f6d9baf6a30 pc=0x117e00 BUILD 8683799428027205777 https://logs.chromium.org/logs/golang/buildbucket/cr-buildbucket/8683799428027205777/+/u/step/11/log/3 panic: test timed out after 18m0s running tests: TestServerContext_ServerContextKey/h2 (17m56s) goroutine 18413 gp=0x42bb69f4380 m=10 mp=0x42bb5df1808 [running]: >>> goroutine 16451 gp=0x42bb64416c0 m=nil [runnable]: runtime.asyncPreempt2() /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/preempt.go:320 +0x14 fp=0x42bb6855468 sp=0x42bb6855440 pc=0x67b64 runtime.asyncPreempt() /home/swarming/.swarming/w/ir/x/w/goroot/src/runtime/preempt_ppc64x.s:75 +0x10c fp=0x42bb6855670 sp=0x42bb6855468 pc=0xae65c context.(*valueCtx).Done(0xb4f94?) <autogenerated>:1 +0x10 fp=0x42bb6855690 sp=0x42bb6855690 pc=0x117e00 Shell cwd was reset to /home/claude/golang_go_78576
Five for five — always a frameless autogenerated context wrapper at +0x10. That fingerprint is too specific to be coincidence. Let me disassemble one for ppc64le.
Ran tools (3 Bash)
Bash cd /home/claude/golang_go_78576/go && ls bin && ./bin/go version 2>&1 | head -2
cd /home/claude/golang_go_78576/go && ls bin && ./bin/go version 2>&1 | head -2
Check for built toolchain
go gofmt go version go1.28-devel_13b477edc7 Thu Aug 20 11:16:25 2026 -0400 linux/amd64
Bash cd /home/claude/golang_go_78576/go && sed -n '1,90p' src/runtime/preempt_ppc64x.s
cd /home/claude/golang_go_78576/go && sed -n '1,90p' src/runtime/preempt_ppc64x.s
Read preempt_ppc64x.s
// Code generated by mkpreempt.go; DO NOT EDIT. //go:build ppc64 || ppc64le #include "go_asm.h" #include "textflag.h" TEXT ·asyncPreempt(SB),NOSPLIT|NOFRAME,$0-0 MOVD R31, -488(R1) MOVD LR, R31 MOVDU R31, -520(R1) MOVD R3, 40(R1) MOVD R4, 48(R1) MOVD R5, 56(R1) MOVD R6, 64(R1) MOVD R7, 72(R1) MOVD R8, 80(R1) MOVD R9, 88(R1) MOVD R10, 96(R1) MOVD R11, 104(R1) MOVD R14, 112(R1) MOVD R15, 120(R1) MOVD R16, 128(R1) MOVD R17, 136(R1) MOVD R18, 144(R1) MOVD R19, 152(R1) MOVD R20, 160(R1) MOVD R21, 168(R1) MOVD R22, 176(R1) MOVD R23, 184(R1) MOVD R24, 192(R1) MOVD R25, 200(R1) MOVD R26, 208(R1) MOVD R27, 216(R1) MOVD R28, 224(R1) MOVD R29, 232(R1) MOVW CR, R31 MOVW R31, 240(R1) MOVD XER, R31 MOVD R31, 248(R1) FMOVD F0, 256(R1) FMOVD F1, 264(R1) FMOVD F2, 272(R1) FMOVD F3, 280(R1) FMOVD F4, 288(R1) FMOVD F5, 296(R1) FMOVD F6, 304(R1) FMOVD F7, 312(R1) FMOVD F8, 320(R1) FMOVD F9, 328(R1) FMOVD F10, 336(R1) FMOVD F11, 344(R1) FMOVD F12, 352(R1) FMOVD F13, 360(R1) FMOVD F14, 368(R1) FMOVD F15, 376(R1) FMOVD F16, 384(R1) FMOVD F17, 392(R1) FMOVD F18, 400(R1) FMOVD F19, 408(R1) FMOVD F20, 416(R1) FMOVD F21, 424(R1) FMOVD F22, 432(R1) FMOVD F23, 440(R1) FMOVD F24, 448(R1) FMOVD F25, 456(R1) FMOVD F26, 464(R1) FMOVD F27, 472(R1) FMOVD F28, 480(R1) FMOVD F29, 488(R1) FMOVD F30, 496(R1) FMOVD F31, 504(R1) MOVFL FPSCR, F0 FMOVD F0, 512(R1) CALL ·asyncPreempt2(SB) FMOVD 512(R1), F0 MOVFL F0, FPSCR FMOVD 504(R1), F31 FMOVD 496(R1), F30 FMOVD 488(R1), F29 FMOVD 480(R1), F28 FMOVD 472(R1), F27 FMOVD 464(R1), F26 FMOVD 456(R1), F25 FMOVD 448(R1), F24 FMOVD 440(R1), F23 FMOVD 432(R1), F22 FMOVD 424(R1), F21 FMOVD 416(R1), F20 FMOVD 408(R1), F19
Bash cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad && mkdir -p wrap && cd ...
cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad && mkdir -p wrap && cd wrap && cat > main.go <<'EOF'
package main
import (
"context"
"fmt"
)
func main() {
ctx := context.WithValue(context.Background(), "k", "v")
ctx2, cancel := context.WithCancel(ctx)
defer cancel()
fmt.Println(ctx2.Done(), ctx.Done())
d, ok := ctx2.Deadline()
fmt.Println(d, ok)
}
EOF
cat > go.mod <<'EOF'
module wrap
go 1.28
EOF
GOROOT=/home/claude/golang_go_78576/go GOOS=linux GOARCH=ppc64le /home/claude/golang_go_78576/go/bin/go build -o wrap.ppc64le . && echo built && GOROOT=/home/claude/golang_go_78576/go /home/claude/golang_go_78576/go/bin/go tool objdump -s 'context\.\(\*valueCtx\)\.Done' wrap.ppc64leDisassemble the autogenerated wrapper for ppc64le
built TEXT context.(*valueCtx).Done(SB) <autogenerated> <autogenerated>:1 0xaa7f0 e8830000 MOVD 0(R3),R4 <autogenerated>:1 0xaa7f4 e8630008 MOVD 8(R3),R3 <autogenerated>:1 0xaa7f8 e9840020 MOVD 32(R4),R12 <autogenerated>:1 0xaa7fc 7d8903a6 MOVD R12,CTR <autogenerated>:1 0xaa800 4e800420 BR (CTR) Shell cwd was reset to /home/claude/golang_go_78576
The wrapper is a frameless tail call, and +0x10 is exactly the BR (CTR). Now the key question: is BR (CTR) marked as an async-unsafe point, and does asyncPreempt preserve CTR?
Ran tools (2 Bash)
Bash cd /home/claude/golang_go_78576/go && grep -n "MarkUnsafePoints\|isUnsafePoint\|isRestartable" src/cmd/internal/obj/ppc6...
cd /home/claude/golang_go_78576/go && grep -n "MarkUnsafePoints\|isUnsafePoint\|isRestartable" src/cmd/internal/obj/ppc64/obj9.go | head
Check ppc64 unsafe point marking
(Bash completed with no output)
Bash grep -n "CTR\|R12" src/runtime/preempt_ppc64x.s | head -20; echo "=== mkpreempt ppc64 ==="; sed -n "$(grep -n 'func genP...
grep -n "CTR\|R12" src/runtime/preempt_ppc64x.s | head -20; echo "=== mkpreempt ppc64 ==="; sed -n "$(grep -n 'func genPPC64' src/runtime/mkpreempt.go | cut -d: -f1),+45p" src/runtime/mkpreempt.go
Check whether asyncPreempt saves CTR on ppc64
142: MOVD 536(R1), R12
144: MOVD R31, CTR
147: JMP (CTR)
=== mkpreempt ppc64 ===
func genPPC64(g *gen) {
p := g.p
// Add integer registers R3-R29
// R0 (zero), R1 (SP), R30 (g) are special and not saved here.
// R2 (TOC pointer in PIC mode), R12 (function entry address in PIC mode) have been saved in sigctxt.pushCall.
// R31 (REGTMP) will be saved manually.
var l = layout{sp: "R1", stack: 32 + 8} // MinFrameSize on PPC64, plus one word for saving R31
for i := 3; i <= 29; i++ {
if i == 12 || i == 13 {
// R12 has been saved in sigctxt.pushCall.
// R13 is TLS pointer, not used by Go code. we must NOT
// restore it, otherwise if we parked and resumed on a
// different thread we'll mess up TLS addresses.
continue
}
reg := fmt.Sprintf("R%d", i)
l.add("MOVD", reg, 8)
}
l.addSpecial(
"MOVW CR, R31\nMOVW R31, %d(R1)",
"MOVW %d(R1), R31\nMOVFL R31, $0xff", // this is MOVW R31, CR
8) // CR is 4-byte wide, but just keep the alignment
l.addSpecial(
"MOVD XER, R31\nMOVD R31, %d(R1)",
"MOVD %d(R1), R31\nMOVD R31, XER",
8)
// Add floating point registers F0-F31.
for i := 0; i <= 31; i++ {
reg := fmt.Sprintf("F%d", i)
l.add("FMOVD", reg, 8)
}
// Add floating point control/status register FPSCR.
l.addSpecial(
"MOVFL FPSCR, F0\nFMOVD F0, %d(R1)",
"FMOVD %d(R1), F0\nMOVFL F0, FPSCR",
8)
p("MOVD R31, -%d(R1)", l.stack-32) // save R31 first, we'll use R31 for saving LR
p("MOVD LR, R31")
p("MOVDU R31, -%d(R1)", l.stack) // allocate frame, save PC of interrupted instruction (in LR)
l.save(g)
p("CALL ·asyncPreempt2(SB)")
l.restore(g)asyncPreempt on ppc64 saves R3–R29, CR, XER, F0–F31, FPSCR — but not CTR. Let me read its resume tail.
Ran tools (2 Bash)
Bash sed -n '135,150p' src/runtime/preempt_ppc64x.s; echo "=== mkpreempt tail ==="; sed -n "$(grep -n 'func genPPC64' src/run...
sed -n '135,150p' src/runtime/preempt_ppc64x.s; echo "=== mkpreempt tail ==="; sed -n "$(grep -n 'func genPPC64' src/runtime/mkpreempt.go | cut -d: -f1),+60p" src/runtime/mkpreempt.go | tail -16
Read asyncPreempt resume tail
MOVD 64(R1), R6
MOVD 56(R1), R5
MOVD 48(R1), R4
MOVD 40(R1), R3
MOVD 520(R1), R31
MOVD R31, LR
MOVD 528(R1), R2
MOVD 536(R1), R12
MOVD (R1), R31
MOVD R31, CTR
MOVD 32(R1), R31
ADD $552, R1
JMP (CTR)
=== mkpreempt tail ===
p("MOVD %d(R1), R31", l.stack) // sigctxt.pushCall has pushed LR, R2, R12 (at interrupt) on stack, restore them
p("MOVD R31, LR")
p("MOVD %d(R1), R2", l.stack+8)
p("MOVD %d(R1), R12", l.stack+16)
p("MOVD (R1), R31") // load PC to CTR
p("MOVD R31, CTR")
p("MOVD 32(R1), R31") // restore R31
p("ADD $%d, R1", l.stack+32) // pop frame (including the space pushed by sigctxt.pushCall)
p("JMP (CTR)")
}
func genRISCV64(g *gen) {
p := g.p
// X0 (zero), X1 (LR), X2 (SP), X3 (GP), X4 (TP), X27 (g), X31 (TMP) are special.Bash grep -rn "unsafePoint\|UnsafePoint\|unsafeMark" src/cmd/internal/obj/ppc64/*.go | head -20; echo "=== generic ==="; grep...
grep -rn "unsafePoint\|UnsafePoint\|unsafeMark" src/cmd/internal/obj/ppc64/*.go | head -20; echo "=== generic ==="; grep -rn "func MarkUnsafePoints" src/cmd/internal/obj/*.go
Look for ppc64 unsafe-point marking
src/cmd/internal/obj/ppc64/obj9.go:1289: p = c.ctxt.StartUnsafePoint(p, c.newprog)
src/cmd/internal/obj/ppc64/obj9.go:1478: p = c.ctxt.EndUnsafePoint(p, c.newprog, -1)
=== generic ===
src/cmd/internal/obj/plist.go:344:func MarkUnsafePoints(ctxt *Link, p0 *Prog, newprog ProgAlloc, isUnsafePoint, isRestartable func(*Prog) bool) {Confirmed the mechanism. Now let me check whether the compiler marks the MOVD Rx,CTR; BR/BL (CTR) sequence as async-unsafe.
Ran tools (4 Bash)
Bash grep -n "REG_CTR\|ACTR\|CTR" src/cmd/internal/obj/ppc64/obj9.go | head -40
grep -n "REG_CTR\|ACTR\|CTR" src/cmd/internal/obj/ppc64/obj9.go | head -40
Find CTR handling in ppc64 obj
972: // Move target address into REG_CTR. 973: // (Indirect branches can only go to REG_LR or REG_CTR.) 980: p.To.Reg = REG_CTR 981: retReg = REG_CTR
Bash grep -n "CALLtail\|BR (CTR)\|ABR\|obj.ARET" src/cmd/compile/internal/ppc64/ssa.go | head -20
grep -n "CALLtail\|BR (CTR)\|ABR\|obj.ARET" src/cmd/compile/internal/ppc64/ssa.go | head -20
Find tail call emission
1875: case ssaop.OpPPC64CALLtail, ssaop.OpPPC64CALLtailinter: 2096: s.Prog(obj.ARET)
Bash sed -n '945,1010p' src/cmd/internal/obj/ppc64/obj9.go
sed -n '945,1010p' src/cmd/internal/obj/ppc64/obj9.go
Read the RET/tail-call lowering
if c.cursym.Func().Text.Mark&LEAF != 0 {
c.cursym.Set(obj.AttrLeaf, true)
break
}
if NeedTOCpointer(c.ctxt) {
q = obj.Appendp(q, c.newprog)
q.As = AMOVD
q.Pos = p.Pos
q.From.Type = obj.TYPE_REG
q.From.Reg = REG_R2
q.To.Type = obj.TYPE_MEM
q.To.Reg = REGSP
q.To.Offset = 24
}
case obj.ARET:
if p.From.Type == obj.TYPE_CONST {
c.ctxt.Diag("using BECOME (%v) is not supported!", p)
break
}
retTarget, retReg := p.To.Sym, p.To.Reg
if retReg == obj.REG_NONE {
retReg = REG_LR
} else {
// Move target address into REG_CTR.
// (Indirect branches can only go to REG_LR or REG_CTR.)
x := newprog()
*x = *p
p.As = AMOVD
p.From.Type = obj.TYPE_REG
p.From.Reg = retReg
p.To.Type = obj.TYPE_REG
p.To.Reg = REG_CTR
retReg = REG_CTR
p.Link = x
p = x
}
if c.cursym.Func().Text.Mark&LEAF != 0 {
if autosize == 0 {
p.As = ABR
p.From = obj.Addr{}
if retTarget == nil {
p.To.Type = obj.TYPE_REG
p.To.Reg = retReg
} else {
p.To.Type = obj.TYPE_BRANCH
p.To.Sym = retTarget
}
p.Mark |= BRANCH
break
}
p.As = AADD
p.From.Type = obj.TYPE_CONST
p.From.Offset = int64(autosize)
p.To.Type = obj.TYPE_REG
p.To.Reg = REGSP
p.Spadj = -autosize
q = c.newprog()
q.As = ABR
q.Pos = p.PosBash sed -n "$(grep -n 'func isAsyncSafePoint' src/runtime/preempt.go | cut -d: -f1),+60p" src/runtime/preempt.go
sed -n "$(grep -n 'func isAsyncSafePoint' src/runtime/preempt.go | cut -d: -f1),+60p" src/runtime/preempt.go
Read isAsyncSafePoint
func isAsyncSafePoint(gp *g, pc, sp, lr uintptr) (bool, uintptr) {
mp := gp.m
// Only user Gs can have safe-points. We check this first
// because it's extremely common that we'll catch mp in the
// scheduler processing this G preemption.
if mp.curg != gp {
return false, 0
}
// Check M state.
if mp.p == 0 || !canPreemptM(mp) {
return false, 0
}
// Check stack space.
if sp < gp.stack.lo || sp-gp.stack.lo < asyncPreemptStack {
return false, 0
}
// If we're in the middle of a secret computation, we can't
// allow any conservative scanning of stacks, as that may lead
// to secrets leaking out from the stack into work buffers.
// Additionally, the preemption code will store the
// machine state (including registers which may contain confidential
// information) into the preemption buffers.
//
// TODO(dmo): there's technically nothing stopping us from doing the
// preemption, granted that don't conservatively scan and we clean up after
// ourselves. This is made slightly harder by the xRegs cached allocations
// that can move between Gs and Ps. In any case, for the intended users (cryptography code)
// they are unlikely get stuck in unterminating loops.
if goexperiment.RuntimeSecret && gp.secret > 0 {
return false, 0
}
// Check if PC is an unsafe-point.
f := findfunc(pc)
if !f.valid() {
// Not Go code.
return false, 0
}
if (GOARCH == "mips" || GOARCH == "mipsle" || GOARCH == "mips64" || GOARCH == "mips64le") && lr == pc+8 && funcspdelta(f, pc) == 0 {
// We probably stopped at a half-executed CALL instruction,
// where the LR is updated but the PC has not. If we preempt
// here we'll see a seemingly self-recursive call, which is in
// fact not.
// This is normally ok, as we use the return address saved on
// stack for unwinding, not the LR value. But if this is a
// call to morestack, we haven't created the frame, and we'll
// use the LR for unwinding, which will be bad.
return false, 0
}
up, startpc := pcdatavalue2(f, abi.PCDATA_UnsafePoint, pc)
if up == abi.UnsafePointUnsafe {
// Unsafe-point marked by compiler. This includes
// atomic sequences (e.g., write barrier) and nosplit
// functions (except at calls).
return false, 0
}
if fd := funcdata(f, abi.FUNCDATA_LocalsPointerMaps); fd == nil || f.flag&abi.FuncFlagAsm != 0 {Let me check the compiler's PCDATA for that wrapper directly.
Ran tools (2 Bash)
Bash cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/wrap && GOROOT=/home/cl...
cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/wrap && GOROOT=/home/claude/golang_go_78576/go GOOS=linux GOARCH=ppc64le /home/claude/golang_go_78576/go/bin/go build -a -gcflags='context=-S' -o /dev/null . 2>&1 | grep -A14 'valueCtx).Done(SB)' | head -30
Dump compiler assembly for the wrapper
0x0000 00000 (<autogenerated>:1) TEXT context.(*valueCtx).Done(SB), DUPOK|NOSPLIT|LEAF|NOFRAME|ABIInternal, $0-8 0x0000 00000 (<autogenerated>:1) FUNCDATA $0, gclocals·wvjpxkknJ4nY1JtrArJJaw==(SB) 0x0000 00000 (<autogenerated>:1) FUNCDATA $1, gclocals·J26BEvPExEQhJvjp9E8Whg==(SB) 0x0000 00000 (<autogenerated>:1) FUNCDATA $5, context.(*valueCtx).Done.arginfo1(SB) 0x0000 00000 (<autogenerated>:1) FUNCDATA $6, context.(*valueCtx).Done.argliveinfo(SB) 0x0000 00000 (<autogenerated>:1) PCDATA $3, $1 0x0000 00000 (<autogenerated>:1) MOVD (R3), R4 0x0004 00004 (<autogenerated>:1) MOVD 8(R3), R3 0x0008 00008 (<autogenerated>:1) MOVD 32(R4), R12 0x000c 00012 (<autogenerated>:1) PCDATA $1, $0 0x000c 00012 (<autogenerated>:1) MOVD R12, CTR 0x0010 00016 (<autogenerated>:1) JMP CTR 0x0000 00 00 83 e8 08 00 63 e8 20 00 84 e9 a6 03 89 7d ......c. ......} 0x0010 20 04 80 4e ..N context.valueCtx.Err STEXT dupok size=140 align=0x0 args=0x30 locals=0x8 funcid=0x17 Shell cwd was reset to /home/claude/golang_go_78576
Bash cd /home/claude/golang_go_78576/go && grep -n "PCDATA_UnsafePoint\|UnsafePointUnsafe\|UnsafePointSafe\|UnsafePointRestar...
cd /home/claude/golang_go_78576/go && grep -n "PCDATA_UnsafePoint\|UnsafePointUnsafe\|UnsafePointSafe\|UnsafePointRestart" src/internal/abi/symtab.go; echo "=== obj usage ==="; grep -rn "UnsafePointUnsafe\|PCDATA_UnsafePoint" src/cmd/internal/obj/*.go | head -20
Check PCDATA index constants
108: PCDATA_UnsafePoint = 0
124:// Special values for the PCDATA_UnsafePoint table.
126: UnsafePointSafe = -1 // Safe for async preemption
127: UnsafePointUnsafe = -2 // Unsafe for async preemption
129: // UnsafePointRestart1(2) apply on a sequence of instructions, within
134: UnsafePointRestart1 = -3
135: UnsafePointRestart2 = -4
137: // Like UnsafePointRestart1, but back to function entry if async preempted.
138: UnsafePointRestartAtEntry = -5
=== obj usage ===
src/cmd/internal/obj/plist.go:297: pcdata.From.Offset = abi.PCDATA_UnsafePoint
src/cmd/internal/obj/plist.go:312: pcdata.From.Offset = abi.PCDATA_UnsafePoint
src/cmd/internal/obj/plist.go:314: pcdata.To.Offset = abi.UnsafePointUnsafe
src/cmd/internal/obj/plist.go:327: pcdata.From.Offset = abi.PCDATA_UnsafePoint
src/cmd/internal/obj/plist.go:353: if p.As == APCDATA && p.From.Offset == abi.PCDATA_UnsafePoint {
src/cmd/internal/obj/plist.go:357: if prevPcdata == abi.UnsafePointUnsafe {
src/cmd/internal/obj/plist.go:384: q.From.Offset = abi.PCDATA_UnsafePoint
src/cmd/internal/obj/plist.go:401: p.From.Offset = abi.PCDATA_UnsafePointThe mechanism is confirmed by construction. Let me check the blast radius — whether regular indirect calls are protected.
Ran tools (6 Bash)
Bash sed -n '124,145p' src/internal/abi/symtab.go; echo "=== restart usages ==="; grep -rn "UnsafePointRestart" src/cmd/inter...
sed -n '124,145p' src/internal/abi/symtab.go; echo "=== restart usages ==="; grep -rn "UnsafePointRestart" src/cmd/internal/obj/ | grep -v "_test" | head
Read UnsafePointRestart semantics
// Special values for the PCDATA_UnsafePoint table. const ( UnsafePointSafe = -1 // Safe for async preemption UnsafePointUnsafe = -2 // Unsafe for async preemption // UnsafePointRestart1(2) apply on a sequence of instructions, within // which if an async preemption happens, we should back off the PC // to the start of the sequence when resuming. // We need two so we can distinguish the start/end of the sequence // in case that two sequences are next to each other. UnsafePointRestart1 = -3 UnsafePointRestart2 = -4 // Like UnsafePointRestart1, but back to function entry if async preempted. UnsafePointRestartAtEntry = -5 ) const MINFUNC = 16 // minimum size for a function const FuncTabBucketSize = 256 * MINFUNC // size of bucket in the pc->func lookup table === restart usages === src/cmd/internal/obj/plist.go:376: val := int64(abi.UnsafePointRestart1) src/cmd/internal/obj/plist.go:378: val = abi.UnsafePointRestart2
Bash cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/wrap && cat > c2.go <<'...
cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/wrap && cat > c2.go <<'EOF'
package main
type I interface{ M() int }
type T struct{ x int }
func (t *T) M() int { return t.x }
//go:noinline
func callit(i I) int { return i.M() + 1 }
func init() { _ = callit }
EOF
GOROOT=/home/claude/golang_go_78576/go GOOS=linux GOARCH=ppc64le /home/claude/golang_go_78576/go/bin/go build -gcflags='-S' -o /dev/null . 2>&1 | grep -A20 '^main.callit(SB)' | head -24Check PCDATA around an indirect call
Shell cwd was reset to /home/claude/golang_go_78576
Bash cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/wrap && ls && GOROOT=/h...
cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/wrap && ls && GOROOT=/home/claude/golang_go_78576/go GOOS=linux GOARCH=ppc64le /home/claude/golang_go_78576/go/bin/go build -gcflags='-S' -o /dev/null . 2>&1 | grep -B2 -A22 'main.callit STEXT' | head -32
Retry indirect call dump
c2.go go.mod main.go wrap.ppc64le 0x0004 00004 (/tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/wrap/c2.go:5) JMP LR 0x0000 00 00 63 e8 20 00 80 4e ..c. ..N main.callit STEXT size=92 align=0x0 args=0x10 locals=0x8 funcid=0x0 0x0000 00000 (/tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/wrap/c2.go:8) TEXT main.callit(SB), ABIInternal, $40-16 0x0000 00000 (/tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/wrap/c2.go:8) MOVD 16(g), R22 0x0004 00004 (/tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/wrap/c2.go:8) PCDATA $0, $-2 0x0004 00004 (/tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/wrap/c2.go:8) CMPU R22, R1 0x0008 00008 (/tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/wrap/c2.go:8) BLT 40 0x000c 00012 (/tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/wrap/c2.go:8) NOP 0x000c 00012 (/tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/wrap/c2.go:8) MOVD R3, 32(R1) 0x0010 00016 (/tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/wrap/c2.go:8) MOVD R4, 40(R1) 0x0014 00020 (/tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/wrap/c2.go:8) MOVD LR, R5 0x0018 00024 (/tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/wrap/c2.go:8) PCDATA $1, $-1 0x0018 00024 (/tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/wrap/c2.go:8) CALL runtime.morestack_noctxt(SB) 0x001c 00028 (/tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/wrap/c2.go:8) PCDATA $0, $-1 0x001c 00028 (/tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/wrap/c2.go:8) MOVD 32(R1), R3 0x0020 00032 (/tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/wrap/c2.go:8) MOVD 40(R1), R4 0x0024 00036 (/tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/wrap/c2.go:8) JMP 0 0x0028 00040 (/tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/wrap/c2.go:8) NOP 0x0028 00040 (/tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/wrap/c2.go:8) MOVD LR, R31 0x002c 00044 (/tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/wrap/c2.go:8) MOVDU R31, -40(R1) 0x0030 00048 (/tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/wrap/c2.go:8) MOVD R3, main.i(R1) 0x0034 00052 (/tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/wrap/c2.go:8) MOVD R4, main.i+8(R1) 0x0038 00056 (/tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/wrap/c2.go:8) FUNCDATA $0, gclocals·Ih7UaEzxol4qYE7mnpMg6w==(SB) 0x0038 00056 (/tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/wrap/c2.go:8) FUNCDATA $1, gclocals·J26BEvPExEQhJvjp9E8Whg==(SB) Shell cwd was reset to /home/claude/golang_go_78576
Bash cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/wrap && GOROOT=/home/cl...
cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/wrap && GOROOT=/home/claude/golang_go_78576/go GOOS=linux GOARCH=ppc64le /home/claude/golang_go_78576/go/bin/go build -gcflags='-S' -o /dev/null . 2>&1 | sed -n '/main.callit STEXT/,/^main\./p' | grep -E "PCDATA\s+\\\$0|CTR|CALL|BL " | head -20
Show PCDATA 0 and CTR sequence in indirect call
0x0004 00004 (/tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/wrap/c2.go:8) PCDATA $0, $-2 0x0018 00024 (/tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/wrap/c2.go:8) CALL runtime.morestack_noctxt(SB) 0x001c 00028 (/tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/wrap/c2.go:8) PCDATA $0, $-1 rel 24+4 t=R_CALLPOWER runtime.morestack_noctxt+0 Shell cwd was reset to /home/claude/golang_go_78576
Bash cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/wrap && GOROOT=/home/cl...
cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/wrap && GOROOT=/home/claude/golang_go_78576/go /home/claude/golang_go_78576/go/bin/go tool objdump -s 'context\.\(\*cancelCtx\)\.Deadline' wrap.ppc64le
Verify the second wrapper shape
TEXT context.(*cancelCtx).Deadline(SB) <autogenerated> <autogenerated>:1 0xaa430 e8830000 MOVD 0(R3),R4 <autogenerated>:1 0xaa434 e8630008 MOVD 8(R3),R3 <autogenerated>:1 0xaa438 e9840018 MOVD 24(R4),R12 <autogenerated>:1 0xaa43c 7d8903a6 MOVD R12,CTR <autogenerated>:1 0xaa440 4e800420 BR (CTR) Shell cwd was reset to /home/claude/golang_go_78576
Bash which qemu-ppc64le qemu-ppc64le-static 2>&1; ls /usr/bin/qemu-* 2>/dev/null | head
which qemu-ppc64le qemu-ppc64le-static 2>&1; ls /usr/bin/qemu-* 2>/dev/null | head
Check for qemu ppc64le
/usr/bin/qemu-ppc64le /usr/bin/qemu-aarch64 /usr/bin/qemu-aarch64_be /usr/bin/qemu-alpha /usr/bin/qemu-amd64 /usr/bin/qemu-arm /usr/bin/qemu-arm64 /usr/bin/qemu-armeb /usr/bin/qemu-armel /usr/bin/qemu-armhf /usr/bin/qemu-cris
qemu-ppc64le is available. Let me build a reproducer: a hot loop whose indirect call through CTR gets async-preempted.
Ran tools (1 Bash)
Bash mkdir -p /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/repro && cd /tmp/...
mkdir -p /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/repro && cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/repro && cat > main.go <<'EOF'
// Reproducer for: ppc64 asyncPreempt clobbers CTR, so a goroutine preempted
// exactly at a "BL/BR (CTR)" indirect branch resumes with CTR pointing at that
// very instruction and spins there forever.
package main
import (
"fmt"
"os"
"runtime"
"sync/atomic"
"time"
)
var counter atomic.Uint64
//go:noinline
func body() { counter.Add(1) }
var fn = body // indirect call: MOVD R12,CTR; BL (CTR)
func spin() {
for {
fn()
}
}
func main() {
runtime.GOMAXPROCS(4)
go spin()
// Generate lots of async preemption signals.
go func() {
for {
runtime.GC()
}
}()
last := counter.Load()
stalled := 0
deadline := time.Now().Add(120 * time.Second)
for time.Now().Before(deadline) {
time.Sleep(200 * time.Millisecond)
cur := counter.Load()
if cur == last {
stalled++
if stalled >= 10 { // 2s with zero progress
fmt.Printf("STUCK: counter frozen at %d\n", cur)
buf := make([]byte, 1<<16)
n := runtime.Stack(buf, true)
os.Stdout.Write(buf[:n])
os.Exit(1)
}
} else {
stalled = 0
}
last = cur
}
fmt.Printf("no stall observed; counter=%d\n", counter.Load())
}
EOF
cat > go.mod <<'EOF'
module repro
go 1.28
EOF
GOROOT=/home/claude/golang_go_78576/go GOOS=linux GOARCH=ppc64le /home/claude/golang_go_78576/go/bin/go build -o repro.ppc64le . && GOROOT=/home/claude/golang_go_78576/go /home/claude/golang_go_78576/go/bin/go tool objdump -s '^main\.spin$' repro.ppc64leBuild ppc64le reproducer and check its CTR sequence
TEXT main.spin(SB) /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/repro/main.go main.go:21 0xae640 eade0010 MOVD 16(R30),R22 main.go:21 0xae644 7c360840 CMPU R22,R1 main.go:21 0xae648 41800010 BLT 0xae658 main.go:21 0xae64c 7ca802a6 MOVD LR,R5 main.go:21 0xae650 4bfe51e1 CALL runtime.morestack_noctxt.abi0(SB) main.go:21 0xae654 4bffffec BR main.spin(SB) main.go:21 0xae658 7fe802a6 MOVD LR,R31 main.go:21 0xae65c fbe1ffe1 MOVDU R31,-32(R1) main.go:23 0xae660 3d600017 ADDIS $0,$23,R11 main.go:23 0xae664 e96b4f20 MOVD 20256(R11),R11 main.go:23 0xae668 e98b0000 MOVD 0(R11),R12 main.go:23 0xae66c 7d8803a6 MOVD R12,LR main.go:23 0xae670 4e800821 BCLRL $20,LT,$1 main.go:23 0xae674 4bffffec BR 0xae660 Shell cwd was reset to /home/claude/golang_go_78576
The compiler uses LR for indirect calls (which asyncPreempt does restore) and CTR only for tail calls — which is exactly why every observed hang is in an <autogenerated> wrapper. Let me build that repro.
Ran tools (2 Bash)
Bash cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/repro && cat > main.go ...
cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/repro && cat > main.go <<'EOF'
// Reproducer: on ppc64/ppc64le, runtime.asyncPreempt does not preserve CTR and
// its resume sequence loads CTR with the resume PC. A goroutine async-preempted
// exactly at the "BR (CTR)" of a tail call (emitted for autogenerated method
// wrappers) therefore resumes with CTR pointing at that very instruction and
// spins on it forever.
package main
import (
"fmt"
"os"
"runtime"
"sync/atomic"
"time"
)
type I interface{ M() int }
type base struct{ x int }
//go:noinline
func (b *base) M() int { return b.x }
// outer embeds the interface, so (*outer).M is an autogenerated wrapper
// compiled to: MOVD 8(R3),R3; MOVD n(R4),R12; MOVD R12,CTR; BR (CTR)
type outer struct{ I }
var v I = &outer{I: &base{x: 1}}
var counter atomic.Uint64
func spin() {
for {
if v.M() != 0 {
counter.Add(1)
}
}
}
func main() {
runtime.GOMAXPROCS(4)
go spin()
go func() {
for {
runtime.GC()
}
}()
last := counter.Load()
stalled := 0
deadline := time.Now().Add(180 * time.Second)
for time.Now().Before(deadline) {
time.Sleep(200 * time.Millisecond)
cur := counter.Load()
if cur == last {
if stalled++; stalled >= 10 {
fmt.Printf("STUCK: counter frozen at %d after %v\n", cur, time.Since(deadline.Add(-180*time.Second)))
buf := make([]byte, 1<<16)
os.Stdout.Write(buf[:runtime.Stack(buf, true)])
os.Exit(1)
}
} else {
stalled = 0
}
last = cur
}
fmt.Printf("no stall observed; counter=%d\n", counter.Load())
}
EOF
GOROOT=/home/claude/golang_go_78576/go GOOS=linux GOARCH=ppc64le /home/claude/golang_go_78576/go/bin/go build -o repro.ppc64le . && GOROOT=/home/claude/golang_go_78576/go /home/claude/golang_go_78576/go/bin/go tool objdump -s 'main\.\(\*outer\)\.M' repro.ppc64leBuild tail-call reproducer
TEXT main.(*outer).M(SB) <autogenerated> <autogenerated>:1 0xaf320 e8830000 MOVD 0(R3),R4 <autogenerated>:1 0xaf324 e8630008 MOVD 8(R3),R3 <autogenerated>:1 0xaf328 e9840018 MOVD 24(R4),R12 <autogenerated>:1 0xaf32c 7d8903a6 MOVD R12,CTR <autogenerated>:1 0xaf330 4e800420 BR (CTR) Shell cwd was reset to /home/claude/golang_go_78576
Bash cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/repro && timeout 200 qe...
cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/repro && timeout 200 qemu-ppc64le ./repro.ppc64le 2>&1 | head -40; echo "exit=$?"
Run reproducer under qemu-ppc64le
240000
no stall observed; counter=5317523504 exit=0 Shell cwd was reset to /home/claude/golang_go_78576
qemu-user only delivers signals at translation-block boundaries, so it can never land on the final BR (CTR). Let me force one-instruction blocks.
Ran tools (3 Bash)
Bash qemu-ppc64le --version | head -2; qemu-ppc64le -h 2>&1 | grep -i "one-insn\|singlestep"
qemu-ppc64le --version | head -2; qemu-ppc64le -h 2>&1 | grep -i "one-insn\|singlestep"
Check qemu single-step options
qemu-ppc64le version 8.2.2 (Debian 1:8.2.2+ds-0ubuntu1.11) Copyright (c) 2003-2023 Fabrice Bellard and the QEMU Project developers -one-insn-per-tb QEMU_ONE_INSN_PER_TB run with one guest instruction per emulated TB -singlestep QEMU_SINGLESTEP deprecated synonym for -one-insn-per-tb
Bash cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/repro && sed -i 's/180 ...
cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/repro && sed -i 's/180 \* time.Second/600 * time.Second/; s/-180\*time.Second/-600*time.Second/' main.go && GOROOT=/home/claude/golang_go_78576/go GOOS=linux GOARCH=ppc64le /home/claude/golang_go_78576/go/bin/go build -o repro.ppc64le . && echo rebuilt
Rebuild with longer deadline
rebuilt Shell cwd was reset to /home/claude/golang_go_78576
Bash cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/repro && timeout 400 qe...
cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/repro && timeout 400 qemu-ppc64le -one-insn-per-tb ./repro.ppc64le > out1.txt 2>&1; echo "exit=$?"; head -45 out1.txt
Run reproducer with one-insn-per-tb
450000
exit=1 STUCK: counter frozen at 550981 after 2.210449072s goroutine 1 [running]: main.main() /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/repro/main.go:58 +0x1c0 goroutine 7 [runnable]: main.(*outer).M(0x0?) <autogenerated>:1 +0x10 main.spin() /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/repro/main.go:33 +0x3c created by main.main in goroutine 1 /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/repro/main.go:41 +0x38 goroutine 8 [runnable]: runtime.Gosched(...) /home/claude/golang_go_78576/go/src/runtime/proc.go:403 runtime.GC() /home/claude/golang_go_78576/go/src/runtime/mgc.go:565 +0x7c main.main.func1() /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/repro/main.go:44 +0x24 created by main.main in goroutine 1 /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/repro/main.go:42 +0x44 Shell cwd was reset to /home/claude/golang_go_78576
Reproduced — goroutine 7 [runnable] frozen at <autogenerated>:1 +0x10, exactly the production signature. Let me get the fatal traceback to match the CI dumps, and survey all CTR uses.
Ran tools (3 Bash)
Bash cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/repro && sed -i 's|os.S...
cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/repro && sed -i 's|os.Stdout.Write(buf\[:runtime.Stack(buf, true)\])\n||' main.go && python3 - <<'EOF'
import re
s=open('main.go').read()
s=s.replace(''' buf := make([]byte, 1<<16)
os.Stdout.Write(buf[:runtime.Stack(buf, true)])
os.Exit(1)''',''' _ = os.Stdout
panic("goroutine stopped making progress")''')
open('main.go','w').write(s)
EOF
GOROOT=/home/claude/golang_go_78576/go GOOS=linux GOARCH=ppc64le /home/claude/golang_go_78576/go/bin/go build -o repro2.ppc64le . && timeout 400 env GOTRACEBACK=system qemu-ppc64le -one-insn-per-tb ./repro2.ppc64le > out2.txt 2>&1; echo exit=$?; grep -n "STUCK\|^goroutine\|asyncPreempt\|autogenerated\|main.spin" out2.txt | head -25Get fatal traceback from reproducer
450000
exit=2 1:STUCK: counter frozen at 291097 after 2.217508333s 4:goroutine 1 gp=0x58c6b39501c0 m=7 mp=0x58c6b3a21008 [running]: 14:goroutine 2 gp=0x58c6b3950700 m=nil [force gc (idle)]: 26:goroutine 3 gp=0x58c6b3950fc0 m=nil [runnable]: 38:goroutine 4 gp=0x58c6b3951180 m=nil [GC scavenge wait]: 54:goroutine 17 gp=0x58c6b3b00000 m=nil [GOMAXPROCS updater (idle)]: 66:goroutine 18 gp=0x58c6b3b001c0 m=nil [finalizer wait]: 76:goroutine 5 gp=0x58c6b3951340 m=nil [runnable]: 77:runtime.asyncPreempt2() 79:runtime.asyncPreempt() 82: <autogenerated>:1 +0x10 fp=0x58c6b39f5f98 sp=0x58c6b39f5f98 pc=0xaede0 83:main.spin() 90:goroutine 6 gp=0x58c6b3951500 m=nil [runnable]: 102:goroutine 33 gp=0x58c6b3b00380 m=nil [GC worker (idle)]: 114:goroutine 34 gp=0x58c6b3b00540 m=nil [GC worker (idle)]: 126:goroutine 35 gp=0x58c6b3b00700 m=nil [GC worker (idle)]: 138:goroutine 36 gp=0x58c6b3b00c40 m=nil [GC worker (idle)]: Shell cwd was reset to /home/claude/golang_go_78576
Bash cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/repro && sed -n '76,90p...
cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/repro && sed -n '76,90p' out2.txt
Show the stuck goroutine stack
goroutine 5 gp=0x58c6b3951340 m=nil [runnable]:
runtime.asyncPreempt2()
/home/claude/golang_go_78576/go/src/runtime/preempt.go:320 +0x14 fp=0x58c6b39f5d70 sp=0x58c6b39f5d48 pc=0x587d4
runtime.asyncPreempt()
/home/claude/golang_go_78576/go/src/runtime/preempt_ppc64x.s:75 +0x10c fp=0x58c6b39f5f78 sp=0x58c6b39f5d70 pc=0x9615c
main.(*outer).M(0x0?)
<autogenerated>:1 +0x10 fp=0x58c6b39f5f98 sp=0x58c6b39f5f98 pc=0xaede0
main.spin()
/tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/repro/main.go:33 +0x3c fp=0x58c6b39f5fc0 sp=0x58c6b39f5f98 pc=0xaeadc
runtime.goexit({})
/home/claude/golang_go_78576/go/src/runtime/asm_ppc64x.s:1057 +0x4 fp=0x58c6b39f5fc0 sp=0x58c6b39f5fc0 pc=0x95894
created by main.main in goroutine 1
/tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/repro/main.go:41 +0x38
goroutine 6 gp=0x58c6b3951500 m=nil [runnable]:
Shell cwd was reset to /home/claude/golang_go_78576Bash cd /home/claude/golang_go_78576/go && grep -rn "REG_CTR" src/cmd/compile/internal/ppc64/*.go src/cmd/internal/obj/ppc64/...
cd /home/claude/golang_go_78576/go && grep -rn "REG_CTR" src/cmd/compile/internal/ppc64/*.go src/cmd/internal/obj/ppc64/obj9.go src/runtime/*_ppc64x.s 2>/dev/null | head -20
Survey CTR uses in generated code
src/cmd/internal/obj/ppc64/obj9.go:972: // Move target address into REG_CTR. src/cmd/internal/obj/ppc64/obj9.go:973: // (Indirect branches can only go to REG_LR or REG_CTR.) src/cmd/internal/obj/ppc64/obj9.go:980: p.To.Reg = REG_CTR src/cmd/internal/obj/ppc64/obj9.go:981: retReg = REG_CTR src/cmd/compile/internal/ppc64/ssa.go:1073: p.To.Reg = ppc64.REG_CTR src/cmd/compile/internal/ppc64/ssa.go:1278: p.To.Reg = ppc64.REG_CTR src/cmd/compile/internal/ppc64/ssa.go:1437: p.To.Reg = ppc64.REG_CTR src/cmd/compile/internal/ppc64/ssa.go:1669: p.To.Reg = ppc64.REG_CTR
Identical to the production dumps, frame for frame. Let me check the other CTR uses — some may be worse than a hang.
Ran tools (2 Bash)
Bash for l in 1073 1278 1437 1669; do echo "===== ssa.go:$l"; sed -n "$((l-16)),$((l+14))p" src/cmd/compile/internal/ppc64/ss...
for l in 1073 1278 1437 1669; do echo "===== ssa.go:$l"; sed -n "$((l-16)),$((l+14))p" src/cmd/compile/internal/ppc64/ssa.go; done 2>/dev/null | head -140
Inspect other CTR uses in the compiler
===== ssa.go:1073
p.From.Reg = ppc64.REG_VS32
p.To.Type = obj.TYPE_REG
p.To.Reg = ppc64.REG_VS32
p.Reg = ppc64.REG_VS32
// Set up CTR loop counter
p = s.Prog(ppc64.AMOVD)
p.From.Type = obj.TYPE_CONST
p.From.Offset = ctr
p.To.Type = obj.TYPE_REG
p.To.Reg = ppc64.REGTMP
p = s.Prog(ppc64.AMOVD)
p.From.Type = obj.TYPE_REG
p.From.Reg = ppc64.REGTMP
p.To.Type = obj.TYPE_REG
p.To.Reg = ppc64.REG_CTR
// Don't generate padding for
// loops with few iterations.
if ctr > 3 {
p = s.Prog(obj.APCALIGN)
p.From.Type = obj.TYPE_CONST
p.From.Offset = 16
}
// generate 4 STXVs to zero 64 bytes
var top *obj.Prog
p = s.Prog(ppc64.ASTXV)
p.From.Type = obj.TYPE_REG
===== ssa.go:1278
p.From.Reg = ppc64.REG_VS32
p.To.Type = obj.TYPE_REG
p.To.Reg = ppc64.REG_VS32
p.Reg = ppc64.REG_VS32
// Set up CTR loop counter
p = s.Prog(ppc64.AMOVD)
p.From.Type = obj.TYPE_CONST
p.From.Offset = ctr
p.To.Type = obj.TYPE_REG
p.To.Reg = ppc64.REGTMP
p = s.Prog(ppc64.AMOVD)
p.From.Type = obj.TYPE_REG
p.From.Reg = ppc64.REGTMP
p.To.Type = obj.TYPE_REG
p.To.Reg = ppc64.REG_CTR
// Set up R31 to hold index value 16
p = s.Prog(ppc64.AMOVD)
p.From.Type = obj.TYPE_CONST
p.From.Offset = 16
p.To.Type = obj.TYPE_REG
p.To.Reg = ppc64.REGTMP
// Don't add padding for alignment
// with few loop iterations.
if ctr > 3 {
p = s.Prog(obj.APCALIGN)
p.From.Type = obj.TYPE_CONST
p.From.Offset = 16
===== ssa.go:1437
// top of the loop
var top *obj.Prog
// Only generate looping code when loop counter is > 1 for >= 64 bytes
if ctr > 1 {
// Set up the CTR
p := s.Prog(ppc64.AMOVD)
p.From.Type = obj.TYPE_CONST
p.From.Offset = ctr
p.To.Type = obj.TYPE_REG
p.To.Reg = ppc64.REGTMP
p = s.Prog(ppc64.AMOVD)
p.From.Type = obj.TYPE_REG
p.From.Reg = ppc64.REGTMP
p.To.Type = obj.TYPE_REG
p.To.Reg = ppc64.REG_CTR
// Use REGTMP as index reg
p = s.Prog(ppc64.AMOVD)
p.From.Type = obj.TYPE_CONST
p.From.Offset = 16
p.To.Type = obj.TYPE_REG
p.To.Reg = ppc64.REGTMP
// Don't adding padding for
// alignment with small iteration
// counts.
if ctr > 3 {
p = s.Prog(obj.APCALIGN)
p.From.Type = obj.TYPE_CONST
===== ssa.go:1669
// top of the loop
var top *obj.Prog
// Only generate looping code when loop counter is > 1 for >= 64 bytes
if ctr > 1 {
// Set up the CTR
p := s.Prog(ppc64.AMOVD)
p.From.Type = obj.TYPE_CONST
p.From.Offset = ctr
p.To.Type = obj.TYPE_REG
p.To.Reg = ppc64.REGTMP
p = s.Prog(ppc64.AMOVD)
p.From.Type = obj.TYPE_REG
p.From.Reg = ppc64.REGTMP
p.To.Type = obj.TYPE_REG
p.To.Reg = ppc64.REG_CTR
p = s.Prog(obj.APCALIGN)
p.From.Type = obj.TYPE_CONST
p.From.Offset = 16
// Generate 16 byte loads and stores.
p = s.Prog(ppc64.ALXV)
p.From.Type = obj.TYPE_MEM
p.From.Reg = srcReg
p.From.Offset = offset
p.To.Type = obj.TYPE_REG
p.To.Reg = ppc64.REG_VS32
if top == nil {
top = pBash grep -n "LoweredZero\|LoweredMove\|LoweredQuadZero\|LoweredQuadMove\|unsafePoint" src/cmd/compile/internal/ssa/_gen/PPC6...
grep -n "LoweredZero\|LoweredMove\|LoweredQuadZero\|LoweredQuadMove\|unsafePoint" src/cmd/compile/internal/ssa/_gen/PPC64Ops.go | head -30
Check unsafePoint marking on CTR loops
502: name: "LoweredZero", 512: unsafePoint: true, 516: name: "LoweredZeroShort", 523: unsafePoint: true, 527: name: "LoweredQuadZeroShort", 535: unsafePoint: true, 539: name: "LoweredQuadZero", 549: unsafePoint: true, 587: name: "LoweredMove", 598: unsafePoint: true, 603: name: "LoweredMoveShort", 612: unsafePoint: true, 617: // The following is similar to the LoweredMove, but uses 621: name: "LoweredQuadMove", 632: unsafePoint: true, 638: name: "LoweredQuadMoveShort", 647: unsafePoint: true,
All the CTR-counted memmove/memclr loops are already marked unsafePoint: true — the tail-call lowering is the one place that was missed. Let me write and verify a fix.
Ran tools (2 Bash)
Bash sed -n '960,1055p' src/cmd/internal/obj/ppc64/obj9.go
sed -n '960,1055p' src/cmd/internal/obj/ppc64/obj9.go
Read full ARET lowering
}
case obj.ARET:
if p.From.Type == obj.TYPE_CONST {
c.ctxt.Diag("using BECOME (%v) is not supported!", p)
break
}
retTarget, retReg := p.To.Sym, p.To.Reg
if retReg == obj.REG_NONE {
retReg = REG_LR
} else {
// Move target address into REG_CTR.
// (Indirect branches can only go to REG_LR or REG_CTR.)
x := newprog()
*x = *p
p.As = AMOVD
p.From.Type = obj.TYPE_REG
p.From.Reg = retReg
p.To.Type = obj.TYPE_REG
p.To.Reg = REG_CTR
retReg = REG_CTR
p.Link = x
p = x
}
if c.cursym.Func().Text.Mark&LEAF != 0 {
if autosize == 0 {
p.As = ABR
p.From = obj.Addr{}
if retTarget == nil {
p.To.Type = obj.TYPE_REG
p.To.Reg = retReg
} else {
p.To.Type = obj.TYPE_BRANCH
p.To.Sym = retTarget
}
p.Mark |= BRANCH
break
}
p.As = AADD
p.From.Type = obj.TYPE_CONST
p.From.Offset = int64(autosize)
p.To.Type = obj.TYPE_REG
p.To.Reg = REGSP
p.Spadj = -autosize
q = c.newprog()
q.As = ABR
q.Pos = p.Pos
if retTarget == nil {
q.To.Type = obj.TYPE_REG
q.To.Reg = retReg
} else {
q.To.Type = obj.TYPE_BRANCH
q.To.Sym = retTarget
}
q.Mark |= BRANCH
q.Spadj = +autosize
q.Link = p.Link
p.Link = q
break
}
p.As = AMOVD
p.From.Type = obj.TYPE_MEM
p.From.Offset = 0
p.From.Reg = REGSP
p.To.Type = obj.TYPE_REG
p.To.Reg = REGTMP
q = c.newprog()
q.As = AMOVD
q.Pos = p.Pos
q.From.Type = obj.TYPE_REG
q.From.Reg = REGTMP
q.To.Type = obj.TYPE_REG
q.To.Reg = REG_LR
q.Link = p.Link
p.Link = q
p = q
if false {
// Debug bad returns
q = c.newprog()
q.As = AMOVD
q.Pos = p.Pos
q.From.Type = obj.TYPE_MEM
q.From.Offset = 0
q.From.Reg = REGTMP
q.To.Type = obj.TYPE_REG
q.To.Reg = REGTMPBash sed -n '1055,1110p' src/cmd/internal/obj/ppc64/obj9.go
sed -n '1055,1110p' src/cmd/internal/obj/ppc64/obj9.go
Read rest of ARET non-leaf path
q.To.Reg = REGTMP
q.Link = p.Link
p.Link = q
p = q
}
prev := p
if autosize != 0 {
q = c.newprog()
q.As = AADD
q.Pos = p.Pos
q.From.Type = obj.TYPE_CONST
q.From.Offset = int64(autosize)
q.To.Type = obj.TYPE_REG
q.To.Reg = REGSP
q.Spadj = -autosize
q.Link = p.Link
prev.Link = q
prev = q
}
q1 = c.newprog()
q1.As = ABR
q1.Pos = p.Pos
if retTarget == nil {
q1.To.Type = obj.TYPE_REG
q1.To.Reg = retReg
} else {
q1.To.Type = obj.TYPE_BRANCH
q1.To.Sym = retTarget
}
q1.Mark |= BRANCH
q1.Spadj = +autosize
q1.Link = q.Link
prev.Link = q1
case AADD:
if p.To.Type == obj.TYPE_REG && p.To.Reg == REGSP && p.From.Type == obj.TYPE_CONST {
p.Spadj = int32(-p.From.Offset)
}
case AMOVDU:
if p.To.Type == obj.TYPE_MEM && p.To.Reg == REGSP {
p.Spadj = int32(-p.To.Offset)
}
if p.From.Type == obj.TYPE_MEM && p.From.Reg == REGSP {
p.Spadj = int32(-p.From.Offset)
}
case obj.AGETCALLERPC:
if cursym.Leaf() {
/* MOVD LR, Rd */
p.As = AMOVD
p.From.Type = obj.TYPE_REG
p.From.Reg = REG_LR
} else {
/* MOVD (RSP), Rd */Now the fix. The dangerous region is from just after MOVD reg, CTR through the BR (CTR).
Ran tools (3 Bash)
Bash cp src/cmd/internal/obj/ppc64/obj9.go /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172...
cp src/cmd/internal/obj/ppc64/obj9.go /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/obj9.go.orig && python3 - <<'PY'
p='src/cmd/internal/obj/ppc64/obj9.go'
s=open(p).read()
old = """ retTarget, retReg := p.To.Sym, p.To.Reg
if retReg == obj.REG_NONE {
retReg = REG_LR
} else {
// Move target address into REG_CTR.
// (Indirect branches can only go to REG_LR or REG_CTR.)
x := newprog()
*x = *p
p.As = AMOVD
p.From.Type = obj.TYPE_REG
p.From.Reg = retReg
p.To.Type = obj.TYPE_REG
p.To.Reg = REG_CTR
retReg = REG_CTR
p.Link = x
p = x
}
"""
new = """ retTarget, retReg := p.To.Sym, p.To.Reg
usesCTR := false
if retReg == obj.REG_NONE {
retReg = REG_LR
} else {
// Move target address into REG_CTR.
// (Indirect branches can only go to REG_LR or REG_CTR.)
x := newprog()
*x = *p
p.As = AMOVD
p.From.Type = obj.TYPE_REG
p.From.Reg = retReg
p.To.Type = obj.TYPE_REG
p.To.Reg = REG_CTR
retReg = REG_CTR
p.Link = x
// Everything from here through the BR (CTR) must be an
// unsafe point. runtime.asyncPreempt does not preserve CTR,
// and its resume sequence leaves CTR holding the resume PC,
// so a goroutine preempted at the BR (CTR) would resume by
// branching to that very instruction and spin there forever.
c.ctxt.StartUnsafePoint(p, c.newprog)
usesCTR = true
p = x
}
endCTRUnsafePoint := func(br *obj.Prog) {
if usesCTR {
c.ctxt.EndUnsafePoint(br, c.newprog, -1)
}
}
"""
assert s.count(old)==1
s=s.replace(old,new)
# 1. LEAF, autosize == 0: p becomes the ABR.
old1 = """ p.Mark |= BRANCH
break
}
"""
new1 = """ p.Mark |= BRANCH
endCTRUnsafePoint(p)
break
}
"""
assert s.count(old1)==1
s=s.replace(old1,new1)
# 2. LEAF, autosize != 0: q is the ABR.
old2 = """ q.Mark |= BRANCH
q.Spadj = +autosize
q.Link = p.Link
p.Link = q
break
}
"""
new2 = """ q.Mark |= BRANCH
q.Spadj = +autosize
q.Link = p.Link
p.Link = q
endCTRUnsafePoint(q)
break
}
"""
assert s.count(old2)==1
s=s.replace(old2,new2)
# 3. Non-leaf: q1 is the ABR.
old3 = """ q1.Link = q.Link
prev.Link = q1
"""
new3 = """ q1.Link = q.Link
prev.Link = q1
endCTRUnsafePoint(q1)
"""
assert s.count(old3)==1
s=s.replace(old3,new3)
open(p,'w').write(s)
print("patched")
PY
gofmt -l src/cmd/internal/obj/ppc64/obj9.go; echo "gofmt ok"Patch ppc64 tail-call lowering
patched gofmt ok
Bash cd /home/claude/golang_go_78576/go/src && GOROOT=/home/claude/golang_go_78576/go ../bin/go install cmd/compile 2>&1 | ta...
cd /home/claude/golang_go_78576/go/src && GOROOT=/home/claude/golang_go_78576/go ../bin/go install cmd/compile 2>&1 | tail -5 && echo "compiler rebuilt" && cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/repro && GOROOT=/home/claude/golang_go_78576/go GOOS=linux GOARCH=ppc64le /home/claude/golang_go_78576/go/bin/go build -a -o repro3.ppc64le . && GOROOT=/home/claude/golang_go_78576/go /home/claude/golang_go_78576/go/bin/go tool objdump -s 'main\.\(\*outer\)\.M' repro3.ppc64le
Rebuild compiler and reproducer with the fix
600000
compiler rebuilt TEXT main.(*outer).M(SB) <autogenerated> <autogenerated>:1 0xaedd0 e8830000 MOVD 0(R3),R4 <autogenerated>:1 0xaedd4 e8630008 MOVD 8(R3),R3 <autogenerated>:1 0xaedd8 e9840018 MOVD 24(R4),R12 <autogenerated>:1 0xaeddc 7d8903a6 MOVD R12,CTR <autogenerated>:1 0xaede0 4e800420 BR (CTR) Shell cwd was reset to /home/claude/golang_go_78576
Bash cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/repro && GOROOT=/home/c...
cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/repro && GOROOT=/home/claude/golang_go_78576/go GOOS=linux GOARCH=ppc64le /home/claude/golang_go_78576/go/bin/go build -a -gcflags='-S' -o /dev/null . 2>&1 | grep -A12 'main\.(\*outer)\.M(SB), DUPOK' | head -14
Verify unsafe-point PCDATA now emitted
0x0000 00000 (<autogenerated>:1) TEXT main.(*outer).M(SB), DUPOK|NOSPLIT|LEAF|NOFRAME|ABIInternal, $0-8 0x0000 00000 (<autogenerated>:1) FUNCDATA $0, gclocals·wvjpxkknJ4nY1JtrArJJaw==(SB) 0x0000 00000 (<autogenerated>:1) FUNCDATA $1, gclocals·J26BEvPExEQhJvjp9E8Whg==(SB) 0x0000 00000 (<autogenerated>:1) FUNCDATA $5, main.(*outer).M.arginfo1(SB) 0x0000 00000 (<autogenerated>:1) FUNCDATA $6, main.(*outer).M.argliveinfo(SB) 0x0000 00000 (<autogenerated>:1) PCDATA $3, $1 0x0000 00000 (<autogenerated>:1) MOVD (R3), R4 0x0004 00004 (<autogenerated>:1) MOVD 8(R3), R3 0x0008 00008 (<autogenerated>:1) MOVD 24(R4), R12 0x000c 00012 (<autogenerated>:1) PCDATA $1, $0 0x000c 00012 (<autogenerated>:1) MOVD R12, CTR 0x0010 00016 (<autogenerated>:1) PCDATA $0, $-2 0x0010 00016 (<autogenerated>:1) JMP CTR Shell cwd was reset to /home/claude/golang_go_78576
PCDATA $0, $-2 now covers the JMP CTR. Running the reproducer against the fixed toolchain.
Ran tools (1 Bash)
Bash cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/repro && timeout 700 en...
cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/repro && timeout 700 env GOTRACEBACK=system qemu-ppc64le -one-insn-per-tb ./repro3.ppc64le > out3.txt 2>&1; echo "exit=$?"; head -5 out3.txt
Run fixed reproducer (10 min)
720000
Command did not complete within its 600s timeout and was moved to the background (ID: bl60yurp3). Output is being written to: /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/tasks/bl60yurp3.output. You will be notified when it completes. To check interim output, use Read on that file path. Session cwd remains /home/claude/golang_go_78576; directory changes made by the backgrounded command do not apply to subsequent commands.
Already past 600s without stalling — the unpatched build hung within seconds. Let me quantify the baseline while it finishes.
Ran tools (2 Bash)
Bash cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/repro && for i in 1 2 3...
cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/repro && for i in 1 2 3; do s=$(date +%s); timeout 150 qemu-ppc64le -one-insn-per-tb ./repro2.ppc64le 2>&1 | head -1; e=$(date +%s); echo " run $i: $((e-s))s"; done
Baseline: how fast unpatched build hangs
500000
STUCK: counter frozen at 231022 after 2.205639573s run 1: 3s STUCK: counter frozen at 149497 after 2.204770912s run 2: 2s STUCK: counter frozen at 364143 after 2.206116194s run 3: 2s Shell cwd was reset to /home/claude/golang_go_78576
Bash cat /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/tasks/bl60yurp3.output
cat /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/tasks/bl60yurp3.output
Check fixed-build result
exit=0 no stall observed; counter=3281372435 [exited with code 0]
Fix verified: unpatched hangs in 2–3s (3/3 runs); patched ran 600s clean (3.3B iterations). Let me sanity-check the patch doesn't break the toolchain.
Ran tools (4 Bash, 1 SendUserFile)
Bash cd /home/claude/golang_go_78576/go/src && export GOROOT=/home/claude/golang_go_78576/go && for a in ppc64le ppc64; do ec...
cd /home/claude/golang_go_78576/go/src && export GOROOT=/home/claude/golang_go_78576/go && for a in ppc64le ppc64; do echo "--- GOARCH=$a build std cmd"; GOOS=linux GOARCH=$a ../bin/go build -a std cmd 2>&1 | tail -3 && echo " OK"; done && echo "--- obj/ppc64 tests" && ../bin/go test cmd/internal/obj/ppc64 2>&1 | tail -3
Cross-build std+cmd for ppc64 and run obj tests
900000
--- GOARCH=ppc64le build std cmd OK --- GOARCH=ppc64 build std cmd OK --- obj/ppc64 tests ok cmd/internal/obj/ppc64 0.711s
Bash export GOROOT=/home/claude/golang_go_78576/go && ../bin/go test cmd/compile/internal/ssagen cmd/internal/obj/... 2>&1 | ...
export GOROOT=/home/claude/golang_go_78576/go && ../bin/go test cmd/compile/internal/ssagen cmd/internal/obj/... 2>&1 | tail -12; echo "=== ppc64le std vet/asm sanity: build a non-leaf tail call ==="; cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/repro && GOOS=linux GOARCH=ppc64le /home/claude/golang_go_78576/go/bin/go build -gcflags='-S' -o /dev/null . 2>&1 | grep -B1 -A3 'PCDATA \$0, \$-2' | grep -A3 -B1 'CTR' | head -20
Run compiler/obj tests
900000
ok cmd/compile/internal/ssagen 0.004s ok cmd/internal/obj 0.202s ? cmd/internal/obj/arm [no test files] ok cmd/internal/obj/arm64 1.001s ok cmd/internal/obj/loong64 2.130s ? cmd/internal/obj/mips [no test files] ok cmd/internal/obj/ppc64 (cached) ok cmd/internal/obj/riscv 4.684s ok cmd/internal/obj/s390x 0.002s ? cmd/internal/obj/wasm [no test files] ok cmd/internal/obj/x86 0.731s === ppc64le std vet/asm sanity: build a non-leaf tail call === -- 0x000c 00012 (<autogenerated>:1) MOVD R12, CTR 0x0010 00016 (<autogenerated>:1) PCDATA $0, $-2 0x0010 00016 (<autogenerated>:1) JMP CTR 0x0014 00020 (<autogenerated>:1) PCDATA $0, $-1 0x0000 00 00 83 e8 08 00 63 e8 18 00 84 e9 a6 03 89 7d ......c........} Shell cwd was reset to /home/claude/golang_go_78576
Bash cd /home/claude/golang_go_78576/go && git diff --stat src/cmd/internal/obj/ppc64/obj9.go && git diff src/cmd/internal/ob...
cd /home/claude/golang_go_78576/go && git diff --stat src/cmd/internal/obj/ppc64/obj9.go && git diff src/cmd/internal/obj/ppc64/obj9.go
Show the patch
src/cmd/internal/obj/ppc64/obj9.go | 16 ++++++++++++++++
1 file changed, 16 insertions(+)
diff --git a/src/cmd/internal/obj/ppc64/obj9.go b/src/cmd/internal/obj/ppc64/obj9.go
index 323420fefa..6d0832c2d0 100644
--- a/src/cmd/internal/obj/ppc64/obj9.go
+++ b/src/cmd/internal/obj/ppc64/obj9.go
@@ -966,6 +966,7 @@ func preprocess(ctxt *obj.Link, cursym *obj.LSym, newprog obj.ProgAlloc) {
}
retTarget, retReg := p.To.Sym, p.To.Reg
+ usesCTR := false
if retReg == obj.REG_NONE {
retReg = REG_LR
} else {
@@ -980,8 +981,20 @@ func preprocess(ctxt *obj.Link, cursym *obj.LSym, newprog obj.ProgAlloc) {
p.To.Reg = REG_CTR
retReg = REG_CTR
p.Link = x
+ // Everything from here through the BR (CTR) must be an
+ // unsafe point. runtime.asyncPreempt does not preserve CTR,
+ // and its resume sequence leaves CTR holding the resume PC,
+ // so a goroutine preempted at the BR (CTR) would resume by
+ // branching to that very instruction and spin there forever.
+ c.ctxt.StartUnsafePoint(p, c.newprog)
+ usesCTR = true
p = x
}
+ endCTRUnsafePoint := func(br *obj.Prog) {
+ if usesCTR {
+ c.ctxt.EndUnsafePoint(br, c.newprog, -1)
+ }
+ }
if c.cursym.Func().Text.Mark&LEAF != 0 {
if autosize == 0 {
@@ -995,6 +1008,7 @@ func preprocess(ctxt *obj.Link, cursym *obj.LSym, newprog obj.ProgAlloc) {
p.To.Sym = retTarget
}
p.Mark |= BRANCH
+ endCTRUnsafePoint(p)
break
}
@@ -1020,6 +1034,7 @@ func preprocess(ctxt *obj.Link, cursym *obj.LSym, newprog obj.ProgAlloc) {
q.Link = p.Link
p.Link = q
+ endCTRUnsafePoint(q)
break
}
@@ -1089,6 +1104,7 @@ func preprocess(ctxt *obj.Link, cursym *obj.LSym, newprog obj.ProgAlloc) {
q1.Link = q.Link
prev.Link = q1
+ endCTRUnsafePoint(q1)
case AADD:
if p.To.Type == obj.TYPE_REG && p.To.Reg == REGSP && p.From.Type == obj.TYPE_CONST {
p.Spadj = int32(-p.From.Offset)Bash cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/repro && cat > main.go ...
cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/repro && cat > main.go <<'EOF'
// Reproducer for go.dev/issue/78576.
//
// On ppc64/ppc64le the assembler lowers a tail call (RET (reg), emitted for
// compiler-generated method wrappers) to:
//
// MOVD reg, CTR
// BR (CTR)
//
// runtime.asyncPreempt does not save/restore CTR, and its resume sequence ends
// with "MOVD <resumePC>, CTR; JMP (CTR)". So a goroutine async-preempted
// exactly at the BR (CTR) resumes with CTR pointing at that very instruction
// and spins on it forever, making no further progress.
//
// Run with:
// GOARCH=ppc64le go build -o repro .
// qemu-ppc64le -one-insn-per-tb ./repro
//
// (-one-insn-per-tb is needed because qemu-user only delivers signals at
// translation-block boundaries, and BR (CTR) always ends a block.)
package main
import (
"fmt"
"runtime"
"sync/atomic"
"time"
)
type I interface{ M() int }
type base struct{ x int }
//go:noinline
func (b *base) M() int { return b.x }
// outer embeds the interface, so (*outer).M is an autogenerated wrapper that
// tail-calls (*base).M through CTR.
type outer struct{ I }
var v I = &outer{I: &base{x: 1}}
var counter atomic.Uint64
func spin() {
for {
if v.M() != 0 {
counter.Add(1)
}
}
}
func main() {
runtime.GOMAXPROCS(4)
go spin()
go func() { // generate async preemption signals
for {
runtime.GC()
}
}()
last, stalled := counter.Load(), 0
for deadline := time.Now().Add(600 * time.Second); time.Now().Before(deadline); {
time.Sleep(200 * time.Millisecond)
cur := counter.Load()
if cur == last {
if stalled++; stalled >= 10 { // 2s with zero progress
panic(fmt.Sprintf("goroutine stopped making progress at %d", cur))
}
} else {
stalled = 0
}
last = cur
}
fmt.Printf("no stall observed; counter=%d\n", counter.Load())
}
EOF
GOROOT=/home/claude/golang_go_78576/go GOOS=linux GOARCH=ppc64le /home/claude/golang_go_78576/go/bin/go vet . && echo "vet ok" && lsFinalize reproducer file
vet ok go.mod main.go out1.txt out2.txt out3.txt repro2.ppc64le repro3.ppc64le repro.ppc64le Shell cwd was reset to /home/claude/golang_go_78576
SendUserFile
Reproducer: hangs in 2-3s under `qemu-ppc64le -one-insn-per-tb`, clean for 10min with the obj9.go fix.
attach
[ "/tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/repro/main.go" ]
normal
1 file delivered to user. /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/repro/main.go → file_uuid: 53b31424-09cc-4784-b488-28a663339b6a
Triage: #78576 is not a net/http bug
It's a ppc64 async-preemption bug in cmd/internal/obj/ppc64. net/http is a bystander.
What the log actually shows
The watchflakes excerpt is empty because the test never failed — it hung. Pulling the full dist.testjson (105MB) from the build:
FAIL net/http 1080.019s→panic: test timed out after 18m0s,running tests: TestErrorContentLength/h1 (17m58s)- All 1526 other tests finished by 11:53:05, 8s into the run. The process then emitted zero output for 17m52s. Only 17 goroutines remain.
- The dial goroutine is
[runnable]— not blocked — stopped atasyncPreempt2, inside a frameless autogenerated wrapper at<autogenerated>:1 +0x10,sp == fp.
Root cause
obj9.go lowers RET (reg) (a tail call, emitted for compiler-generated method wrappers) to MOVD reg, CTR; BR (CTR). runtime.asyncPreempt on ppc64 saves R3–R29, R31, LR, R2, CR, XER, F0–F31, FPSCR — not CTR — and its resume tail is:
MOVD (R1), R31 // R31 = resumePC
MOVD R31, CTR // CTR = resumePC
JMP (CTR)
So a goroutine preempted exactly at the BR (CTR) resumes with CTR pointing at that instruction itself → a one-instruction infinite loop. sysmon re-preempts it every 10ms and it re-enters the same loop forever. freezetheworld at the watchdog panic catches it in _Grunnable — which is exactly the stack in the dump.
The compiler uses LR for ordinary indirect calls (MOVD R12,LR; BCLRL), and LR is restored — which is why only tail calls are exposed, and why every occurrence is in an <autogenerated> wrapper. The CTR-counted LoweredZero/LoweredMove loops are already unsafePoint: true; the tail call is the one site that was missed, because it's inserted in the assembler backend after SSA-level marking.
Duplicates
Same builder, same fingerprint (asyncPreempt2 → preempt_ppc64x.s:75 +0x10c → <autogenerated>:1 +0x10), five for five — all context wrappers reached via the dial path:
| Issue | Test | Wrapper |
|---|---|---|
| #78199 | TestServerContentType/h1 | (*cancelCtx).Deadline |
| #78576 | TestErrorContentLength/h1 | (*cancelCtx).Deadline |
| #78821 | TestTransportRespectRequestWantsClose/h1/… | (*valueCtx).Done |
| #78897 | TestTransportRespectRequestWantsClose/h1/… | (*valueCtx).Done |
| #78915 | TestServerContext_ServerContextKey/h2 | (*valueCtx).Done |
Reproduced
qemu-user only delivers signals at translation-block boundaries and BR (CTR) always ends a block, so -one-insn-per-tb is required. With it, the attached program hangs in 2–3s, 3/3 runs, producing the identical traceback:
goroutine 5 ... [runnable]:
runtime.asyncPreempt2()
preempt.go:320 +0x14
runtime.asyncPreempt()
preempt_ppc64x.s:75 +0x10c
main.(*outer).M(0x0?)
<autogenerated>:1 +0x10 fp=0x58c6b39f5f98 sp=0x58c6b39f5f98
Fix (applied to your tree)
src/cmd/internal/obj/ppc64/obj9.go, +16 lines: mark from just after MOVD reg,CTR through the BR (CTR) as an unsafe point, covering all three RET-expansion paths (leaf/no-frame, leaf/frame, non-leaf). Emits PCDATA $0, $-2 over exactly the one branch instruction. Saving CTR in asyncPreempt instead isn't workable — the resume has to branch through LR or CTR, and LR holds live interrupted state.
Verified: reproducer clean for 600s / 3.3B iterations; std+cmd cross-build for ppc64le and ppc64; cmd/internal/obj/... and cmd/compile/internal/ssagen tests pass.
Original file saved at scratchpad/obj9.go.orig if you want to revert. I did not run all.bash or test on real ppc64le hardware — worth doing before mailing, and the CL should probably move #78199/#78821/#78897/#78915 under a new runtime/cmd/compile issue rather than leaving them as net/http flakes.
You Tue 25 Aug 2026 14:44:12
What kind of test can we write for this? Are there existing tests for which instruction sequences are marked async-preempt-unsafe?
Assistant Tue 25 Aug 2026 14:44:57
Good question — let me survey what precedent exists.
Ran tools (2 Bash)
Bash cd /home/claude/golang_go_78576/go/src && grep -rln "UnsafePoint" --include="*_test.go" . ; echo "--- asyncPreempt in te...
cd /home/claude/golang_go_78576/go/src && grep -rln "UnsafePoint" --include="*_test.go" . ; echo "--- asyncPreempt in tests ---"; grep -rln "asyncPreempt\|AsyncPreempt\|asyncpreempt" --include="*_test.go" . ; echo "--- testdata progs ---"; grep -rln "asyncPreempt\|AsyncPreempt" runtime/testdata/ 2>/dev/null
Find existing unsafe-point / async-preempt tests
runtime/export_debug_test.go runtime/time_test.go runtime/export_test.go runtime/debug_test.go runtime/unsafepoint_test.go cmd/compile/internal/types2/check_test.go cmd/api/main_test.go text/template/exec_test.go go/types/check_test.go internal/fmtsort/sort_test.go reflect/nih_test.go reflect/set_test.go reflect/all_test.go --- asyncPreempt in tests --- runtime/goroutineleakprofile_test.go runtime/proc_test.go runtime/crash_unix_test.go runtime/traceback_system_test.go internal/trace/trace_test.go --- testdata progs --- runtime/testdata/testprog/preempt.go
Bash ls cmd/internal/obj/ppc64/*_test.go cmd/internal/obj/*_test.go runtime/mkpreempt*.go 2>/dev/null; echo "--- maymorestack...
ls cmd/internal/obj/ppc64/*_test.go cmd/internal/obj/*_test.go runtime/mkpreempt*.go 2>/dev/null; echo "--- maymorestack ---"; grep -rn "maymorestack\|mayMoreStackPreempt" --include="*.go" cmd/compile/internal/base/ runtime/ | head
List obj tests and maymorestack machinery
cmd/internal/obj/line_test.go
cmd/internal/obj/objfile_test.go
cmd/internal/obj/ppc64/asm_test.go
cmd/internal/obj/sizeof_test.go
runtime/mkpreempt.go
--- maymorestack ---
cmd/compile/internal/base/flag.go:310: Ctxt.Flag_maymorestack = Debug.MayMoreStack
runtime/crash_unix_test.go:79: if strings.Contains(os.Getenv("GOFLAGS"), "mayMoreStackPreempt") {
runtime/debug.go:188:// mayMoreStackPreempt is a maymorestack hook that forces a preemption
runtime/debug.go:195:// X=(-{gc,asm}flags={runtime/...,reflect,sync}=-d=maymorestack=runtime.mayMoreStackPreempt) GOFLAGS=${X[@]}
runtime/debug.go:205:// Ensure mayMoreStackPreempt can be called for all ABIs.
runtime/debug.go:208://go:linkname mayMoreStackPreempt
runtime/debug.go:209:func mayMoreStackPreempt() {
runtime/debug.go:221:// mayMoreStackMove is a maymorestack hook that forces stack movement
runtime/debug.go:224:// See mayMoreStackPreempt.
runtime/goroutineleakprofile_test.go:17: // Some tests have false negatives under mayMoreStackPreempt and mayMoreStackMove.runtime/unsafepoint_test.go exists — that's the direct precedent.
Ran tools (6 Bash)
Bash wc -l runtime/unsafepoint_test.go && cat runtime/unsafepoint_test.go
wc -l runtime/unsafepoint_test.go && cat runtime/unsafepoint_test.go
Read runtime/unsafepoint_test.go
122 runtime/unsafepoint_test.go
// Copyright 2023 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 runtime_test
import (
"internal/testenv"
"os"
"os/exec"
"reflect"
"runtime"
"strconv"
"strings"
"testing"
)
// This is the function we'll be testing.
// It has a simple write barrier in it.
func setGlobalPointer() {
globalPointer = nil
}
var globalPointer *int
func TestUnsafePoint(t *testing.T) {
testenv.MustHaveExec(t)
switch runtime.GOARCH {
case "amd64", "arm64":
default:
t.Skipf("test not enabled for %s", runtime.GOARCH)
}
// Get a reference we can use to ask the runtime about
// which of its instructions are unsafe preemption points.
f := runtime.FuncForPC(reflect.ValueOf(setGlobalPointer).Pointer())
// Disassemble the test function.
// Note that normally "go test runtime" would strip symbols
// and prevent this step from working. So there's a hack in
// cmd/go/internal/test that exempts runtime tests from
// symbol stripping.
cmd := exec.Command(testenv.GoToolPath(t), "tool", "objdump", "-s", "setGlobalPointer", os.Args[0])
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("can't objdump %v:\n%s", err, out)
}
lines := strings.Split(string(out), "\n")[1:]
// Walk through assembly instructions, checking preemptible flags.
var entry uint64
var startedWB bool
var doneWB bool
instructionCount := 0
unsafeCount := 0
for _, line := range lines {
line = strings.TrimSpace(line)
t.Logf("%s", line)
parts := strings.Fields(line)
if len(parts) < 4 {
continue
}
if !strings.HasPrefix(parts[0], "unsafepoint_test.go:") {
continue
}
pc, err := strconv.ParseUint(parts[1][2:], 16, 64)
if err != nil {
t.Fatalf("can't parse pc %s: %v", parts[1], err)
}
if entry == 0 {
entry = pc
}
// Note that some platforms do ASLR, so the PCs in the disassembly
// don't match PCs in the address space. Only offsets from function
// entry make sense.
unsafe := runtime.UnsafePoint(f.Entry() + uintptr(pc-entry))
t.Logf("unsafe: %v\n", unsafe)
instructionCount++
if unsafe {
unsafeCount++
}
// All the instructions inside the write barrier must be unpreemptible.
if startedWB && !doneWB && !unsafe {
t.Errorf("instruction %s must be marked unsafe, but isn't", parts[1])
}
// Detect whether we're in the write barrier.
switch runtime.GOARCH {
case "arm64":
if parts[3] == "MOVWU" {
// The unpreemptible region starts after the
// load of runtime.writeBarrier.
startedWB = true
}
if parts[3] == "MOVD" && parts[4] == "ZR," {
// The unpreemptible region ends after the
// write of nil.
doneWB = true
}
case "amd64":
if parts[3] == "CMPL" {
startedWB = true
}
if parts[3] == "MOVQ" && (parts[4] == "$0x0," || parts[4] == "X15,") {
doneWB = true
}
}
}
if instructionCount == 0 {
t.Errorf("no instructions")
}
if unsafeCount == instructionCount {
t.Errorf("no interruptible instructions")
}
// Note that there are other instructions marked unpreemptible besides
// just the ones required by the write barrier. Those include possibly
// the preamble and postamble, as well as bleeding out from the
// write barrier proper into adjacent instructions (in both directions).
// Hopefully we can clean up the latter at some point.
}Bash grep -n "func UnsafePoint" runtime/export_test.go && sed -n "$(grep -n 'func UnsafePoint' runtime/export_test.go | cut -...
grep -n "func UnsafePoint" runtime/export_test.go && sed -n "$(grep -n 'func UnsafePoint' runtime/export_test.go | cut -d: -f1),+8p" runtime/export_test.go
Check runtime.UnsafePoint helper
1918:func UnsafePoint(pc uintptr) bool {
func UnsafePoint(pc uintptr) bool {
fi := findfunc(pc)
v := pcdatavalue(fi, abi.PCDATA_UnsafePoint, pc)
switch v {
case abi.UnsafePointUnsafe:
return true
case abi.UnsafePointSafe:
return false
case abi.UnsafePointRestart1, abi.UnsafePointRestart2, abi.UnsafePointRestartAtEntry:Bash head -60 cmd/internal/obj/ppc64/asm_test.go; echo "..."; grep -n "^func Test" cmd/internal/obj/ppc64/asm_test.go
head -60 cmd/internal/obj/ppc64/asm_test.go; echo "..."; grep -n "^func Test" cmd/internal/obj/ppc64/asm_test.go
Read ppc64 asm_test.go structure
// Copyright 2020 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 ppc64
import (
"bytes"
"fmt"
"internal/buildcfg"
"internal/testenv"
"math"
"os"
"path/filepath"
"regexp"
"strings"
"testing"
"cmd/internal/obj"
"cmd/internal/objabi"
)
var platformEnvs = [][]string{
{"GOOS=aix", "GOARCH=ppc64"},
{"GOOS=linux", "GOARCH=ppc64"},
{"GOOS=linux", "GOARCH=ppc64le"},
}
const invalidPCAlignSrc = `
TEXT test(SB),0,$0-0
ADD $2, R3
PCALIGN $128
RET
`
const validPCAlignSrc = `
TEXT test(SB),0,$0-0
ADD $2, R3
PCALIGN $16
MOVD $8, R16
ADD $8, R4
PCALIGN $32
ADD $8, R3
PCALIGN $8
ADD $4, R8
RET
`
const x64pgm = `
TEXT test(SB),0,$0-0
OR R0, R0
OR R0, R0
OR R0, R0
OR R0, R0
OR R0, R0
OR R0, R0
OR R0, R0
OR R0, R0
OR R0, R0
OR R0, R0
...
167:func TestPfxAlign(t *testing.T) {
210:func TestLarge(t *testing.T) {
324:func TestPCalign(t *testing.T) {
402:func TestRegValueAlignment(t *testing.T) {
433:func TestAddrClassifier(t *testing.T) {
546:func TestOptabReinit(t *testing.T) {Bash sed -n '324,400p' cmd/internal/obj/ppc64/asm_test.go
sed -n '324,400p' cmd/internal/obj/ppc64/asm_test.go
Read TestPCalign harness pattern
func TestPCalign(t *testing.T) {
var pattern8 = `0x...8\s.*ADD\s..,\sR8`
var pattern16 = `0x...[80]\s.*MOVD\s..,\sR16`
var pattern32 = `0x...0\s.*ADD\s..,\sR3`
testenv.MustHaveGoBuild(t)
dir := t.TempDir()
// generate a test with valid uses of PCALIGN
tmpfile := filepath.Join(dir, "x.s")
err := os.WriteFile(tmpfile, []byte(validPCAlignSrc), 0644)
if err != nil {
t.Fatalf("can't write output: %v\n", err)
}
// build generated file without errors and assemble it
cmd := testenv.Command(t, testenv.GoToolPath(t), "tool", "asm", "-o", filepath.Join(dir, "x.o"), "-S", tmpfile)
cmd.Env = append(os.Environ(), "GOARCH=ppc64le", "GOOS=linux")
out, err := cmd.CombinedOutput()
if err != nil {
t.Errorf("Build failed: %v, output: %s", err, out)
}
matched, err := regexp.MatchString(pattern8, string(out))
if err != nil {
t.Fatal(err)
}
if !matched {
t.Errorf("The 8 byte alignment is not correct: %t, output:%s\n", matched, out)
}
matched, err = regexp.MatchString(pattern16, string(out))
if err != nil {
t.Fatal(err)
}
if !matched {
t.Errorf("The 16 byte alignment is not correct: %t, output:%s\n", matched, out)
}
matched, err = regexp.MatchString(pattern32, string(out))
if err != nil {
t.Fatal(err)
}
if !matched {
t.Errorf("The 32 byte alignment is not correct: %t, output:%s\n", matched, out)
}
// generate a test with invalid use of PCALIGN
tmpfile = filepath.Join(dir, "xi.s")
err = os.WriteFile(tmpfile, []byte(invalidPCAlignSrc), 0644)
if err != nil {
t.Fatalf("can't write output: %v\n", err)
}
// build test with errors and check for messages
cmd = testenv.Command(t, testenv.GoToolPath(t), "tool", "asm", "-o", filepath.Join(dir, "xi.o"), "-S", tmpfile)
cmd.Env = append(os.Environ(), "GOARCH=ppc64le", "GOOS=linux")
out, err = cmd.CombinedOutput()
if !strings.Contains(string(out), "Unexpected alignment") {
t.Errorf("Invalid alignment not detected for PCALIGN\n")
}
}
// Verify register constants are correctly aligned. Much of the ppc64 assembler assumes masking out significant
// bits will produce a valid register number:
// REG_Rx & 31 == x
// REG_Fx & 31 == x
// REG_Vx & 31 == x
// REG_VSx & 63 == x
// REG_SPRx & 1023 == x
// REG_CRx & 7 == x
//
// VR and FPR disjointly overlap VSR, interpreting as VSR registers should produce the correctly overlapped VSR.
// REG_FPx & 63 == xBash sed -n '1870,1900p' src/cmd/compile/internal/ppc64/ssa.go 2>/dev/null || sed -n '1870,1900p' cmd/compile/internal/ppc64/...
sed -n '1870,1900p' src/cmd/compile/internal/ppc64/ssa.go 2>/dev/null || sed -n '1870,1900p' cmd/compile/internal/ppc64/ssa.go
See how CALLtail is emitted
}
case ssaop.OpPPC64CALLstatic:
s.Call(v)
case ssaop.OpPPC64CALLtail, ssaop.OpPPC64CALLtailinter:
s.TailCall(v)
case ssaop.OpPPC64CALLclosure, ssaop.OpPPC64CALLinter:
p := s.Prog(ppc64.AMOVD)
p.From.Type = obj.TYPE_REG
p.From.Reg = v.Args[0].Reg()
p.To.Type = obj.TYPE_REG
p.To.Reg = ppc64.REG_LR
if v.Args[0].Reg() != ppc64.REG_R12 {
v.Fatalf("Function address for %v should be in R12 %d but is in %d", v.LongString(), ppc64.REG_R12, p.From.Reg)
}
pp := s.Call(v)
// Convert the call into a blrl with hint this is not a subroutine return.
// The full bclrl opcode must be specified when passing a hint.
pp.As = ppc64.ABCL
pp.From.Type = obj.TYPE_CONST
pp.From.Offset = ppc64.BO_ALWAYS
pp.Reg = ppc64.REG_CR0LT // The preferred value if BI is ignored.
pp.To.Reg = ppc64.REG_LR
pp.AddRestSourceConst(1)
if ppc64.NeedTOCpointer(base.Ctxt) {Bash cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad && mkdir -p asmtest && ...
cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad && mkdir -p asmtest && cd asmtest && printf 'TEXT ·tail(SB),0,$0-0\nMOVD $0, R3\nRET (R3)\n' > t.s && GOROOT=/home/claude/golang_go_78576/go GOARCH=ppc64le GOOS=linux /home/claude/golang_go_78576/go/bin/go tool asm -S -o t.o t.s 2>&1 | head -20
Test whether asm can express RET (Rx)
<unlinkable>.tail STEXT nosplit asm size=12 align=0x0 args=0x0 locals=0x0 funcid=0x0 leaf 0x0000 00000 (t.s:1) TEXT <unlinkable>.tail(SB), NOSPLIT|LEAF|NOFRAME, $0-0 0x0000 00000 (t.s:1) FUNCDATA $5, <unlinkable>.tail.arginfo0(SB) 0x0000 00000 (t.s:1) FUNCDATA $0, <unlinkable>.tail.args_stackmap(SB) 0x0000 00000 (t.s:2) MOVD $0, R3 0x0004 00004 (t.s:3) MOVD R3, CTR 0x0008 00008 (t.s:3) JMP CTR 0x0000 00 00 60 38 a6 03 69 7c 20 04 80 4e ..`8..i| ..N Shell cwd was reset to /home/claude/golang_go_78576
go tool asm still has the old obj package — let me rebuild it.
Ran tools (1 Bash)
Bash cd /home/claude/golang_go_78576/go/src && GOROOT=/home/claude/golang_go_78576/go ../bin/go install cmd/asm cmd/link && c...
cd /home/claude/golang_go_78576/go/src && GOROOT=/home/claude/golang_go_78576/go ../bin/go install cmd/asm cmd/link && cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/asmtest && GOROOT=/home/claude/golang_go_78576/go GOARCH=ppc64le GOOS=linux /home/claude/golang_go_78576/go/bin/go tool asm -S -o t.o t.s 2>&1 | head -20
Rebuild asm/link and retest
300000
<unlinkable>.tail STEXT nosplit asm size=12 align=0x0 args=0x0 locals=0x0 funcid=0x0 leaf 0x0000 00000 (t.s:1) TEXT <unlinkable>.tail(SB), NOSPLIT|LEAF|NOFRAME, $0-0 0x0000 00000 (t.s:1) FUNCDATA $5, <unlinkable>.tail.arginfo0(SB) 0x0000 00000 (t.s:1) FUNCDATA $0, <unlinkable>.tail.args_stackmap(SB) 0x0000 00000 (t.s:2) MOVD $0, R3 0x0004 00004 (t.s:3) MOVD R3, CTR 0x0008 00008 (t.s:3) PCDATA $0, $-2 0x0008 00008 (t.s:3) JMP CTR 0x000c 00012 (t.s:3) PCDATA $0, $-1 0x0000 00 00 60 38 a6 03 69 7c 20 04 80 4e ..`8..i| ..N Shell cwd was reset to /home/claude/golang_go_78576
Both approaches work. Let me write the actual tests. First the fast cross-arch one.
Ran tools (3 Bash)
Bash cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/asmtest && cat > t2.s <...
cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/asmtest && cat > t2.s <<'EOF' TEXT ·leafNoFrame(SB),NOSPLIT|NOFRAME,$0-0 MOVD $0, R3 RET (R3) TEXT ·leafFrame(SB),NOSPLIT,$8-0 MOVD $0, R3 RET (R3) TEXT ·nonLeaf(SB),$8-0 CALL ·leafNoFrame(SB) MOVD $0, R3 RET (R3) EOF GOROOT=/home/claude/golang_go_78576/go GOARCH=ppc64le GOOS=linux /home/claude/golang_go_78576/go/bin/go tool asm -S -o t2.o t2.s 2>&1 | grep -E "STEXT|CTR|PCDATA\s+\\\$0"
Verify all three RET expansion paths
(Bash completed with no output)
Bash cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/asmtest && GOROOT=/home...
cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/asmtest && GOROOT=/home/claude/golang_go_78576/go GOARCH=ppc64le GOOS=linux /home/claude/golang_go_78576/go/bin/go tool asm -S -o t2.o t2.s 2>&1 | head -40
See full asm output
t2.s:1: expected '(', found |
t2.s:5: illegal or missing addressing mode for symbol NOSPLIT
asm: assembly of t2.s failed
Shell cwd was reset to /home/claude/golang_go_78576Bash cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/asmtest && cat > t2.s <...
cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/asmtest && cat > t2.s <<'EOF' // 4 = NOSPLIT, 512 = NOFRAME (see textflag.h). TEXT ·leafNoFrame(SB),4|512,$0-0 MOVD $0, R3 RET (R3) TEXT ·leafFrame(SB),4,$8-0 MOVD $0, R3 RET (R3) TEXT ·nonLeaf(SB),4,$8-0 CALL ·leafNoFrame(SB) MOVD $0, R3 RET (R3) EOF GOROOT=/home/claude/golang_go_78576/go GOARCH=ppc64le GOOS=linux /home/claude/golang_go_78576/go/bin/go tool asm -S -o t2.o t2.s 2>&1 | grep -E "STEXT|CTR|PCDATA"
Verify all three RET expansion paths
<unlinkable>.leafNoFrame STEXT nosplit asm size=12 align=0x0 args=0x0 locals=0x0 funcid=0x0 leaf 0x0004 00004 (t2.s:4) MOVD R3, CTR 0x0008 00008 (t2.s:4) PCDATA $0, $-2 0x0008 00008 (t2.s:4) JMP CTR 0x000c 00012 (t2.s:4) PCDATA $0, $-1 <unlinkable>.leafFrame STEXT nosplit asm size=24 align=0x0 args=0x0 locals=0x8 funcid=0x0 leaf 0x000c 00012 (t2.s:8) MOVD R3, CTR 0x0010 00016 (t2.s:8) PCDATA $0, $-2 0x0014 00020 (t2.s:8) JMP CTR 0x0018 00024 (t2.s:8) PCDATA $0, $-1 <unlinkable>.nonLeaf STEXT nosplit asm size=36 align=0x0 args=0x0 locals=0x8 funcid=0x0 0x0010 00016 (t2.s:13) MOVD R3, CTR 0x0014 00020 (t2.s:13) PCDATA $0, $-2 0x0020 00032 (t2.s:13) JMP CTR 0x0024 00036 (t2.s:13) PCDATA $0, $-1 Shell cwd was reset to /home/claude/golang_go_78576
All three paths bracketed correctly. Now let me add the real test.
Ran tools (2 Bash)
Bash cd /home/claude/golang_go_78576/go && cp src/cmd/internal/obj/ppc64/asm_test.go /tmp/claude-1003/-home-claude-golang-go-...
cd /home/claude/golang_go_78576/go && cp src/cmd/internal/obj/ppc64/asm_test.go /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/asm_test.go.orig && cat >> src/cmd/internal/obj/ppc64/asm_test.go <<'EOF'
// A tail call is lowered to "MOVD Rx, CTR; BR (CTR)". runtime.asyncPreempt
// does not preserve CTR, and its resume sequence leaves CTR holding the resume
// PC, so a goroutine preempted anywhere between the load of CTR and the branch
// would resume by branching to the wrong place (for a preemption at the branch
// itself, to that very instruction, spinning there forever). Check that the
// whole sequence is marked as an unsafe point. See go.dev/issue/78576.
const tailCallSrc = `
// 4 = NOSPLIT, 512 = NOFRAME (see textflag.h).
TEXT ·leafNoFrame(SB),4|512,$0-0
MOVD $0, R3
RET (R3)
TEXT ·leafFrame(SB),4,$8-0
MOVD $0, R3
RET (R3)
TEXT ·nonLeaf(SB),4,$8-0
CALL ·leafNoFrame(SB)
MOVD $0, R3
RET (R3)
`
func TestTailCallUnsafePoint(t *testing.T) {
testenv.MustHaveGoBuild(t)
dir := t.TempDir()
tmpfile := filepath.Join(dir, "x.s")
if err := os.WriteFile(tmpfile, []byte(tailCallSrc), 0644); err != nil {
t.Fatalf("can't write output: %v\n", err)
}
for _, goarch := range []string{"ppc64", "ppc64le"} {
cmd := testenv.Command(t, testenv.GoToolPath(t), "tool", "asm", "-o", filepath.Join(dir, "x.o"), "-S", tmpfile)
cmd.Env = append(os.Environ(), "GOARCH="+goarch, "GOOS=linux")
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("GOARCH=%s: assembly failed: %v, output:\n%s", goarch, err, out)
}
// Walk the -S output tracking the current PCDATA_UnsafePoint value,
// and check it at every branch through CTR.
branches := 0
unsafePoint := int64(abi.UnsafePointSafe)
for _, line := range strings.Split(string(out), "\n") {
f := strings.Fields(line)
switch {
case len(f) >= 2 && f[0] == "TEXT":
unsafePoint = abi.UnsafePointSafe
case len(f) >= 5 && f[2] == "PCDATA" && f[3] == fmt.Sprintf("$%d,", abi.PCDATA_UnsafePoint):
v, err := strconv.ParseInt(strings.TrimPrefix(f[4], "$"), 10, 64)
if err != nil {
t.Fatalf("GOARCH=%s: can't parse %q: %v", goarch, line, err)
}
unsafePoint = v
case len(f) >= 4 && f[2] == "JMP" && f[3] == "CTR":
branches++
if unsafePoint != abi.UnsafePointUnsafe {
t.Errorf("GOARCH=%s: %s\n\tbranch through CTR has unsafe point %d, want %d",
goarch, strings.TrimSpace(line), unsafePoint, abi.UnsafePointUnsafe)
}
}
}
if want := strings.Count(tailCallSrc, "RET\t(R3)"); branches != want {
t.Errorf("GOARCH=%s: found %d branches through CTR, want %d; output:\n%s", goarch, branches, want, out)
}
}
}
EOF
python3 - <<'PY'
p='src/cmd/internal/obj/ppc64/asm_test.go'
s=open(p).read()
s=s.replace('''import (
"bytes"
"fmt"''','''import (
"bytes"
"fmt"''')
s=s.replace(''' "regexp"
"strings"
"testing"
"cmd/internal/obj"''',''' "regexp"
"strconv"
"strings"
"testing"
"internal/abi"
"cmd/internal/obj"''')
open(p,'w').write(s)
PY
gofmt -l src/cmd/internal/obj/ppc64/asm_test.go; GOROOT=/home/claude/golang_go_78576/go ./bin/go test cmd/internal/obj/ppc64 -run TestTailCallUnsafePoint -v 2>&1 | tail -20Add and run the obj-level test
300000
0x0004 00004 (/tmp/TestTailCallUnsafePoint2241844321/001/x.s:11) MOVDU R31, -40(R1)
0x0008 00008 (/tmp/TestTailCallUnsafePoint2241844321/001/x.s:11) FUNCDATA $5, <unlinkable>.nonLeaf.arginfo0(SB)
0x0008 00008 (/tmp/TestTailCallUnsafePoint2241844321/001/x.s:11) FUNCDATA $0, <unlinkable>.nonLeaf.args_stackmap(SB)
0x0008 00008 (/tmp/TestTailCallUnsafePoint2241844321/001/x.s:12) CALL <unlinkable>.leafNoFrame(SB)
0x000c 00012 (/tmp/TestTailCallUnsafePoint2241844321/001/x.s:13) MOVD $0, R3
0x0010 00016 (/tmp/TestTailCallUnsafePoint2241844321/001/x.s:14) MOVD R3, CTR
0x0014 00020 (/tmp/TestTailCallUnsafePoint2241844321/001/x.s:14) PCDATA $0, $-2
0x0014 00020 (/tmp/TestTailCallUnsafePoint2241844321/001/x.s:14) MOVD (R1), R31
0x0018 00024 (/tmp/TestTailCallUnsafePoint2241844321/001/x.s:14) MOVD R31, LR
0x001c 00028 (/tmp/TestTailCallUnsafePoint2241844321/001/x.s:14) ADD $40, R1
0x0020 00032 (/tmp/TestTailCallUnsafePoint2241844321/001/x.s:14) JMP CTR
0x0024 00036 (/tmp/TestTailCallUnsafePoint2241844321/001/x.s:14) PCDATA $0, $-1
0x0000 a6 02 e8 7f d9 ff e1 fb 01 00 00 48 00 00 60 38 ...........H..`8
0x0010 a6 03 69 7c 00 00 e1 eb a6 03 e8 7f 28 00 21 38 ..i|........(.!8
0x0020 20 04 80 4e ..N
rel 8+4 t=R_CALLPOWER <unlinkable>.leafNoFrame+0
--- FAIL: TestTailCallUnsafePoint (0.01s)
FAIL
FAIL cmd/internal/obj/ppc64 0.012s
FAILBash GOROOT=/home/claude/golang_go_78576/go ./bin/go test cmd/internal/obj/ppc64 -run TestTailCallUnsafePoint 2>&1 | grep -E ...
GOROOT=/home/claude/golang_go_78576/go ./bin/go test cmd/internal/obj/ppc64 -run TestTailCallUnsafePoint 2>&1 | grep -E "asm_test.go:|want|FAIL" | head -10
Show the assertion failure
--- FAIL: TestTailCallUnsafePoint (0.02s)
asm_test.go:626: GOARCH=ppc64: found 0 branches through CTR, want 3; output:
asm_test.go:626: GOARCH=ppc64le: found 0 branches through CTR, want 3; output:
FAIL
FAIL cmd/internal/obj/ppc64 0.017s
FAILIndex off-by-one in the field parsing — the disassembly lines have three leading columns.
Ran tools (1 Bash)
Bash python3 - <<'PY' ...
python3 - <<'PY'
p='src/cmd/internal/obj/ppc64/asm_test.go'
s=open(p).read()
old=''' switch {
case len(f) >= 2 && f[0] == "TEXT":
unsafePoint = abi.UnsafePointSafe
case len(f) >= 5 && f[2] == "PCDATA" && f[3] == fmt.Sprintf("$%d,", abi.PCDATA_UnsafePoint):
v, err := strconv.ParseInt(strings.TrimPrefix(f[4], "$"), 10, 64)
if err != nil {
t.Fatalf("GOARCH=%s: can't parse %q: %v", goarch, line, err)
}
unsafePoint = v
case len(f) >= 4 && f[2] == "JMP" && f[3] == "CTR":'''
new=''' // Lines look like:
// sym STEXT ...
// <tab>0x0014 00020 (x.s:14)<tab>PCDATA<tab>$0,<tab>$-2
switch {
case len(f) >= 2 && f[1] == "STEXT":
unsafePoint = abi.UnsafePointSafe
case len(f) >= 6 && f[3] == "PCDATA" && f[4] == fmt.Sprintf("$%d,", abi.PCDATA_UnsafePoint):
v, err := strconv.ParseInt(strings.TrimPrefix(f[5], "$"), 10, 64)
if err != nil {
t.Fatalf("GOARCH=%s: can't parse %q: %v", goarch, line, err)
}
unsafePoint = v
case len(f) >= 5 && f[3] == "JMP" && f[4] == "CTR":'''
assert s.count(old)==1
s=s.replace(old,new)
open(p,'w').write(s)
PY
gofmt -l src/cmd/internal/obj/ppc64/asm_test.go; GOROOT=/home/claude/golang_go_78576/go ./bin/go test cmd/internal/obj/ppc64 -run TestTailCallUnsafePoint 2>&1 | tail -5Fix field indices and rerun
ok cmd/internal/obj/ppc64 0.012s
Now verify it actually catches the bug — revert the fix and re-run.
Ran tools (2 Bash)
Bash cp src/cmd/internal/obj/ppc64/obj9.go /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172...
cp src/cmd/internal/obj/ppc64/obj9.go /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/obj9.go.fixed && cp /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/obj9.go.orig src/cmd/internal/obj/ppc64/obj9.go && GOROOT=/home/claude/golang_go_78576/go ./bin/go install cmd/asm && echo "=== unpatched obj9.go ===" && GOROOT=/home/claude/golang_go_78576/go ./bin/go test cmd/internal/obj/ppc64 -run TestTailCallUnsafePoint 2>&1 | grep -E "asm_test.go:6|FAIL|ok" | head -8
Verify test fails without the fix
300000
=== unpatched obj9.go ===
--- FAIL: TestTailCallUnsafePoint (0.01s)
asm_test.go:623: GOARCH=ppc64: 0x0008 00008 (/tmp/TestTailCallUnsafePoint1707624480/001/x.s:5) JMP CTR
asm_test.go:623: GOARCH=ppc64: 0x0014 00020 (/tmp/TestTailCallUnsafePoint1707624480/001/x.s:9) JMP CTR
asm_test.go:623: GOARCH=ppc64: 0x0020 00032 (/tmp/TestTailCallUnsafePoint1707624480/001/x.s:14) JMP CTR
asm_test.go:623: GOARCH=ppc64le: 0x0008 00008 (/tmp/TestTailCallUnsafePoint1707624480/001/x.s:5) JMP CTR
asm_test.go:623: GOARCH=ppc64le: 0x0014 00020 (/tmp/TestTailCallUnsafePoint1707624480/001/x.s:9) JMP CTR
asm_test.go:623: GOARCH=ppc64le: 0x0020 00032 (/tmp/TestTailCallUnsafePoint1707624480/001/x.s:14) JMP CTR
FAILBash cp /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/obj9.go.fixed src/cmd/i...
cp /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/obj9.go.fixed src/cmd/internal/obj/ppc64/obj9.go && GOROOT=/home/claude/golang_go_78576/go ./bin/go install cmd/asm cmd/compile cmd/link && GOROOT=/home/claude/golang_go_78576/go ./bin/go test cmd/internal/obj/ppc64 2>&1 | tail -3
Restore fix and confirm green
300000
ok cmd/internal/obj/ppc64 0.719s
Now the end-to-end runtime test. Let me check what I can use to locate the wrapper.
Ran tools (9 Bash)
Bash grep -n -A10 "^type ITab" src/internal/abi/iface.go 2>/dev/null || grep -rn -A10 "^type ITab" src/internal/abi/
grep -n -A10 "^type ITab" src/internal/abi/iface.go 2>/dev/null || grep -rn -A10 "^type ITab" src/internal/abi/
Check abi.ITab layout
17:type ITab struct {
18- Inter *InterfaceType
19- Type *Type
20- Hash uint32 // copy of Type.Hash. Used for type switches.
21- Fun [1]uintptr // variable sized. fun[0]==0 means Type does not implement Inter.
22-}
23-
24-// Size returns the size of the itab in memory.
25-func (it *ITab) Size() int {
26- size := int(unsafe.Sizeof(ITab{}))
27- if it.Fun[0] == 0 {Bash cp src/runtime/unsafepoint_test.go /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/sc...
cp src/runtime/unsafepoint_test.go /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/unsafepoint_test.go.orig && cat >> src/runtime/unsafepoint_test.go <<'EOF'
// tailCallOuter embeds an interface, so the compiler generates a wrapper for
// the promoted method M. On ppc64 that wrapper ends in a tail call:
//
// MOVD Rx, CTR
// BR (CTR)
//
// runtime.asyncPreempt does not preserve CTR, and its resume sequence leaves
// CTR holding the resume PC, so a goroutine preempted at that branch would
// resume by branching to that very instruction and spin there forever. The
// branch must therefore be marked as an unsafe point. See go.dev/issue/78576.
type tailCallInner interface{ M() int }
type tailCallImpl struct{}
func (tailCallImpl) M() int { return 42 }
type tailCallOuter struct{ tailCallInner }
var tailCallValue tailCallInner = tailCallOuter{tailCallImpl{}}
func TestUnsafePointTailCall(t *testing.T) {
switch runtime.GOARCH {
case "ppc64", "ppc64le":
default:
t.Skipf("test not enabled for %s", runtime.GOARCH)
}
testenv.MustHaveExec(t)
if got := tailCallValue.M(); got != 42 {
t.Fatalf("tailCallValue.M() = %d, want 42", got)
}
// The itab's first method slot holds the generated wrapper for
// tailCallOuter.M, which is the function containing the tail call.
iface := (*struct {
tab *abi.ITab
data unsafe.Pointer
})(unsafe.Pointer(&tailCallValue))
f := runtime.FuncForPC(iface.tab.Fun[0])
if f == nil {
t.Fatal("no func for the tailCallOuter.M wrapper")
}
// See TestUnsafePoint for why objdump works here.
cmd := exec.Command(testenv.GoToolPath(t), "tool", "objdump", "-s", "^"+regexp.QuoteMeta(f.Name())+"$", os.Args[0])
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("can't objdump %v:\n%s", err, out)
}
// Walk the disassembly and check the branch through CTR. As in
// TestUnsafePoint, only offsets from the function entry are meaningful.
var entry uint64
branches := 0
for _, line := range strings.Split(string(out), "\n")[1:] {
parts := strings.Fields(strings.TrimSpace(line))
if len(parts) < 4 || !strings.HasPrefix(parts[0], "<autogenerated>:") {
continue
}
pc, err := strconv.ParseUint(parts[1][2:], 16, 64)
if err != nil {
t.Fatalf("can't parse pc %s: %v", parts[1], err)
}
if entry == 0 {
entry = pc
}
t.Logf("%s", strings.TrimSpace(line))
if parts[3] != "BR" || parts[4] != "(CTR)" {
continue
}
branches++
if !runtime.UnsafePoint(f.Entry() + uintptr(pc-entry)) {
t.Errorf("%s\n\tbranch through CTR must be marked unsafe, but isn't", strings.TrimSpace(line))
}
}
if branches != 1 {
t.Errorf("found %d branches through CTR in %s, want 1; output:\n%s", branches, f.Name(), out)
}
}
EOF
python3 - <<'PY'
p='src/runtime/unsafepoint_test.go'
s=open(p).read()
s=s.replace('''import (
"internal/testenv"
"os"
"os/exec"
"reflect"
"runtime"
"strconv"
"strings"
"testing"
)''','''import (
"internal/abi"
"internal/testenv"
"os"
"os/exec"
"reflect"
"regexp"
"runtime"
"strconv"
"strings"
"testing"
"unsafe"
)''')
open(p,'w').write(s)
PY
gofmt -l src/runtime/unsafepoint_test.go; GOROOT=/home/claude/golang_go_78576/go GOOS=linux GOARCH=ppc64le ./bin/go vet runtime 2>&1 | head -5; echo "vet done"Add runtime end-to-end test
300000
vet done
Bash cd /home/claude/golang_go_78576/go/src && GOROOT=/home/claude/golang_go_78576/go GOOS=linux GOARCH=ppc64le ../bin/go tes...
cd /home/claude/golang_go_78576/go/src && GOROOT=/home/claude/golang_go_78576/go GOOS=linux GOARCH=ppc64le ../bin/go test -exec 'qemu-ppc64le' runtime -run 'TestUnsafePointTailCall' -v 2>&1 | tail -25
Run runtime test under qemu-ppc64le
900000
=== RUN TestUnsafePointTailCall
unsafepoint_test.go:193: <autogenerated>:1 0x4d7a00 e8830000 MOVD 0(R3),R4
unsafepoint_test.go:193: <autogenerated>:1 0x4d7a04 e8630008 MOVD 8(R3),R3
unsafepoint_test.go:193: <autogenerated>:1 0x4d7a08 e9840018 MOVD 24(R4),R12
unsafepoint_test.go:193: <autogenerated>:1 0x4d7a0c 7d8903a6 MOVD R12,CTR
unsafepoint_test.go:193: <autogenerated>:1 0x4d7a10 4e800420 BR (CTR)
--- PASS: TestUnsafePointTailCall (0.16s)
PASS
ok runtime 0.228sBash cp /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/obj9.go.orig cmd/intern...
cp /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/obj9.go.orig cmd/internal/obj/ppc64/obj9.go && GOROOT=/home/claude/golang_go_78576/go ../bin/go install cmd/compile cmd/asm cmd/link && echo "=== unpatched ===" && GOROOT=/home/claude/golang_go_78576/go GOOS=linux GOARCH=ppc64le ../bin/go test -a -exec 'qemu-ppc64le' runtime -run 'TestUnsafePointTailCall' 2>&1 | grep -E "must be marked|FAIL|ok " | head -5
Verify runtime test fails without the fix
900000
=== unpatched ===
--- FAIL: TestUnsafePointTailCall (1.99s)
branch through CTR must be marked unsafe, but isn't
FAIL
FAIL runtime 2.097s
FAILBash cp /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/obj9.go.fixed cmd/inter...
cp /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/obj9.go.fixed cmd/internal/obj/ppc64/obj9.go && GOROOT=/home/claude/golang_go_78576/go ../bin/go install cmd/compile cmd/asm cmd/link && GOROOT=/home/claude/golang_go_78576/go GOOS=linux GOARCH=ppc64le ../bin/go test -a -exec 'qemu-ppc64le' runtime -run 'TestUnsafePoint' 2>&1 | tail -3 && GOROOT=/home/claude/golang_go_78576/go ../bin/go test runtime -run TestUnsafePoint cmd/internal/obj/ppc64 2>&1 | tail -3
Restore fix, confirm both tests green on ppc64le and amd64
900000
ok runtime 0.160s ok runtime 0.120s
Bash cd /home/claude/golang_go_78576/go && git diff --stat && git status --short
cd /home/claude/golang_go_78576/go && git diff --stat && git status --short
Summarize all changes
src/cmd/internal/obj/ppc64/asm_test.go | 74 ++++++++++++++++++++++++++++++ src/runtime/unsafepoint_test.go | 83 ++++++++++++++++++++++++++++++++++ 2 files changed, 157 insertions(+) M src/cmd/internal/obj/ppc64/asm_test.go M src/runtime/unsafepoint_test.go
Bash grep -c "StartUnsafePoint" src/cmd/internal/obj/ppc64/obj9.go; ls -la src/cmd/internal/obj/ppc64/obj9.go; diff <(cat /tm...
grep -c "StartUnsafePoint" src/cmd/internal/obj/ppc64/obj9.go; ls -la src/cmd/internal/obj/ppc64/obj9.go; diff <(cat /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/obj9.go.fixed) src/cmd/internal/obj/ppc64/obj9.go && echo "MATCHES fixed version"
Verify obj9.go state
2 -rw-rw-r-- 1 claude claude 40183 Aug 25 14:50 src/cmd/internal/obj/ppc64/obj9.go MATCHES fixed version
Bash git diff --stat HEAD -- src/cmd/internal/obj/ppc64/obj9.go; echo "---log---"; git log --oneline -3; echo "---stash/statu...
git diff --stat HEAD -- src/cmd/internal/obj/ppc64/obj9.go; echo "---log---"; git log --oneline -3; echo "---stash/status---"; git status --short --untracked-files=no; echo "---diff cached---"; git diff --cached --stat
Investigate git state
---log--- 92ad37fc5f obj9: fix CTR bug 13b477edc7 runtime: fix flaky refill detection in TestScanAllocIssue77573 72755de33c os, internal/poll: use splice for copies involving pipes ---stash/status--- M src/cmd/internal/obj/ppc64/asm_test.go M src/runtime/unsafepoint_test.go ---diff cached---
Bash git show --stat 92ad37fc5f | head -8; git show 92ad37fc5f -- src/cmd/internal/obj/ppc64/obj9.go | grep -c "^+"
git show --stat 92ad37fc5f | head -8; git show 92ad37fc5f -- src/cmd/internal/obj/ppc64/obj9.go | grep -c "^+"
Check the existing commit
commit 92ad37fc5f42e04b032707dc7070133afe46b1ce
Author: Russ Cox <rsc@golang.org>
Date: Tue Aug 25 14:39:22 2026 -0400
obj9: fix CTR bug
src/cmd/internal/obj/ppc64/obj9.go | 16 ++++++++++++++++
1 file changed, 16 insertions(+)
17Both tests are written and verified. Note: you committed the fix as 92ad37fc5f obj9: fix CTR bug; the two test files are the uncommitted changes in the tree.
Existing precedent
There's exactly one test of this kind: runtime/unsafepoint_test.go:TestUnsafePoint (added 2023). It disassembles a function with a write barrier and asserts every instruction inside the barrier is unpreemptible:
f := runtime.FuncForPC(reflect.ValueOf(setGlobalPointer).Pointer())
cmd := exec.Command(testenv.GoToolPath(t), "tool", "objdump", "-s", "setGlobalPointer", os.Args[0])
...
unsafe := runtime.UnsafePoint(f.Entry() + uintptr(pc-entry))
It's amd64, arm64 only, and it detects the barrier by hardcoded mnemonic matching per arch. runtime.UnsafePoint is an export_test.go wrapper over pcdatavalue(fi, abi.PCDATA_UnsafePoint, pc). It works because cmd/go/internal/test exempts runtime tests from symbol stripping.
Nothing else covers this. In particular the unsafePoint: true flags on LoweredMove/LoweredZero/LoweredQuad* in _gen/PPC64Ops.go — which are marked precisely because they hold a live CTR — have no test at all. (-d=maymorestack=runtime.mayMoreStackPreempt stresses preemption, but only at function entry, so it can't hit this.)
What I added
1. cmd/internal/obj/ppc64.TestTailCallUnsafePoint — the cheap one, and the one that matters most operationally because it runs on every builder rather than only the ppc64 ones. Assembles three RET (R3) shapes (leaf/NOFRAME, leaf with frame, non-leaf) for both ppc64 and ppc64le, then walks the go tool asm -S listing tracking the current PCDATA_UnsafePoint value and asserts it is UnsafePointUnsafe at every JMP CTR:
case len(f) >= 6 && f[3] == "PCDATA" && f[4] == fmt.Sprintf("$%d,", abi.PCDATA_UnsafePoint):
unsafePoint = v
case len(f) >= 5 && f[3] == "JMP" && f[4] == "CTR":
branches++
if unsafePoint != abi.UnsafePointUnsafe { t.Errorf(...) }
Checking all three shapes matters — the non-leaf expansion puts the LR restore and ADD $40, R1 between the CTR load and the branch, and a preemption there is equally fatal. Verified: 6 failures (3 shapes × 2 arches) before the fix, passes after, 12ms.
2. runtime.TestUnsafePointTailCall — end-to-end, ppc64/ppc64le only, appended to the existing file. Uses an embedded-interface type so the compiler generates the wrapper, locates it through the itab's method slot ((*abi.ITab).Fun[0] — the wrapper is invisible to runtime.Callers precisely because it's a tail call), objdumps it, and asserts runtime.UnsafePoint at the BR (CTR). Verified under qemu-ppc64le: fails before the fix, passes after.
This one earns its keep despite the arch restriction: test 1 only checks the assembler's listing, whereas this checks that the marking survives into the linked pclntab and that the runtime's own lookup agrees.
What I didn't write, and why
- A test that provokes the actual hang. The vulnerable window is one instruction; my reproducer needed
qemu -one-insn-per-tbto get signals to land there at all, and it's still probabilistic. Not viable in the standard suite. - A whole-binary invariant scan — assert that every
bcctr-form instruction in a linked binary is an unsafe point. That's the strongest version and would also cover theLoweredMove/LoweredZeroCTR loops and any future CTR use, rather than just the site we know about. I skipped it becausego tool objdumpover all ofruntime.testruns into minutes. If you want it as a separately-tagged slow test, or pushed intocmd/link's tests where a full disassembly pass is more at home, I'll write it.
You Tue 25 Aug 2026 14:53:51
What about the other architectures, like MIPS, RISCV, and ARM? Do they have this bug?
Assistant Tue 25 Aug 2026 14:54:17
Good question — the general invariant is "every register that can be live at an async-safe point must be preserved by asyncPreempt." Let me audit each arch's save list.
Ran tools (4 Bash)
Bash grep -n "^func gen\|addSpecial(" src/runtime/mkpreempt.go
grep -n "^func gen\|addSpecial(" src/runtime/mkpreempt.goList per-arch generators and special registers
273:func (l *layout) addSpecial(save, restore string, size int) {
349:func gen386(g *gen) {
387:func genAMD64(g *gen) {
517:func genARM(g *gen) {
531: l.addSpecial(
538: lfp.addSpecial(
563:func genARM64(g *gen) {
582: l.addSpecial(
586: l.addSpecial(
700:func genMIPS(g *gen, _64bit bool) {
732: l.addSpecial(
736: l.addSpecial(
743: lfp.addSpecial(
773:func genLoong64(g *gen) {
815: l.addSpecial(
822: l.addSpecial(
909:func genPPC64(g *gen) {
928: l.addSpecial(
932: l.addSpecial(
942: l.addSpecial(
966:func genRISCV64(g *gen) {
998:func genS390X(g *gen) {
1005: l.addSpecial(
1033:func genWasm(g *gen) {Bash cd /home/claude/golang_go_78576/go/src && for a in ARM MIPS Loong64 S390X; do echo "=========== gen$a"; sed -n "$(grep -...
cd /home/claude/golang_go_78576/go/src && for a in ARM MIPS Loong64 S390X; do echo "=========== gen$a"; sed -n "$(grep -n "func gen$a" runtime/mkpreempt.go | cut -d: -f1),+55p" runtime/mkpreempt.go | grep -E "addSpecial|^\s+//|special|not saved|l :=|lfp" | head -25; done
Inspect special-register handling per arch
=========== genARM
sed: -e expression #1, char 4: unknown command: `
'
=========== genMIPS
// Add integer registers R1-R22, R24-R25, R28
// R0 (zero), R23 (REGTMP), R29 (SP), R30 (g), R31 (LR) are special,
// and not saved here. R26 and R27 are reserved by kernel and not used.
l.addSpecial(
l.addSpecial(
// Add floating point control/status register FCR31 (FCR0-FCR30 are irrelevant)
var lfp = layout{sp: "R29", stack: l.stack}
lfp.addSpecial(
// Add floating point registers F0-F31.
lfp.add(movf, reg, regsize)
// allocate frame, save PC of interrupted instruction (in LR)
p(mov+" R31, -%d(R29)", lfp.stack)
p(sub+" $%d, R29", lfp.stack)
=========== genLoong64
p, label := g.p, g.label
// Add integer registers r4-r21 r23-r29 r31
// R0 (zero), R30 (REGTMP), R2 (tp), R3 (SP), R22 (g), R1 (LR) are special,
// Add condition flag register fcc0-fcc7
// MOVV FCCx, R4,
// BSTRINSV $msb, R4, $lsb, R5
// BSTRPICKV $msb, R5, $lsb, R4
// MOVV R4, FCCx
l.addSpecial(
// Add floating point control/status register fcsr0,
// fcsr1~fcsr3 are aliases for some fields in fcsr0.
l.addSpecial(
// Create layouts for lasx, lsx and fp registers.
=========== genS390X
// Add integer registers R0-R12
// R13 (g), R14 (LR), R15 (SP) are special, and not saved here.
// Saving R10 (REGTMP) is not necessary, but it is saved anyway.
l.addSpecial(
// Add floating point registers F0-F31.
// allocate frame, save PC of interrupted instruction (in LR) and flags (condition code)Bash for f in runtime/preempt_arm.s runtime/preempt_arm64.s runtime/preempt_mips64x.s runtime/preempt_mipsx.s runtime/preempt...
for f in runtime/preempt_arm.s runtime/preempt_arm64.s runtime/preempt_mips64x.s runtime/preempt_mipsx.s runtime/preempt_riscv64.s runtime/preempt_loong64.s runtime/preempt_s390x.s runtime/preempt_386.s runtime/preempt_amd64.s; do echo "=== $f"; grep -oE "\b(HI|LO|CPSR|FPSCR|FCR31|FPCR|FPSR|NZCV|FCC[0-7]|FCSR|CR|XER|CTR|LR|PC|FLAGS)\b" $f | sort -u | tr '\n' ' '; echo; done
Which special registers each arch saves
=== runtime/preempt_arm.s CPSR FPCR === runtime/preempt_arm64.s FPSR NZCV === runtime/preempt_mips64x.s FCR31 HI LO === runtime/preempt_mipsx.s FCR31 HI LO === runtime/preempt_riscv64.s === runtime/preempt_loong64.s FCC0 FCC1 FCC2 FCC3 FCC4 FCC5 FCC6 FCC7 === runtime/preempt_s390x.s === runtime/preempt_386.s === runtime/preempt_amd64.s
Bash grep -rn "MarkUnsafePoints" cmd/internal/obj/*/*.go
grep -rn "MarkUnsafePoints" cmd/internal/obj/*/*.go
Which arches mark assembler-generated unsafe points
cmd/internal/obj/loong64/asm.go:651: obj.MarkUnsafePoints(c.ctxt, c.cursym.Func().Text, c.newprog, c.isUnsafePoint, c.isRestartable) cmd/internal/obj/mips/asm0.go:535: obj.MarkUnsafePoints(c.ctxt, c.cursym.Func().Text, c.newprog, c.isUnsafePoint, c.isRestartable) cmd/internal/obj/arm64/asm7.go:1222: obj.MarkUnsafePoints(c.ctxt, c.cursym.Func().Text, c.newprog, c.isUnsafePoint, c.isRestartable) cmd/internal/obj/x86/asm6.go:2244: obj.MarkUnsafePoints(ctxt, s.Func().Text, newprog, useTLS, nil) cmd/internal/obj/s390x/asmz.go:534: obj.MarkUnsafePoints(c.ctxt, c.cursym.Func().Text, c.newprog, c.isUnsafePoint, nil) cmd/internal/obj/riscv/obj.go:5184: obj.MarkUnsafePoints(ctxt, cursym.Func().Text, newprog, isUnsafePoint, nil)
Two arches never call obj.MarkUnsafePoints in their assembler backend: ppc64 (the bug) and arm. Let me dig into that.
Ran tools (2 Bash)
Bash sed -n "$(grep -n 'func genARM(' runtime/mkpreempt.go | cut -d: -f1),+16p" runtime/mkpreempt.go; echo "=== saved GPRs in...
sed -n "$(grep -n 'func genARM(' runtime/mkpreempt.go | cut -d: -f1),+16p" runtime/mkpreempt.go; echo "=== saved GPRs in preempt_arm.s ==="; grep -oE "MOVW R[0-9]+," runtime/preempt_arm.s | sort -u -V | tr '\n' ' '; echo; echo "=== arm64 ==="; sed -n "$(grep -n 'func genARM64(' runtime/mkpreempt.go | cut -d: -f1),+16p" runtime/mkpreempt.goWhich GPRs do ARM and ARM64 save
func genARM(g *gen) {
p := g.p
// Add integer registers R0-R12.
// R13 (SP), R14 (LR), R15 (PC) are special and not saved here.
var l = layout{sp: "R13", stack: 4} // add LR slot
for i := 0; i <= 12; i++ {
reg := fmt.Sprintf("R%d", i)
if i == 10 {
continue // R10 is g register, no need to save/restore
}
l.add("MOVW", reg, 4)
}
// Add flag register.
l.addSpecial(
"MOVW CPSR, R0\nMOVW R0, %d(R13)",
"MOVW %d(R13), R0\nMOVW R0, CPSR",
=== saved GPRs in preempt_arm.s ===
MOVW R0, MOVW R1, MOVW R2, MOVW R3, MOVW R4, MOVW R5, MOVW R6, MOVW R7, MOVW R8, MOVW R9, MOVW R11, MOVW R12,
=== arm64 ===
func genARM64(g *gen) {
const vReg = "R0" // *xRegState
p := g.p
// Add integer registers R0-R26
// R27 (REGTMP), R28 (g), R29 (FP), R30 (LR), R31 (SP) are special
// and not saved here.
var l = layout{sp: "RSP", stack: 8} // add slot to save PC of interrupted instruction
for i := 0; i < 26; i += 2 {
if i == 18 {
i--
continue // R18 is not used, skip
}
regs := []regInfo{
{name: fmt.Sprintf("R%d", i), size: 8},
{name: fmt.Sprintf("R%d", i+1), size: 8},
}
l.add2("STP", "LDP", regs, [2]string{"(", ")"}, false)Bash cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/repro && export GOROOT=...
cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/repro && export GOROOT=/home/claude/golang_go_78576/go for a in 386 amd64 arm arm64 loong64 mips mips64 ppc64le riscv64 s390x; do echo "===== GOARCH=$a" GOOS=linux GOARCH=$a $GOROOT/bin/go build -gcflags='-S' -o /dev/null . 2>&1 | sed -n '/main\.(\*outer)\.M(SB), DUPOK/,/^main\./p' | grep -E "^\s+0x" | sed 's/([^)]*)//' | head -8 done
Disassemble the tail-call wrapper on every arch
600000
===== GOARCH=386 0x0000 00000 TEXT main.(*outer).M(SB), DUPOK|NOFRAME|ABIInternal, $0-8 0x0000 00000 MOVL TLS, CX 0x0007 00007 PCDATA $0, $-2 0x0007 00007 MOVL (CX)(TLS*2), CX 0x000d 00013 PCDATA $0, $-1 0x000d 00013 CMPL SP, 8(CX) 0x0010 00016 PCDATA $0, $-2 0x0010 00016 JLS 38 ===== GOARCH=amd64 0x0000 00000 TEXT main.(*outer).M(SB), DUPOK|NOSPLIT|NOFRAME|ABIInternal, $0-8 0x0000 00000 FUNCDATA $0, gclocals·wvjpxkknJ4nY1JtrArJJaw==(SB) 0x0000 00000 FUNCDATA $1, gclocals·J26BEvPExEQhJvjp9E8Whg==(SB) 0x0000 00000 FUNCDATA $5, main.(*outer).M.arginfo1(SB) 0x0000 00000 FUNCDATA $6, main.(*outer).M.argliveinfo(SB) 0x0000 00000 PCDATA $3, $1 0x0000 00000 MOVQ (AX), CX 0x0003 00003 MOVQ 8(AX), AX ===== GOARCH=arm 0x0000 00000 TEXT main.(*outer).M(SB), DUPOK|LEAF|NOFRAME|ABIInternal, $-4-8 0x0000 00000 FUNCDATA $0, gclocals·wvjpxkknJ4nY1JtrArJJaw==(SB) 0x0000 00000 FUNCDATA $1, gclocals·J26BEvPExEQhJvjp9E8Whg==(SB) 0x0000 00000 FUNCDATA $5, main.(*outer).M.arginfo1(SB) 0x0000 00000 MOVW $-4(FP), R0 0x0004 00004 MOVW main.~p0(FP), R1 0x0008 00008 MOVW (R1), R2 0x000c 00012 MOVW 4(R1), R1 ===== GOARCH=arm64 0x0000 00000 TEXT main.(*outer).M(SB), DUPOK|LEAF|NOFRAME|ABIInternal, $0-8 0x0000 00000 FUNCDATA $0, gclocals·wvjpxkknJ4nY1JtrArJJaw==(SB) 0x0000 00000 FUNCDATA $1, gclocals·J26BEvPExEQhJvjp9E8Whg==(SB) 0x0000 00000 FUNCDATA $5, main.(*outer).M.arginfo1(SB) 0x0000 00000 FUNCDATA $6, main.(*outer).M.argliveinfo(SB) 0x0000 00000 PCDATA $3, $1 0x0000 00000 LDP (R0), (R1, R0) 0x0004 00004 MOVD 24(R1), R1 ===== GOARCH=loong64 0x0000 00000 TEXT main.(*outer).M(SB), DUPOK|NOSPLIT|LEAF|NOFRAME|ABIInternal, $-8-8 0x0000 00000 FUNCDATA $0, gclocals·wvjpxkknJ4nY1JtrArJJaw==(SB) 0x0000 00000 FUNCDATA $1, gclocals·J26BEvPExEQhJvjp9E8Whg==(SB) 0x0000 00000 FUNCDATA $5, main.(*outer).M.arginfo1(SB) 0x0000 00000 FUNCDATA $6, main.(*outer).M.argliveinfo(SB) 0x0000 00000 PCDATA $3, $1 0x0000 00000 MOVV (R4), R5 0x0004 00004 MOVV 8(R4), R4 ===== GOARCH=mips 0x0000 00000 TEXT main.(*outer).M(SB), DUPOK|LEAF|NOFRAME|ABIInternal, $-4-8 0x0000 00000 FUNCDATA $0, gclocals·kX4bwiXK7mpLFRO/ju1zqQ==(SB) 0x0000 00000 FUNCDATA $1, gclocals·YeUK6n5S8X3IB0iLvWMeNg==(SB) 0x0000 00000 FUNCDATA $5, main.(*outer).M.arginfo1(SB) 0x0000 00000 MOVW $-4(FP), R1 0x0004 00004 MOVW main.~p0(FP), R2 0x0008 00008 MOVW (R2), R3 0x000c 00012 MOVW 4(R2), R2 ===== GOARCH=mips64 0x0000 00000 TEXT main.(*outer).M(SB), DUPOK|LEAF|NOFRAME|ABIInternal, $-8-16 0x0000 00000 FUNCDATA $0, gclocals·kX4bwiXK7mpLFRO/ju1zqQ==(SB) 0x0000 00000 FUNCDATA $1, gclocals·YeUK6n5S8X3IB0iLvWMeNg==(SB) 0x0000 00000 FUNCDATA $5, main.(*outer).M.arginfo1(SB) 0x0000 00000 MOVV $-8(FP), R1 0x0004 00004 MOVV main.~p0(FP), R2 0x0008 00008 MOVV (R2), R3 0x000c 00012 MOVV 8(R2), R2 ===== GOARCH=ppc64le 0x0000 00000 TEXT main.(*outer).M(SB), DUPOK|NOSPLIT|LEAF|NOFRAME|ABIInternal, $0-8 0x0000 00000 FUNCDATA $0, gclocals·wvjpxkknJ4nY1JtrArJJaw==(SB) 0x0000 00000 FUNCDATA $1, gclocals·J26BEvPExEQhJvjp9E8Whg==(SB) 0x0000 00000 FUNCDATA $5, main.(*outer).M.arginfo1(SB) 0x0000 00000 FUNCDATA $6, main.(*outer).M.argliveinfo(SB) 0x0000 00000 PCDATA $3, $1 0x0000 00000 MOVD (R3), R4 0x0004 00004 MOVD 8(R3), R3 ===== GOARCH=riscv64 0x0000 00000 TEXT main.(*outer).M(SB), DUPOK|LEAF|NOFRAME|ABIInternal, $0-8 0x0000 00000 FUNCDATA $0, gclocals·wvjpxkknJ4nY1JtrArJJaw==(SB) 0x0000 00000 FUNCDATA $1, gclocals·J26BEvPExEQhJvjp9E8Whg==(SB) 0x0000 00000 FUNCDATA $5, main.(*outer).M.arginfo1(SB) 0x0000 00000 FUNCDATA $6, main.(*outer).M.argliveinfo(SB) 0x0000 00000 PCDATA $3, $1 0x0000 00000 MOV (X10), X9 0x0002 00002 MOV 8(X10), X10 ===== GOARCH=s390x 0x0000 00000 TEXT main.(*outer).M(SB), DUPOK|NOSPLIT|LEAF|NOFRAME|ABIInternal, $0-8 0x0000 00000 FUNCDATA $0, gclocals·kX4bwiXK7mpLFRO/ju1zqQ==(SB) 0x0000 00000 FUNCDATA $1, gclocals·YeUK6n5S8X3IB0iLvWMeNg==(SB) 0x0000 00000 FUNCDATA $5, main.(*outer).M.arginfo1(SB) 0x0000 00000 FUNCDATA $6, main.(*outer).M.argliveinfo(SB) 0x0000 00000 PCDATA $3, $1 0x0000 00000 MOVD (R2), R0 0x0006 00006 MOVD 8(R2), R2 Shell cwd was reset to /home/claude/golang_go_78576
Let me see the branch instruction at the end of each wrapper.
Ran tools (2 Bash)
Bash cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/repro && export GOROOT=...
cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/repro && export GOROOT=/home/claude/golang_go_78576/go
for a in 386 amd64 arm arm64 loong64 mips mips64 ppc64le riscv64 s390x; do
printf "%-9s " "$a"
GOOS=linux GOARCH=$a $GOROOT/bin/go build -gcflags='-S' -o /dev/null . 2>&1 \
| sed -n '/main\.(\*outer)\.M(SB), DUPOK/,/^main\.[a-z]/p' | grep -E "^\s+0x" | sed 's/([^)]*)//' \
| grep -vE "FUNCDATA|TEXT" | tail -3 | sed 's/^\s*//' | tr '\n' ' | '
echo
doneShow tail-call branch per arch
600000
386 0x0050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0x0060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0x0070 00 00 00 00 .... amd64 0x0060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0x0070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0x0080 00 00 00 00 00 00 00 00 ........ arm 0x0050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0x0060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0x0070 00 00 00 00 .... arm64 0x0060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0x0070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0x0080 00 00 00 00 00 00 00 00 ........ loong64 0x0060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0x0070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0x0080 00 00 00 00 00 00 00 00 ........ mips 0x0050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0x0060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0x0070 00 00 00 00 .... mips64 0x0060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0x0070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0x0080 00 00 00 00 00 00 00 00 ........ ppc64le 0x0060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0x0070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0x0080 00 00 00 00 00 00 00 00 ........ riscv64 0x0060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0x0070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0x0080 00 00 00 00 00 00 00 00 ........ s390x 0x0060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0x0070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0x0080 00 00 00 00 00 00 00 00 ........ Shell cwd was reset to /home/claude/golang_go_78576
Bash cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/repro && export GOROOT=...
cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/repro && export GOROOT=/home/claude/golang_go_78576/go
for a in 386 amd64 arm arm64 loong64 mips mips64 ppc64le riscv64 s390x; do
printf "%-9s " "$a"
GOOS=linux GOARCH=$a $GOROOT/bin/go build -gcflags='-S' -o /dev/null . 2>&1 \
| sed -n '/main\.(\*outer)\.M(SB), DUPOK/,/^main\.[a-z]/p' \
| grep -E "^ 0x[0-9a-f]+ [0-9]{5} " | sed 's/([^)]*)//' | grep -vE "FUNCDATA|TEXT|PCDATA" \
| tail -2 | sed 's/^\s*0x[0-9a-f]* [0-9]* *//' | tr '\n' '@' | sed 's/@/ || /g'
echo
doneShow last two instructions of each wrapper
600000
386 CALL runtime.morestack_noctxt(SB) || JMP 0 || amd64 MOVQ 24(CX), CX || JMP CX || arm MOVW R1, 4(R0) || JMP (R2) || arm64 MOVD 24(R1), R1 || RET (R1) || loong64 MOVV 24(R5), R5 || JMP (R5) || mips JMP (R3) || NOOP || mips64 JMP (R3) || NOOP || ppc64le MOVD R12, CTR || JMP CTR || riscv64 MOV 24(X9), X9 || JALR X0, X9 || s390x MOVD R0, R1 || JMP R1 || Shell cwd was reset to /home/claude/golang_go_78576
Decisive: every other arch branches through a general-purpose register. Let me confirm the save lists and check the two loose ends (386, arm64's RET (R1) lowering).
Ran tools (5 Bash)
Bash cd /home/claude/golang_go_78576/go && echo "=== riscv64 saved X regs ==="; grep -oE "^ MOV X[0-9]+, " src/runtime/preemp...
cd /home/claude/golang_go_78576/go && echo "=== riscv64 saved X regs ==="; grep -oE "^ MOV X[0-9]+, " src/runtime/preempt_riscv64.s | sort -uV | tr '\n' ' '; echo; echo "=== arm LR + resume tail ==="; head -6 src/runtime/preempt_arm.s | tail -3; tail -6 src/runtime/preempt_arm.s; echo "=== arm64 RET (Rx) lowering ==="; cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/asmtest && printf 'TEXT ·tail(SB),4|512,$0-0\nMOVD $0, R1\nRET (R1)\n' > a64.s && GOROOT=/home/claude/golang_go_78576/go GOARCH=arm64 GOOS=linux /home/claude/golang_go_78576/go/bin/go tool asm -S -o a64.o a64.s 2>&1 | grep -E "0x[0-9a-f]+ [0-9]{5}" | grep -v FUNCDATACheck riscv save list, arm resume, arm64 RET lowering
=== riscv64 saved X regs === === arm LR + resume tail === #include "textflag.h" TEXT ·asyncPreempt(SB),NOSPLIT|NOFRAME,$0-0 MOVW 12(R13), R2 MOVW 8(R13), R1 MOVW 4(R13), R0 MOVW 188(R13), R14 MOVW.P 192(R13), R15 UNDEF === arm64 RET (Rx) lowering === 0x0000 00000 (a64.s:1) TEXT <unlinkable>.tail(SB), NOSPLIT|LEAF|NOFRAME, $0-0 0x0000 00000 (a64.s:2) MOVD ZR, R1 0x0004 00004 (a64.s:3) RET (R1) Shell cwd was reset to /home/claude/golang_go_78576
Bash cd /home/claude/golang_go_78576/go && head -12 src/runtime/preempt_riscv64.s | tail -8; echo "..."; grep -oE "\bX[0-9]+\...
cd /home/claude/golang_go_78576/go && head -12 src/runtime/preempt_riscv64.s | tail -8; echo "..."; grep -oE "\bX[0-9]+\b" src/runtime/preempt_riscv64.s | sort -uV | tr '\n' ' '; echo; echo "=== 386 wrapper tail ==="; cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/repro && GOOS=linux GOARCH=386 GOROOT=/home/claude/golang_go_78576/go /home/claude/golang_go_78576/go/bin/go build -gcflags='-S' -o /dev/null . 2>&1 | sed -n '/main\.(\*outer)\.M(SB), DUPOK/,/^main\.[a-z]/p' | grep -E "^ 0x[0-9a-f]+ [0-9]{5} " | sed 's/([^)]*)//' | grep -vE "FUNCDATA"riscv save list and 386 wrapper
TEXT ·asyncPreempt(SB),NOSPLIT|NOFRAME,$0-0 MOV X1, -464(X2) SUB $464, X2 MOV X5, 8(X2) MOV X6, 16(X2) MOV X7, 24(X2) MOV X8, 32(X2) ... X1 X2 X5 X6 X7 X8 X9 X10 X11 X12 X13 X14 X15 X16 X17 X18 X19 X20 X21 X22 X23 X24 X25 X26 X28 X29 X30 X31 === 386 wrapper tail === 0x0000 00000 TEXT main.(*outer).M(SB), DUPOK|NOFRAME|ABIInternal, $0-8 0x0000 00000 MOVL TLS, CX 0x0007 00007 PCDATA $0, $-2 0x0007 00007 MOVL (CX)(TLS*2), CX 0x000d 00013 PCDATA $0, $-1 0x000d 00013 CMPL SP, 8(CX) 0x0010 00016 PCDATA $0, $-2 0x0010 00016 JLS 38 0x0012 00018 PCDATA $0, $-1 0x0012 00018 LEAL +4(FP), AX 0x0016 00022 MOVL main.~p0+4(SP), CX 0x001a 00026 MOVL (CX), DX 0x001c 00028 MOVL 4(CX), CX 0x001f 00031 MOVL 12(DX), DX 0x0022 00034 MOVL CX, (AX) 0x0024 00036 PCDATA $1, $0 0x0024 00036 JMP DX 0x0026 00038 NOP 0x0026 00038 PCDATA $1, $-1 0x0026 00038 PCDATA $0, $-2 0x0026 00038 CALL runtime.morestack_noctxt(SB) 0x002b 00043 PCDATA $0, $-1 0x002b 00043 JMP 0 Shell cwd was reset to /home/claude/golang_go_78576
Bash cd /home/claude/golang_go_78576/go && for f in arm64/asm7.go mips/asm0.go loong64/asm.go riscv/obj.go s390x/asmz.go; do ...
cd /home/claude/golang_go_78576/go && for f in arm64/asm7.go mips/asm0.go loong64/asm.go riscv/obj.go s390x/asmz.go; do echo "===== $f"; sed -n "$(grep -n 'func.*isUnsafePoint' src/cmd/internal/obj/$f | head -1 | cut -d: -f1),+14p" src/cmd/internal/obj/$f; done 2>/dev/null | head -80
Compare isUnsafePoint implementations
===== arm64/asm7.go
func (c *ctxt7) isUnsafePoint(p *obj.Prog) bool {
// If p explicitly uses REGTMP, it's unsafe to preempt, because the
// preemption sequence clobbers REGTMP.
return p.From.Reg == REGTMP || p.To.Reg == REGTMP || p.Reg == REGTMP ||
p.From.Type == obj.TYPE_REGREG && p.From.Offset == REGTMP ||
p.To.Type == obj.TYPE_REGREG && p.To.Offset == REGTMP
}
// isRestartable returns whether p is a multi-instruction sequence that,
// if preempted, can be restarted.
func (c *ctxt7) isRestartable(p *obj.Prog) bool {
if c.isUnsafePoint(p) {
return false
}
// If p is a multi-instruction sequence with uses REGTMP inserted by
===== mips/asm0.go
func (c *ctxt0) isUnsafePoint(p *obj.Prog) bool {
// If p explicitly uses REGTMP, it's unsafe to preempt, because the
// preemption sequence clobbers REGTMP.
return p.From.Reg == REGTMP || p.To.Reg == REGTMP || p.Reg == REGTMP
}
// isRestartable returns whether p is a multi-instruction sequence that,
// if preempted, can be restarted.
func (c *ctxt0) isRestartable(p *obj.Prog) bool {
if c.isUnsafePoint(p) {
return false
}
// If p is a multi-instruction sequence with uses REGTMP inserted by
// the assembler in order to materialize a large constant/offset, we
// can restart p (at the start of the instruction sequence), recompute
===== loong64/asm.go
func (c *ctxt0) isUnsafePoint(p *obj.Prog) bool {
// If p explicitly uses REGTMP, it's unsafe to preempt, because the
// preemption sequence clobbers REGTMP.
return p.From.Reg == REGTMP || p.To.Reg == REGTMP || p.Reg == REGTMP
}
// isRestartable returns whether p is a multi-instruction sequence that,
// if preempted, can be restarted.
func (c *ctxt0) isRestartable(p *obj.Prog) bool {
if c.isUnsafePoint(p) {
return false
}
// If p is a multi-instruction sequence with uses REGTMP inserted by
// the assembler in order to materialize a large constant/offset, we
// can restart p (at the start of the instruction sequence), recompute
===== riscv/obj.go
func isUnsafePoint(p *obj.Prog) bool {
return p.Mark&USES_REG_TMP == USES_REG_TMP || p.From.Reg == REG_TMP || p.To.Reg == REG_TMP || p.Reg == REG_TMP
}
func ParseSuffix(prog *obj.Prog, cond string) (err error) {
cond = strings.TrimPrefix(cond, ".")
switch prog.As {
case AFCVTWS, AFCVTLS, AFCVTWUS, AFCVTLUS, AFCVTWD, AFCVTLD, AFCVTWUD, AFCVTLUD:
prog.Scond, err = rmSuffixEncode(cond)
case AFENCE:
if cond == "TSO" {
prog.Scond = fenceTsoSuffixBit
} else {
err = fmt.Errorf("unrecognized suffix .%q", cond)
}
===== s390x/asmz.go
func (c *ctxtz) isUnsafePoint(p *obj.Prog) bool {
if p.From.Reg == REGTMP || p.To.Reg == REGTMP || p.Reg == REGTMP {
return true
}
for _, a := range p.RestArgs {
if a.Reg == REGTMP {
return true
}
}
return p.Mark&USETMP != 0
}
func isint32(v int64) bool {
return int64(int32(v)) == v
}Bash echo "=== riscv: is X31 (REG_TMP) saved? ==="; grep -nE "^ MOV X31|^ MOV [0-9]+\(X2\), X31" src/runtime/preempt_riscv64....
echo "=== riscv: is X31 (REG_TMP) saved? ==="; grep -nE "^ MOV X31|^ MOV [0-9]+\(X2\), X31" src/runtime/preempt_riscv64.s; grep -n "REG_TMP =" src/cmd/internal/obj/riscv/cpu.go echo "=== mips HI/LO ==="; grep -nE "HI|LO" src/runtime/preempt_mips64x.s echo "=== x86 flags ==="; grep -nE "PUSHF|POPF" src/runtime/preempt_amd64.s src/runtime/preempt_386.s echo "=== arm64 RET (R1) encoding ==="; cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/asmtest && GOROOT=/home/claude/golang_go_78576/go GOARCH=arm64 GOOS=linux /home/claude/golang_go_78576/go/bin/go tool asm -S -o a64.o a64.s 2>&1 | tail -2
Final cross-checks
=== riscv: is X31 (REG_TMP) saved? === 302: USES_REG_TMP = 1 << iota === mips HI/LO === 36: MOVV HI, R1 38: MOVV LO, R1 114: MOVV R1, LO 116: MOVV R1, HI === x86 flags === src/runtime/preempt_386.s:7: PUSHFL src/runtime/preempt_386.s:46: POPFL src/runtime/preempt_amd64.s:12: PUSHFQ src/runtime/preempt_amd64.s:230: POPFQ === arm64 RET (R1) encoding === 0x0004 00004 (a64.s:3) RET (R1) 0x0000 e1 03 1f aa 20 00 5f d6 00 00 00 00 00 00 00 00 .... ._......... Shell cwd was reset to /home/claude/golang_go_78576
Bash cd /home/claude/golang_go_78576/go && echo "riscv X31 occurrences:"; grep -n "X31" src/runtime/preempt_riscv64.s; echo "...
cd /home/claude/golang_go_78576/go && echo "riscv X31 occurrences:"; grep -n "X31" src/runtime/preempt_riscv64.s; echo "riscv REG_TMP:"; grep -n "REG_TMP\s*=" src/cmd/internal/obj/riscv/cpu.go; echo "=== wasm ==="; cat src/runtime/preempt_wasm.s
Confirm riscv X31 and wasm
riscv X31 occurrences: 125: MOV (X2), X31 127: JMP (X31) riscv REG_TMP: 186: REG_TMP = REG_T6 // Reserved for assembler use. 302: USES_REG_TMP = 1 << iota === wasm === // Code generated by mkpreempt.go; DO NOT EDIT. #include "go_asm.h" #include "textflag.h" TEXT ·asyncPreempt(SB),NOSPLIT|NOFRAME,$0-0 // No async preemption on wasm UNDEF
riscv64's resume tail is MOV (X2), X31; JMP (X31) — X31 is REG_TMP. Let me check the rest.
Ran tools (1 Bash)
Bash for f in 386 amd64 arm arm64 loong64 mips64x riscv64 s390x ppc64x; do printf "%-9s " "$f"; tail -4 src/runtime/preempt_$...
for f in 386 amd64 arm arm64 loong64 mips64x riscv64 s390x ppc64x; do printf "%-9s " "$f"; tail -4 src/runtime/preempt_$f.s | grep -v UNDEF | tr '\n' ';' | sed 's/\t/ /g'; echo; done; echo; echo "REGTMP per arch:"; grep -hn "REGTMP\s*=" src/cmd/internal/obj/{arm,arm64,mips,loong64,s390x,ppc64}/cpu*.go 2>/dev/null | headResume sequence of every arch
386 MOVL 0(SP), AX; ADJSP $-156; POPFL; RET; amd64 ADJSP $-112; POPFQ; POPQ BP; RET; arm MOVW 4(R13), R0; MOVW 188(R13), R14; MOVW.P 192(R13), R15; arm64 MOVD -8(RSP), R29; MOVD (RSP), R27; ADD $256, RSP; RET (R27); loong64 MOVV 232(R3), R1; MOVV (R3), R30; ADDV $240, R3; JMP (R30); mips64x MOVV 488(R29), R31; MOVV (R29), R23; ADDV $496, R29; JMP (R23); riscv64 MOV 464(X2), X1; MOV (X2), X31; ADD $472, X2; JMP (X31); s390x MOVWZ -248(R15), R10; TMLH R10, $(3<<12); MOVD -256(R15), R10; JMP (R10); ppc64x MOVD R31, CTR; MOVD 32(R1), R31; ADD $552, R1; JMP (CTR); REGTMP per arch: 230: REGTMP = REG_R30 // used by the assembler
One more sweep to make sure ppc64 has no other unmarked CTR use.
Ran tools (2 Bash)
Bash echo "=== all REG_CTR / BDNZ uses in ppc64 backends ==="; grep -rn "REG_CTR\|ABDNZ\|ABC\b" src/cmd/internal/obj/ppc64/*....
echo "=== all REG_CTR / BDNZ uses in ppc64 backends ==="; grep -rn "REG_CTR\|ABDNZ\|ABC\b" src/cmd/internal/obj/ppc64/*.go src/cmd/compile/internal/ppc64/*.go | grep -v "_test" | grep -vE "^\S+:\s*//" | head -20
Sweep for remaining CTR uses
=== all REG_CTR / BDNZ uses in ppc64 backends ===
src/cmd/internal/obj/ppc64/a.out.go:285: REG_CTR = REG_SPR0 + 9
src/cmd/internal/obj/ppc64/a.out.go:326: PPC64DWARFRegisters[REG_CTR] = 66
src/cmd/internal/obj/ppc64/a.out.go:483: ABC
src/cmd/internal/obj/ppc64/a.out.go:493: ABDNZ // Decrement CTR, and branch if CTR != 0
src/cmd/internal/obj/ppc64/list9.go:86: case REG_CTR:
src/cmd/compile/internal/ppc64/ssa.go:1073: p.To.Reg = ppc64.REG_CTR
src/cmd/compile/internal/ppc64/ssa.go:1129: p = s.Prog(ppc64.ABC)
src/cmd/compile/internal/ppc64/ssa.go:1278: p.To.Reg = ppc64.REG_CTR
src/cmd/compile/internal/ppc64/ssa.go:1329: p = s.Prog(ppc64.ABC)
src/cmd/compile/internal/ppc64/ssa.go:1437: p.To.Reg = ppc64.REG_CTR
src/cmd/compile/internal/ppc64/ssa.go:1508: p = s.Prog(ppc64.ABC)
src/cmd/compile/internal/ppc64/ssa.go:1669: p.To.Reg = ppc64.REG_CTR
src/cmd/compile/internal/ppc64/ssa.go:1755: p = s.Prog(ppc64.ABC)
src/cmd/internal/obj/ppc64/obj9.go:757: case ABC,
src/cmd/internal/obj/ppc64/obj9.go:981: p.To.Reg = REG_CTR
src/cmd/internal/obj/ppc64/obj9.go:982: retReg = REG_CTR
src/cmd/internal/obj/ppc64/asm9.go:294: {as: ABC, a1: C_U15CON, a2: C_CRBIT, a6: C_BRA, type_: 16, size: 4}, // bc bo, bi, label
src/cmd/internal/obj/ppc64/asm9.go:295: {as: ABC, a1: C_U15CON, a2: C_CRBIT, a6: C_LR, type_: 18, size: 4}, // bclr bo, bi
src/cmd/internal/obj/ppc64/asm9.go:296: {as: ABC, a1: C_U15CON, a2: C_CRBIT, a3: C_U15CON, a6: C_LR, type_: 18, size: 4}, // bclr bo, bi, bh
src/cmd/internal/obj/ppc64/asm9.go:297: {as: ABC, a1: C_U15CON, a2: C_CRBIT, a6: C_CTR, type_: 18, size: 4}, // bcctr bo, biBash grep -n "REGTMP\s*=\|REGTMP =" src/cmd/internal/obj/arm/a.out.go src/cmd/internal/obj/arm64/a.out.go src/cmd/internal/ob...
grep -n "REGTMP\s*=\|REGTMP =" src/cmd/internal/obj/arm/a.out.go src/cmd/internal/obj/arm64/a.out.go src/cmd/internal/obj/mips/a.out.go src/cmd/internal/obj/s390x/a.out.go src/cmd/internal/obj/ppc64/a.out.go 2>/dev/null
Confirm REGTMP per arch
src/cmd/internal/obj/mips/a.out.go:236: REGTMP = REG_R23 /* used by the linker */ src/cmd/internal/obj/arm/a.out.go:99: REGTMP = REG_R11 src/cmd/internal/obj/arm/a.out.go:109: FREGTMP = REG_F15 src/cmd/internal/obj/ppc64/a.out.go:300: REGTMP = REG_R31 /* used by the linker */ src/cmd/internal/obj/s390x/a.out.go:144: REGTMP = REG_R10 // scratch register used in the assembler and linker src/cmd/internal/obj/arm64/a.out.go:284: REGTMP = REG_R27 // reserved for liblink
No — ppc64/ppc64le is the only one. The audit is clean, and the reason it's unique is structural.
The general invariant
asyncPreempt's resume tail has to get the resume PC into something to branch back. Whatever register that is gets clobbered, so it must never be live in compiled code at a safe point. Here's what each arch burns:
| arch | resume tail | burns | mitigation |
|---|---|---|---|
| 386 | POPFL; RET |
— (stack) | none needed |
| amd64 | POPFQ; POPQ BP; RET |
— (stack) | none needed |
| arm | MOVW.P 192(R13), R15 |
— (writes PC directly) | none needed |
| arm64 | MOVD (RSP), R27; RET (R27) |
R27 = REGTMP | MarkUnsafePoints |
| loong64 | MOVV (R3), R30; JMP (R30) |
R30 = REGTMP | MarkUnsafePoints |
| mips/mips64 | MOVV (R29), R23; JMP (R23) |
R23 = REGTMP | MarkUnsafePoints |
| riscv64 | MOV (X2), X31; JMP (X31) |
X31 = REG_TMP | MarkUnsafePoints |
| s390x | MOVD -256(R15), R10; JMP (R10) |
R10 = REGTMP | MarkUnsafePoints |
| ppc64 | MOVD R31, CTR; JMP (CTR) |
CTR | none |
| wasm | UNDEF |
— (no async preemption) | n/a |
Every register-branching arch burns REGTMP, and each one mitigates with the identical predicate — verbatim in arm64, mips, and loong64:
// If p explicitly uses REGTMP, it's unsafe to preempt, because the preemption sequence clobbers REGTMP.
ppc64's REGTMP is R31, and asyncPreempt does save and restore R31. So ppc64 legitimately concluded it didn't need the REGTMP marking — and it's one of only two arches that never calls obj.MarkUnsafePoints. But its resume burns CTR, which is exactly the register its own tail-call lowering leaves live. The mitigation everyone else has was REGTMP-shaped, so it didn't apply, and nothing else covered it.
ARM, the other arch with no marking
Safe for a different reason: it needs no scratch at all. MOVW.P 192(R13), R15 loads the resume PC straight into PC while popping the frame, and LR is restored from the pushCall slot the instruction before. Nothing is burned, so nothing needs marking. Its REGTMP (R11) is saved along with R0–R9 and R12.
The other hazard class
A dedicated branch register isn't the only way to have a special register live across an instruction boundary. The main other case is MIPS HI/LO — live between MULT and MFLO — and it's handled: preempt_mips64x.s saves and restores both. Likewise every flags/FP-status register that can be live is saved: ARM CPSR/FPCR, arm64 NZCV/FPSR, mips FCR31, loong64 FCC0-7/FCSR, ppc64 CR/XER/FPSCR, x86 via PUSHFL/PUSHFQ. riscv64 has none of these to save. Things like MXCSR and the RISC-V rounding mode aren't live values — Go sets them once at startup.
ppc64 is now fully covered
Sweeping every REG_CTR write in both ppc64 backends: four in cmd/compile/internal/ppc64/ssa.go (the bdnz counters in LoweredZero/LoweredMove/LoweredQuad*, all already unsafePoint: true in _gen/PPC64Ops.go) and one in obj9.go:981 — the tail call, fixed in 92ad37fc5f.
One design note, worth about a line in the CL: the predicate everywhere is spelled as "uses REGTMP" rather than "uses a register asyncPreempt doesn't preserve." That's what let this sit undetected. TestTailCallUnsafePoint as written is ppc64-specific; if you want the stronger guard, the generalizable form is to assert per-arch that whatever register the resume tail branches through is never live in compiled output.
You Tue 25 Aug 2026 15:07:17
Is CTR just a name for R3?
Assistant Tue 25 Aug 2026 15:07:37
No — CTR is a genuinely separate register, not an alias for any GPR.
Ran tools (1 Bash)
Bash sed -n '270,300p' src/cmd/internal/obj/ppc64/a.out.go | grep -nE "REG_R0|REG_R31|REG_SPR0|REG_CTR|REG_LR|REG_XER|REG_R12...
sed -n '270,300p' src/cmd/internal/obj/ppc64/a.out.go | grep -nE "REG_R0|REG_R31|REG_SPR0|REG_CTR|REG_LR|REG_XER|REG_R12" ; echo "--- ranges ---"; grep -nE "^\s+REG_R0 |^\s+REG_F0 |^\s+REG_SPR0 " src/cmd/internal/obj/ppc64/a.out.go
Show ppc64 register numbering
12: REG_SPR0 = obj.RBasePPC64 + 1024 // first of 1024 registers 14: REG_XER = REG_SPR0 + 1 15: REG_LR = REG_SPR0 + 8 16: REG_CTR = REG_SPR0 + 9 18: REGZERO = REG_R0 /* set to zero */ 31: REGTMP = REG_R31 /* used by the linker */ --- ranges --- 49: REG_R0 = obj.RBasePPC64 + iota 281: REG_SPR0 = obj.RBasePPC64 + 1024 // first of 1024 registers
No. CTR is the PowerPC Count Register — SPR 9, one of the special-purpose registers, in a completely separate number space from the GPRs:
REG_R0 = obj.RBasePPC64 + iota // GPRs: +0 .. +31
REG_SPR0 = obj.RBasePPC64 + 1024 // first of 1024 registers
REG_XER = REG_SPR0 + 1
REG_LR = REG_SPR0 + 8
REG_CTR = REG_SPR0 + 9
It has its own move instructions (mtctr/mfctr, i.e. mtspr 9/mfspr 9) and can't be used as an ALU operand at all.
The thing that probably looked like aliasing is the wrapper's MOVD R12, CTR — R12 is a GPR holding the target address, and that instruction copies it into CTR. Two distinct registers. From the encodings we saw:
7d8903a6=mtctr r12(opcode 31, XO 467 =mtspr, spr=9)7c6903a6=mtctr r3— my asm test withRET (R3); only bits 6–10 differ4e800420=bctr(opcode 19, XO 528 =bcctr, BO=20 "always")
Two facts about CTR explain the whole bug:
- It's one of only two indirect-branch targets on PowerPC (LR is the other), hence the comment in
obj9.go: "Indirect branches can only go to REG_LR or REG_CTR." Every other Go architecture branches through a GPR, whichasyncPreemptsaves — that's why they're unaffected. - Its architectural purpose is as a loop counter (
bdnz= decrement CTR, branch if nonzero), which is why the ppc64LoweredZero/LoweredMovesequences also hold it live across instructions — and why those were already markedunsafePoint: true.
Incidentally R3 does have a role in that wrapper, just not this one: it's the first integer argument register in the ppc64 ABI, so it holds the receiver on entry — MOVD 8(R3), R3 replaces it with the embedded interface's data word for the callee. R12 is separate again: ELFv2 requires the target address there so the callee can compute its own TOC from the global entry point.