Skip to content

fix: save LR in arm64 non-G0 asmcall trampoline (root-cause of darwin/arm64 hang) - #193

Merged
lirenjie95 merged 2 commits into
masterfrom
fix-arm64-asmcall-trampoline
Sep 10, 2026
Merged

fix: save LR in arm64 non-G0 asmcall trampoline (root-cause of darwin/arm64 hang)#193
lirenjie95 merged 2 commits into
masterfrom
fix-arm64-asmcall-trampoline

Conversation

@lirenjie95

@lirenjie95 lirenjie95 commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

What

Root-caused and fixed the darwin/arm64 asmcall non-G0 trampoline hang found by task 4.3 CI (TestCallFuncP0 timing out on macos-latest), and extended CI so all supported OS/ISA combos get real trampoline coverage.

Root cause

The arm64 ASMCALL macro was CALL R8; RET — it never saved LR (x30). Unlike amd64, where return addresses live on the stack, on arm64 BLR R8 overwrites x30 with the trampoline's own return address (CallFuncP*+8). The C callee (a leaf) returns correctly to +8, but the trampoline's own RET then jumps to x30 — which still holds +8 — and spins forever jumping to itself.

Reproduced locally on macOS arm64 and confirmed with lldb: the hung thread's pc == lr == CallFuncP0+8 (the RET instruction), process state R (spinning, not blocked).

This is an arm64 ISA bug, not darwin-specific: linux/arm64 would hang identically — it was simply never covered (CI only ran the tests on linux/amd64). The G0 variants passed because G0ASMCALL already saves/restores x30. SP alignment, PAC/arm64e, and stack protectors were all ruled out.

Changes

  • asmcall/arm64.s: ASMCALL now saves/restores x30 around the call (and 16-byte-aligns SP per AAPCS), mirroring the G0 variant. amd64 is untouched.
  • asmcall/calltest/call_test.go, asmcall/calltest/calltest.go: build tags relaxed to (linux || darwin || windows) && (amd64 || arm64); comments updated with the root cause. macOS CI now runs the trampoline tests instead of compile/vet-only.
  • .github/workflows/ci.yml:
    • added an ubuntu-24.04-arm leg — the arm64 LR bug class was invisible to linux/amd64;
    • added a windows-latest (amd64) leg — amd64.s has a Win64 calling-convention path (args in CX/DX/R8 via GOOS_windows) no leg ever executed. mem-ring is unix-only and test/go depends on it, so this leg runs go test ./asmcall/... only; the runner image's MSYS2 mingw-w64 gcc covers the cgo-built calltest callees.
  • docs/ci.md: documented the new legs.

This supersedes the previously considered fallback of restricting the asm fast path to Linux — macOS and Windows keep the fast path.

Testing

  • Local macOS arm64 (Go 1.18.1): go test ./asmcall/... passes, including all 8 trampoline variants on darwin (previously hanging).
  • Stress: 200k iterations of CallFuncP{0..3} with GC()/Gosched() interleaved — pass.
  • gofmt -l clean; go vet -unsafeptr=false ./... clean.
  • Cross-compile checked: linux/arm64, linux/amd64, windows/amd64 all build.
  • The new ubuntu-24.04-arm, windows-latest and updated macos-latest legs validate on this PR's CI run.

…arm64

asmcall/arm64.s: the ASMCALL macro (CALL R8; RET) never saved LR (x30).
On arm64, BLR overwrites x30 with the trampoline's own return address,
so after the C callee returned, the trampoline's RET jumped to itself
and spun forever (observed as the darwin/arm64 hang found by task 4.3
CI; pc == lr == CallFuncP0+8 confirmed with lldb). It is an arm64 ISA
bug, not darwin-specific: linux/arm64 would hang identically but was
never covered by CI. Save/restore x30 (and 16-byte-align SP per AAPCS)
around the call, mirroring what the G0 variant already does. amd64 is
unaffected (return addresses live on the stack).

asmcall/calltest/call_test.go: gate tests (linux || darwin) instead of
linux-only; the root cause is fixed so darwin runs the trampoline tests.

.github/workflows/ci.yml: add an ubuntu-24.04-arm leg so both ISAs get
real trampoline coverage; update the macOS leg comment.

docs/ci.md: document the linux/arm64 leg and the updated macOS leg.
@codecov-commenter

codecov-commenter commented Sep 9, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.09%. Comparing base (7208be8) to head (380d22c).

Additional details and impacted files
@@           Coverage Diff           @@
##           master     #193   +/-   ##
=======================================
  Coverage   97.09%   97.09%           
=======================================
  Files          24       24           
  Lines        3955     3955           
=======================================
  Hits         3840     3840           
  Misses        115      115           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

asmcall/amd64.s already has a Win64 calling-convention path (arguments in
CX/DX/R8, selected via GOOS_windows), and amd64 CALL/RET is stack-based so
the arm64 LR bug class does not apply -- but no CI leg ever executed it.

asmcall/calltest: relax build tags to (linux || darwin || windows) &&
(amd64 || arm64) so the trampoline tests run on windows/amd64.

.github/workflows/ci.yml: add a windows-latest leg. mem-ring is unix-only
and test/go depends on it, so the leg runs go test ./asmcall/... only; the
runner image's MSYS2 mingw-w64 gcc covers the cgo-built calltest callees.

docs/ci.md: document the windows leg.
@lirenjie95
lirenjie95 merged commit 7b0ce7c into master Sep 10, 2026
8 checks passed
@lirenjie95
lirenjie95 deleted the fix-arm64-asmcall-trampoline branch September 10, 2026 10:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants