mirror of
https://github.com/blacktop/ipsw.git
synced 2026-05-08 12:22:26 +00:00
chore: refactor enosys detection and add unit tests
This commit is contained in:
+54
-29
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user