vendor tsgo

This commit is contained in:
2026-07-09 16:50:43 -04:00
parent c06ea2e5a4
commit 98978e4930
5804 changed files with 1556156 additions and 101 deletions

View File

@@ -0,0 +1,151 @@
//go:build darwin && arm64
package fswatch
import (
"bytes"
"fmt"
"os/exec"
"path/filepath"
"regexp"
"sort"
"strings"
"testing"
)
// TestCallbackASMTouchesOnlySafeRegisters verifies that fsEventsCallbackASM
// (the C callback entered from CFRunLoop) only touches registers that are
// safe to clobber under AAPCS, i.e. registers the C caller doesn't expect
// to find unchanged after the call.
//
// We are entered from C (FSEvents -> CFRunLoopRun -> ... -> our callback)
// and never transition into Go ABI; we must therefore obey the standard
// arm64 AAPCS contract:
//
// Callee-saved (must be preserved across our call):
// R19-R28 (general) F8-F15 (float) R29 (FP) R30 (LR)
// Caller-saved (free to clobber):
// R0-R8 (args/return) F0-F7 (args/return)
// R9-R15 (scratch) F16-F31 (scratch)
// R16, R17 (IP0/IP1: linker trampoline scratch; caller-saved)
// Special / restricted:
// R18: platform register; reserved by darwin (do not touch)
// RSP: stack pointer (we manage)
// ZR: zero register (read-only constant)
// PC: program counter (read-only, appears in PC-relative addresses)
//
// We *do* touch R29 and R30, but only because we save the caller's value
// to the stack on entry and restore it on exit (R29 to set up our frame
// chain pointer; R30 because each of our BLs clobbers LR). Treating them
// as allowed in this test is correct so long as the prologue/epilogue
// continue to save/restore them; a bare reference without that bookkeeping
// would still be a bug, but a much more obvious one to spot in review.
//
// The motivating failure was a silent R27 (REGTMP) clobber from
// `MOVD ·sym(SB), Rn` pseudo-instruction expansion (cmd/internal/obj/
// arm64/a.out.go: REGTMP = REG_R27); FSEvents holds a CFAllocator pointer
// in R27 across our callback and uses it for CFRelease afterwards, so the
// clobber surfaces as a SIGSEGV inside objc_release deep in CFRunLoopRun.
// The crash is layout-sensitive and not reliably caught by the
// race-detector test suite alone, hence this static check.
//
// A whitelist (rather than a blacklist of "known dangerous" registers)
// guards against any future Go toolchain change that introduces a new
// kind of pseudo-instruction expansion using a register we hadn't
// previously thought to forbid: any unfamiliar register name in the
// disassembly will fail the test.
//
// If the asm is ever rewritten to use the save-and-restore strategy
// (mirroring runtime/cgo/abi_arm64.h's SAVE_R19_TO_R28 / RESTORE_R19_TO_R28),
// the safe set here will need to be extended to include R19-R28 (and the
// test should be supplemented with a check that the prologue/epilogue
// actually save and restore them).
func TestCallbackASMTouchesOnlySafeRegisters(t *testing.T) {
t.Parallel()
// `go test` (without -c) strips the test binary, so we can't disassemble
// it for symbol-level inspection. Build a fresh, unstripped copy.
bin := filepath.Join(t.TempDir(), "callback-disasm.test")
if out, err := exec.Command("go", "test", "-c", "-o", bin, ".").CombinedOutput(); err != nil {
t.Fatalf("go test -c failed: %v\n%s", err, out)
}
out, err := exec.Command("go", "tool", "objdump", "-s", "fsEventsCallbackASM", bin).CombinedOutput()
if err != nil {
t.Fatalf("go tool objdump failed: %v\n%s", err, out)
}
if len(out) == 0 {
t.Fatalf("go tool objdump produced no output; symbol fsEventsCallbackASM not found in %s", bin)
}
// Set of registers safe to touch when called from C.
safe := map[string]bool{
"RSP": true, "ZR": true, "ZRW": true, "PC": true,
// Frame/link registers: managed by our prologue/epilogue.
"R29": true, "R30": true,
}
// Caller-saved general-purpose: R0-R17.
for i := range 18 {
safe[fmt.Sprintf("R%d", i)] = true
}
// Caller-saved float: F0-F7 and F16-F31.
for i := range 8 {
safe[fmt.Sprintf("F%d", i)] = true
}
for i := 16; i <= 31; i++ {
safe[fmt.Sprintf("F%d", i)] = true
}
// Match register tokens of the form Rnn / Fnn / RSP / ZR / PC.
regToken := regexp.MustCompile(`\b([RF]\d+|RSP|RZR|ZR|PC)\b`)
// Each disassembly line looks like:
// fsevents_darwin_ffi_arm64.s:106\t0x10012b1e0\t\td10083ff\t\tSUB $32, RSP, RSP\t
// We only want to inspect the instruction text (the last tab-delimited
// non-empty field). The header line ("TEXT _fsEventsCallbackASM(SB) ...")
// is skipped.
type violation struct{ reg, line string }
var violations []violation
seen := map[string]bool{}
for raw := range strings.SplitSeq(string(out), "\n") {
line := strings.TrimRight(raw, " \t")
if line == "" || strings.HasPrefix(line, "TEXT ") {
continue
}
fields := strings.Split(line, "\t")
// Find the rightmost non-empty field: the instruction text.
var inst string
for i := len(fields) - 1; i >= 0; i-- {
if f := strings.TrimSpace(fields[i]); f != "" {
inst = f
break
}
}
if inst == "" {
continue
}
for _, m := range regToken.FindAllString(inst, -1) {
if safe[m] {
continue
}
if seen[m] {
continue
}
seen[m] = true
violations = append(violations, violation{reg: m, line: line})
}
}
if len(violations) > 0 {
sort.Slice(violations, func(i, j int) bool { return violations[i].reg < violations[j].reg })
var b bytes.Buffer
fmt.Fprintf(&b, "fsEventsCallbackASM touches register(s) the C caller (CFRunLoop/FSEvents) "+
"expects preserved or that are platform-reserved on darwin/arm64. The C ABI "+
"requires R19-R28, F8-F15 to be preserved across the call, and R18 to be left "+
"untouched. See the REGTMP hazard note in fsevents_darwin_ffi_arm64.s.\n")
for _, v := range violations {
fmt.Fprintf(&b, " %s first appears in: %s\n", v.reg, v.line)
}
t.Fatal(b.String())
}
}