chore: refactor enosys detection and add unit tests

This commit is contained in:
blacktop
2026-05-03 09:24:18 -06:00
parent 29d37aca51
commit 498b0bd9bf
2 changed files with 123 additions and 29 deletions
+54 -29
View File
@@ -22,54 +22,79 @@ import (
"github.com/blacktop/ipsw/internal/download"
)
// isEnosys checks whether the function at callAddr is an enosys stub
// by reading its first instructions and matching against known patterns:
// - ARM64: [bti c;] mov w0, #0x4e; ret
// - x86_64: [endbr64;] push rbp; mov rbp, rsp; mov eax, 0x4e; pop rbp; ret
// enosysErrno is the value returned by the xnu enosys() stub: ENOSYS == 78.
const enosysErrno = 0x4E
// Instruction patterns emitted by clang for the enosys() leaf function and any
// other syscall whose body is `return ENOSYS;`. Patterns are matched as raw
// bytes so detection works without a disassembler.
//
// ARM64 leaf functions don't sign LR so plain `ret` is expected; arm64e kernels
// built with `-mbranch-protection=pac-ret+leaf` would emit `retab` instead and
// silently fall through here. Not currently observed on shipping kernels.
const (
arm64InsnLen = 4
arm64BtiC uint32 = 0xD503245F // bti c
arm64MovW0 uint32 = 0x528009C0 // mov w0, #0x4e (ENOSYS)
arm64Ret uint32 = 0xD65F03C0 // ret
)
var (
// push rbp; mov rbp, rsp; mov eax, 0x4e; pop rbp; ret
x86EnosysPlain = [11]byte{0x55, 0x48, 0x89, 0xE5, 0xB8, enosysErrno, 0x00, 0x00, 0x00, 0x5D, 0xC3}
// endbr64; push rbp; mov rbp, rsp; mov eax, 0x4e; pop rbp; ret
x86Endbr64 = [4]byte{0xF3, 0x0F, 0x1E, 0xFA}
)
// isEnosys reads the first few instructions at callAddr and returns true if
// they match a known enosys stub pattern. See arm64BtiC/x86EnosysPlain for the
// expected sequences.
func isEnosys(m *macho.File, callAddr uint64) bool {
switch m.FileTOC.FileHeader.CPU {
switch m.CPU {
case types.CPUArm64:
return isEnosysARM64(m, callAddr)
var buf [3 * arm64InsnLen]byte // up to: bti c + mov + ret
if _, err := m.ReadAtAddr(buf[:], callAddr); err != nil {
return false
}
return matchEnosysARM64(buf[:])
case types.CPUAmd64:
return isEnosysX86(m, callAddr)
var buf [len(x86Endbr64) + len(x86EnosysPlain)]byte
if _, err := m.ReadAtAddr(buf[:], callAddr); err != nil {
return false
}
return matchEnosysX86(buf[:])
default:
return false
}
}
func isEnosysARM64(m *macho.File, addr uint64) bool {
const (
arm64BtiC = 0xD503245F // bti c
arm64Mov = 0x528009C0 // mov w0, #0x4e
arm64Ret = 0xD65F03C0 // ret
)
var buf [12]byte // up to 3 instructions: bti c + mov + ret
if _, err := m.ReadAtAddr(buf[:], addr); err != nil {
// matchEnosysARM64 reports whether buf starts with the ARM64 enosys pattern
// `[bti c;] mov w0, #0x4e; ret`. buf must hold at least 3 instructions.
func matchEnosysARM64(buf []byte) bool {
if len(buf) < 3*arm64InsnLen {
return false
}
i0 := binary.LittleEndian.Uint32(buf[0:4])
i1 := binary.LittleEndian.Uint32(buf[4:8])
i2 := binary.LittleEndian.Uint32(buf[8:12])
if i0 == arm64Mov && i1 == arm64Ret {
if i0 == arm64MovW0 && i1 == arm64Ret {
return true
}
return i0 == arm64BtiC && i1 == arm64Mov && i2 == arm64Ret
return i0 == arm64BtiC && i1 == arm64MovW0 && i2 == arm64Ret
}
func isEnosysX86(m *macho.File, addr uint64) bool {
// push rbp; mov rbp, rsp; mov eax, 0x4e; pop rbp; ret
plain := [11]byte{0x55, 0x48, 0x89, 0xE5, 0xB8, 0x4E, 0x00, 0x00, 0x00, 0x5D, 0xC3}
// endbr64; push rbp; mov rbp, rsp; mov eax, 0x4e; pop rbp; ret
endbr64 := [4]byte{0xF3, 0x0F, 0x1E, 0xFA}
var buf [15]byte // endbr64 (4) + plain (11)
if _, err := m.ReadAtAddr(buf[:], addr); err != nil {
return false
}
if [11]byte(buf[0:11]) == plain {
// matchEnosysX86 reports whether buf starts with the x86_64 enosys pattern
// `[endbr64;] push rbp; mov rbp, rsp; mov eax, 0x4e; pop rbp; ret`.
func matchEnosysX86(buf []byte) bool {
if len(buf) >= len(x86EnosysPlain) && [11]byte(buf[:len(x86EnosysPlain)]) == x86EnosysPlain {
return true
}
return [4]byte(buf[0:4]) == endbr64 && [11]byte(buf[4:15]) == plain
prefixed := len(x86Endbr64) + len(x86EnosysPlain)
if len(buf) < prefixed {
return false
}
return [4]byte(buf[:len(x86Endbr64)]) == x86Endbr64 &&
[11]byte(buf[len(x86Endbr64):prefixed]) == x86EnosysPlain
}
//go:embed data/syscall.gz
+69
View File
@@ -0,0 +1,69 @@
package kernelcache
import "testing"
func TestMatchEnosysARM64(t *testing.T) {
// mov w0, #0x4e ; ret
plain := []byte{0xC0, 0x09, 0x80, 0x52, 0xC0, 0x03, 0x5F, 0xD6, 0x00, 0x00, 0x00, 0x00}
// bti c ; mov w0, #0x4e ; ret
bti := []byte{0x5F, 0x24, 0x03, 0xD5, 0xC0, 0x09, 0x80, 0x52, 0xC0, 0x03, 0x5F, 0xD6}
// pacibsp ; mov w0, #0x4e ; ret (something else)
pacib := []byte{0x7F, 0x23, 0x03, 0xD5, 0xC0, 0x09, 0x80, 0x52, 0xC0, 0x03, 0x5F, 0xD6}
// mov w1, #0x4e ; ret (wrong register)
wrongReg := []byte{0xC1, 0x09, 0x80, 0x52, 0xC0, 0x03, 0x5F, 0xD6, 0x00, 0x00, 0x00, 0x00}
// mov w0, #0x4f ; ret (wrong errno)
wrongErrno := []byte{0xE0, 0x09, 0x80, 0x52, 0xC0, 0x03, 0x5F, 0xD6, 0x00, 0x00, 0x00, 0x00}
cases := []struct {
name string
buf []byte
want bool
}{
{"plain mov+ret", plain, true},
{"bti+mov+ret", bti, true},
{"pacibsp prefix not matched", pacib, false},
{"wrong register w1", wrongReg, false},
{"wrong errno", wrongErrno, false},
{"too short", plain[:7], false},
{"empty", nil, false},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := matchEnosysARM64(tc.buf); got != tc.want {
t.Errorf("matchEnosysARM64(%x) = %v, want %v", tc.buf, got, tc.want)
}
})
}
}
func TestMatchEnosysX86(t *testing.T) {
// push rbp; mov rbp,rsp; mov eax,0x4e; pop rbp; ret
plain := []byte{0x55, 0x48, 0x89, 0xE5, 0xB8, 0x4E, 0x00, 0x00, 0x00, 0x5D, 0xC3, 0, 0, 0, 0}
// endbr64 ; <plain>
endbr := []byte{0xF3, 0x0F, 0x1E, 0xFA, 0x55, 0x48, 0x89, 0xE5, 0xB8, 0x4E, 0x00, 0x00, 0x00, 0x5D, 0xC3}
// wrong errno (0x4F)
wrongErrno := []byte{0x55, 0x48, 0x89, 0xE5, 0xB8, 0x4F, 0x00, 0x00, 0x00, 0x5D, 0xC3, 0, 0, 0, 0}
// missing pop rbp (replaced with nop)
wrongEpilogue := []byte{0x55, 0x48, 0x89, 0xE5, 0xB8, 0x4E, 0x00, 0x00, 0x00, 0x90, 0xC3, 0, 0, 0, 0}
cases := []struct {
name string
buf []byte
want bool
}{
{"plain", plain, true},
{"endbr64+plain", endbr, true},
{"wrong errno", wrongErrno, false},
{"wrong epilogue", wrongEpilogue, false},
{"too short for plain", plain[:10], false},
{"endbr but truncated body", endbr[:14], false},
{"empty", nil, false},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := matchEnosysX86(tc.buf); got != tc.want {
t.Errorf("matchEnosysX86(%x) = %v, want %v", tc.buf, got, tc.want)
}
})
}
}