#78576 (HTTP content-length flake)

/home/claude/golang_go_78576/go · Tue 25 Aug 2026 15:11:09 · 8 messages
10d2d89d-010c-44e4-8cf8-1c57aa7d3172

You Tue 25 Aug 2026 08:10:29

Triage and debug go.dev/issue/78576

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
command
ls -a && echo "---" && cat *.md 2>/dev/null | head -50
description
Inspect working directory
Result
.
..
env.sh
go
---
ToolSearch select:WebFetch,WebSearch
query
select:WebFetch,WebSearch
max_results
5
WebFetch https://go.dev/issue/78576
url
https://go.dev/issue/78576
prompt
Reproduce the full issue text: title, labels, author, the complete body including any code, and all comments verbatim.
Result
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
command
cat /home/claude/golang_go_78576/env.sh; echo "---"; ls /home/claude/golang_go_78576/go | head -40
description
Check env and go tree
Result
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
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.
Result
# 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...
command
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
description
Check go version
Result
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...
command
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'])"
description
Fetch issue via API
Result
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/
command
grep -rn "TestErrorContentLength" src/net/http/ 
description
Find the test
Result
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 " ...
command
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'])
"
description
Fetch issue comments
Result
==== 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 failure
Bash sed -n '7660,7740p' src/net/http/serve_test.go
command
sed -n '7660,7740p' src/net/http/serve_test.go
description
Read the test
Result
		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...
command
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 -2
description
Fetch build page
Result
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
command
grep -rn "func run(" src/net/http/clientserver_test.go | head; sed -n '1,120p' src/net/http/clientserver_test.go
description
Read clientserver test harness
Result
// 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...
command
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.json
description
Query buildbucket for build details
Result
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_78576
Bash cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad && tail -c +6 gb.json >...
command
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'))
"
description
Inspect failing steps
Result
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...
command
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.txt
description
Fetch dist test output log
Result
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...
command
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
description
Grep for failures in output log
Result
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...
command
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"
description
Fetch dist.testjson
Result
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' ...
command
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.txt
description
Extract net/http output
Result
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...
command
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
description
Find panic in net/http output
Result
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_78576
Bash cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad && sed -n '5100,5112p' ...
command
cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad && sed -n '5100,5112p' nethttp.txt
description
Show the timeout panic header
Result
--- 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_78576

This 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' ...
command
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
description
Extract and enumerate goroutines
Result
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...
command
cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad && sed -n '173,280p' stack.txt
description
Print the interesting goroutines
Result
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_78576

Now 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) ...
command
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
description
Locate dialCtx in current tree
Result
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
command
git log --oneline -5 b1c8857f 2>&1 | head
description
Check if failure commit is present locally
Result
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
command
sed -n '240,300p' src/net/dial.go && echo "===== dialCtx =====" && sed -n '520,610p' src/net/dial.go
description
Read Dialer.deadline and dialCtx
Result
		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
command
grep -rn "testHookStepTime" src/ | head -20
description
Find testHookStepTime
Result
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...
command
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
description
Inspect net/http log head
Result
=== 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_78576

Strong 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' ...
command
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)
EOF
description
Timeline of the subtest events
Result
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_78576
Bash cd /home/claude/golang_go_78576/go && git log --oneline -8 --since=2026-01-01 -- src/net/dial.go
command
cd /home/claude/golang_go_78576/go && git log --oneline -8 --since=2026-01-01 -- src/net/dial.go
description
Recent changes to net/dial.go
Result
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' ...
command
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)
EOF
description
Find last completed net/http test
Result
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_78576
Bash git log -1 --format="%H %ad %s" 136332571b && git show --stat 136332571b | tail -5
command
git log -1 --format="%H %ad %s" 136332571b && git show --stat 136332571b | tail -5
description
Check cancel2 shadow fix
Result
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' ...
command
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)
EOF
description
List tests that never completed
Result
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_78576
Bash cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad && python3 - <<'EOF' ...
command
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])
EOF
description
Event rate histogram + TestOmitHTTP2Vet timing
Result
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_78576

Only 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
command
sed -n '290,340p' src/runtime/preempt.go
description
Read asyncPreempt2
Result
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 -...
command
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
description
Read goschedImpl
Result
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 ...
command
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
description
Read asyncPreempt2 and goschedImpl
Result
//
// 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 runnext
Bash sed -n "$(grep -n 'func goschedImpl' src/runtime/proc.go | cut -d: -f1),+60p" src/runtime/proc.go | tail -40
command
sed -n "$(grep -n 'func goschedImpl' src/runtime/proc.go | cut -d: -f1),+60p" src/runtime/proc.go | tail -40
description
Rest of goschedImpl
Result
	}

	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:systemstack

