-
Notifications
You must be signed in to change notification settings - Fork 44
ARM64 asm uses reserved registers #46
Description
Background
In golang for arm64, the following registers are marked as reserved
- R18
- R27
- R28
- R29
- R30
The registers of note here are R27 and R29 as they are used by the mul macro (expanded in gfpMul).
Explanation
I'm not sure what the impact is of overwriting R27, but R29 (the frame pointer) gets written to the stack when calling another function after executing the mul code. The go runtime assumes that it is able to traverse the stack by repeatedly dereferencing the frame pointer. Some examples of functions that traverse the stack by following the frame pointer are:
- https://github.com/golang/go/blob/go1.24.1/src/runtime/mprof.go#L574-L618
- https://github.com/golang/go/blob/go1.24.1/src/runtime/tracestack.go#L251-L263
Because the frame pointer has been clobbered, these stack traversals can segfault.
Test
When run on arm64, the following test (added here) may produce a SIGSEGV: segmentation violation. The crash does not happen every time, as it requires lock contention inside the call to fmt.Print("").
t.Run("mul_fp_corruption", func(t *testing.T) {
// By enabling mutex profiling, the go runtime will traverse the stack
// to measure mutex operations when a mutex is unlocked when a goroutine
// is blocking on it.
runtime.SetMutexProfileFraction(1)
var wg sync.WaitGroup
wg.Add(testTimes)
for i := 0; i < testTimes; i++ {
// If multiple goroutines interact with a global sync pool, the
// goroutines may block on acquiring a lock. When that happens, the
// mutex profiler will traverse the stack.
go func() {
defer wg.Done()
a := togfP(randomGF(rand.Reader))
b := togfP(randomGF(rand.Reader))
c := &gfP{}
// Corrupt the Frame Pointer.
gfpMul(c, a, b)
// Calling another function stores the FP on the stack.
//
// Additionally, Print ends up interacting with a global sync
// pool which can cause this stack to be traversed.
fmt.Print("")
}()
}
wg.Wait()
})