vendor tsgo
This commit is contained in:
151
tools/tsgo/internal/fswatch/fsevents_darwin_ffi_arm64_test.go
Normal file
151
tools/tsgo/internal/fswatch/fsevents_darwin_ffi_arm64_test.go
Normal 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())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user