This 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...
command
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" 
description
Inspect procresize work detection
Result
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...
command
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"
description
Read runqempty and startTheWorld wakeup logic
Result
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...
command
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'])
"
done
description
Search Go tracker for related issues
Result
=== 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.....
command
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; done
description
Search for ppc64le power10 flake issues in 2026
Result
=== 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 ...
command
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
description
Fetch related flake issue bodies
Result
===== 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...
command
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 8687055014685645281
description
Probe issue 78199 build for same signature
Result
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_78576
Bash cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad && sed -i 's|-d "{\\"id...
command
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 8687055014685645281
description
Fix accept header and retry probe
Result
#!/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_78576
Bash cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad && cat > probe2.py <<'P...
command
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 8687055014685645281
description
Probe 78199 build properly
Result
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_78576

Identical 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 ===" ...
command
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
description
Read asyncPreempt2 tail and xReg code
Result
//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.s
Bash cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad && for b in 86843197859...
command
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
description
Probe remaining sibling builds
Result
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
command
cd /home/claude/golang_go_78576/go && ls bin && ./bin/go version 2>&1 | head -2
description
Check for built toolchain
Result
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
command
cd /home/claude/golang_go_78576/go && sed -n '1,90p' src/runtime/preempt_ppc64x.s
description
Read preempt_ppc64x.s
Result
// 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 ...
command
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.ppc64le
description
Disassemble the autogenerated wrapper for ppc64le
Result
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...
command
cd /home/claude/golang_go_78576/go && grep -n "MarkUnsafePoints\|isUnsafePoint\|isRestartable" src/cmd/internal/obj/ppc64/obj9.go | head
description
Check ppc64 unsafe point marking
Result
(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...
command
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
description
Check whether asyncPreempt saves CTR on ppc64
Result
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...
command
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
description
Read asyncPreempt resume tail
Result
	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...
command
grep -rn "unsafePoint\|UnsafePoint\|unsafeMark" src/cmd/internal/obj/ppc64/*.go | head -20; echo "=== generic ==="; grep -rn "func MarkUnsafePoints" src/cmd/internal/obj/*.go
description
Look for ppc64 unsafe-point marking
Result
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
command
grep -n "REG_CTR\|ACTR\|CTR" src/cmd/internal/obj/ppc64/obj9.go | head -40
description
Find CTR handling in ppc64 obj
Result
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
command
grep -n "CALLtail\|BR (CTR)\|ABR\|obj.ARET" src/cmd/compile/internal/ppc64/ssa.go | head -20
description
Find tail call emission
Result
1875:	case ssaop.OpPPC64CALLtail, ssaop.OpPPC64CALLtailinter:
2096:		s.Prog(obj.ARET)
Bash sed -n '945,1010p' src/cmd/internal/obj/ppc64/obj9.go
command
sed -n '945,1010p' src/cmd/internal/obj/ppc64/obj9.go
description
Read the RET/tail-call lowering
Result
			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.Pos
Bash sed -n "$(grep -n 'func isAsyncSafePoint' src/runtime/preempt.go | cut -d: -f1),+60p" src/runtime/preempt.go
command
sed -n "$(grep -n 'func isAsyncSafePoint' src/runtime/preempt.go | cut -d: -f1),+60p" src/runtime/preempt.go
description
Read isAsyncSafePoint
Result
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...
command
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
description
Dump compiler assembly for the wrapper
Result
	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...
command
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
description
Check PCDATA index constants
Result
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_UnsafePoint

The 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...
command
sed -n '124,145p' src/internal/abi/symtab.go; echo "=== restart usages ==="; grep -rn "UnsafePointRestart" src/cmd/internal/obj/ | grep -v "_test" | head
description
Read UnsafePointRestart semantics
Result
// 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 <<'...
command
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 -24
description
Check PCDATA around an indirect call
Result
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...
command
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
description
Retry indirect call dump
Result
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...
command
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
description
Show PCDATA 0 and CTR sequence in indirect call
Result
	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...
command
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
description
Verify the second wrapper shape
Result
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
command
which qemu-ppc64le qemu-ppc64le-static 2>&1; ls /usr/bin/qemu-* 2>/dev/null | head
description
Check for qemu ppc64le
Result
/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/...
command
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.ppc64le
description
Build ppc64le reproducer and check its CTR sequence
Result
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 ...
command
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.ppc64le
description
Build tail-call reproducer
Result
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...
command
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=$?"
description
Run reproducer under qemu-ppc64le
timeout
240000
Result
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"
command
qemu-ppc64le --version | head -2; qemu-ppc64le -h 2>&1 | grep -i "one-insn\|singlestep" 
description
Check qemu single-step options
Result
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 ...
command
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
description
Rebuild with longer deadline
Result
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...
command
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
description
Run reproducer with one-insn-per-tb
timeout
450000
Result
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...
command
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 -25
description
Get fatal traceback from reproducer
timeout
450000
Result
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...
command
cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/repro && sed -n '76,90p' out2.txt
description
Show the stuck goroutine stack
Result
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_78576
Bash cd /home/claude/golang_go_78576/go && grep -rn "REG_CTR" src/cmd/compile/internal/ppc64/*.go src/cmd/internal/obj/ppc64/...
command
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
description
Survey CTR uses in generated code
Result
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...
command
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
description
Inspect other CTR uses in the compiler
Result
===== 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 = p
Bash grep -n "LoweredZero\|LoweredMove\|LoweredQuadZero\|LoweredQuadMove\|unsafePoint" src/cmd/compile/internal/ssa/_gen/PPC6...
command
grep -n "LoweredZero\|LoweredMove\|LoweredQuadZero\|LoweredQuadMove\|unsafePoint" src/cmd/compile/internal/ssa/_gen/PPC64Ops.go | head -30
description
Check unsafePoint marking on CTR loops
Result
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
command
sed -n '960,1055p' src/cmd/internal/obj/ppc64/obj9.go
description
Read full ARET lowering
Result
			}

		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 = REGTMP
Bash sed -n '1055,1110p' src/cmd/internal/obj/ppc64/obj9.go
command
sed -n '1055,1110p' src/cmd/internal/obj/ppc64/obj9.go
description
Read rest of ARET non-leaf path
Result
				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...
command
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"
description
Patch ppc64 tail-call lowering
Result
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...
command
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
description
Rebuild compiler and reproducer with the fix
timeout
600000
Result
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...
command
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
description
Verify unsafe-point PCDATA now emitted
Result
	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...
command
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
description
Run fixed reproducer (10 min)
timeout
720000
Result
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...
command
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
description
Baseline: how fast unpatched build hangs
timeout
500000
Result
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
command
cat /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/tasks/bl60yurp3.output
description
Check fixed-build result
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...
command
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
description
Cross-build std+cmd for ppc64 and run obj tests
timeout
900000
Result
--- 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 | ...
command
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
description
Run compiler/obj tests
timeout
900000
Result
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...
command
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
description
Show the patch
Result
 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 ...
command
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" && ls
description
Finalize reproducer file
Result
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
caption
Reproducer: hangs in 2-3s under `qemu-ppc64le -one-insn-per-tb`, clean for 10min with the obj9.go fix.
display
attach
files
[
	"/tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/repro/main.go"
]
status
normal
Result
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 at asyncPreempt2, 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...
command
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
description
Find existing unsafe-point / async-preempt tests
Result
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...
command
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
description
List obj tests and maymorestack machinery
Result
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
command
wc -l runtime/unsafepoint_test.go && cat runtime/unsafepoint_test.go
description
Read runtime/unsafepoint_test.go
Result
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 -...
command
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
description
Check runtime.UnsafePoint helper
Result
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
command
head -60 cmd/internal/obj/ppc64/asm_test.go; echo "..."; grep -n "^func Test" cmd/internal/obj/ppc64/asm_test.go
description
Read ppc64 asm_test.go structure
Result
// 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
command
sed -n '324,400p' cmd/internal/obj/ppc64/asm_test.go
description
Read TestPCalign harness pattern
Result
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 == x
Bash sed -n '1870,1900p' src/cmd/compile/internal/ppc64/ssa.go 2>/dev/null || sed -n '1870,1900p' cmd/compile/internal/ppc64/...
command
sed -n '1870,1900p' src/cmd/compile/internal/ppc64/ssa.go 2>/dev/null || sed -n '1870,1900p' cmd/compile/internal/ppc64/ssa.go
description
See how CALLtail is emitted
Result
		}

	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 && ...
command
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
description
Test whether asm can express RET (Rx)
Result
<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...
command
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
description
Rebuild asm/link and retest
timeout
300000
Result
<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 <...
command
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"
description
Verify all three RET expansion paths
Result
(Bash completed with no output)
Bash cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/asmtest && GOROOT=/home...
command
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
description
See full asm output
Result
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_78576
Bash cd /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/asmtest && cat > t2.s <...
command
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"
description
Verify all three RET expansion paths
Result
<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-...
command
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 -20
description
Add and run the obj-level test
timeout
300000
Result
        	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
FAIL
Bash GOROOT=/home/claude/golang_go_78576/go ./bin/go test cmd/internal/obj/ppc64 -run TestTailCallUnsafePoint 2>&1 | grep -E ...
command
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
description
Show the assertion failure
Result
--- 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
FAIL

Index off-by-one in the field parsing — the disassembly lines have three leading columns.

Ran tools (1 Bash)
Bash python3 - <<'PY' ...
command
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 -5
description
Fix field indices and rerun
Result
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...
command
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
description
Verify test fails without the fix
timeout
300000
Result
=== 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
FAIL
Bash cp /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/obj9.go.fixed src/cmd/i...
command
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
description
Restore fix and confirm green
timeout
300000
Result
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/
command
grep -n -A10 "^type ITab" src/internal/abi/iface.go 2>/dev/null || grep -rn -A10 "^type ITab" src/internal/abi/
description
Check abi.ITab layout
Result
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...
command
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"
description
Add runtime end-to-end test
timeout
300000
Result
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...
command
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
description
Run runtime test under qemu-ppc64le
timeout
900000
Result
=== 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.228s
Bash cp /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/obj9.go.orig cmd/intern...
command
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
description
Verify runtime test fails without the fix
timeout
900000
Result
=== unpatched ===
--- FAIL: TestUnsafePointTailCall (1.99s)
        	branch through CTR must be marked unsafe, but isn't
FAIL
FAIL	runtime	2.097s
FAIL
Bash cp /tmp/claude-1003/-home-claude-golang-go-78576/10d2d89d-010c-44e4-8cf8-1c57aa7d3172/scratchpad/obj9.go.fixed cmd/inter...
command
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
description
Restore fix, confirm both tests green on ppc64le and amd64
timeout
900000
Result
ok  	runtime	0.160s
ok  	runtime	0.120s
Bash cd /home/claude/golang_go_78576/go && git diff --stat && git status --short
command
cd /home/claude/golang_go_78576/go && git diff --stat && git status --short
description
Summarize all changes
Result
 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...
command
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"
description
Verify obj9.go state
Result
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...
command
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
description
Investigate git state
Result
---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 "^+"
command
git show --stat 92ad37fc5f | head -8; git show 92ad37fc5f -- src/cmd/internal/obj/ppc64/obj9.go | grep -c "^+" 
description
Check the existing commit
Result
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(+)
17

Both 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-tb to 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 the LoweredMove/LoweredZero CTR loops and any future CTR use, rather than just the site we know about. I skipped it because go tool objdump over all of runtime.test runs into minutes. If you want it as a separately-tagged slow test, or pushed into cmd/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
command
grep -n "^func gen\|addSpecial(" src/runtime/mkpreempt.go
description
List per-arch generators and special registers
Result
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 -...
command
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
description
Inspect special-register handling per arch
Result
=========== 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...
command
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
description
Which special registers each arch saves
Result
=== 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
command
grep -rn "MarkUnsafePoints" cmd/internal/obj/*/*.go
description
Which arches mark assembler-generated unsafe points
Result
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...
command
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.go
description
Which GPRs do ARM and ARM64 save
Result
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=...
command
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
description
Disassemble the tail-call wrapper on every arch
timeout
600000
Result
===== 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=...
command
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
done
description
Show tail-call branch per arch
timeout
600000
Result
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=...
command
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
done
description
Show last two instructions of each wrapper
timeout
600000
Result
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...
command
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 FUNCDATA
description
Check riscv save list, arm resume, arm64 RET lowering
Result
=== 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]+\...
command
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"
description
riscv save list and 386 wrapper
Result
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 ...
command
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
description
Compare isUnsafePoint implementations
Result
===== 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....
command
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
description
Final cross-checks
Result
=== 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 "...
command
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
description
Confirm riscv X31 and wasm
Result
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_$...
command
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 | head
description
Resume sequence of every arch
Result
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/*....
command
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
description
Sweep for remaining CTR uses
Result
=== 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, bi
Bash 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...
command
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
description
Confirm REGTMP per arch
Result
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...
command
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
description
Show ppc64 register numbering
Result
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 with RET (R3); only bits 6–10 differ
  • 4e800420 = bctr (opcode 19, XO 528 = bcctr, BO=20 "always")

Two facts about CTR explain the whole bug:

  1. 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, which asyncPreempt saves — that's why they're unaffected.
  2. Its architectural purpose is as a loop counter (bdnz = decrement CTR, branch if nonzero), which is why the ppc64 LoweredZero/LoweredMove sequences also hold it live across instructions — and why those were already marked unsafePoint: 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.