diff --git a/README.md b/README.md index 06cec84..366b2ff 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,16 @@ Go library for monkey patching ## Compatibility - **Go version:** tested from `go1.7` to `go1.19` -- **Architectures:** `x86`, `amd64` +- **Architectures:** `x86`, `amd64`, `arm64` (ARM64 not supported on macos) - **Operating systems:** tested in `macos`, `linux` and `windows`. +### ARM64 support +The support for ARM64 have some caveats. For example: +- On Windows 11 ARM64 works like any other platform, we test it with tiny methods and worked correctly. +- On Linux ARM64 the minimum target and source method size must be > 24 bytes. This means a simple 1 liner method will silently fail. In our tests we have methods at least with 3 lines and works correctly (beware of the compiler optimizations). +This doesn't happen in any other platform because the assembly code we emit is really short (x64: 12 bytes / x86: 7 bytes), but for ARM64 is exactly 24 bytes. +- On MacOS ARM64 the patching fails with `EACCES: permission denied` when calling `syscall.Mprotect`. There's no current workaround for this issue, if you use an Apple Silicon Mac you can use a docker container or docker dev environment. + ## Features - Can patch package functions, instance functions (by pointer or by value), and create new functions from scratch. diff --git a/patcher.go b/patcher.go index 5d259a0..298f00a 100644 --- a/patcher.go +++ b/patcher.go @@ -46,7 +46,7 @@ func PatchMethod(target, redirection interface{}) (*Patch, error) { } // Patches an instance func by using two parameters, the target struct type and the method name inside that type, -//this func will be redirected to the "redirection" func. Note: The first parameter of the redirection func must be the object instance. +// this func will be redirected to the "redirection" func. Note: The first parameter of the redirection func must be the object instance. func PatchInstanceMethodByName(target reflect.Type, methodName string, redirection interface{}) (*Patch, error) { method, ok := target.MethodByName(methodName) if !ok && target.Kind() == reflect.Struct { diff --git a/patcher_arm64.go b/patcher_arm64.go new file mode 100644 index 0000000..a17ccbc --- /dev/null +++ b/patcher_arm64.go @@ -0,0 +1,42 @@ +//go:build arm64 +// +build arm64 + +package mpatch + +import "unsafe" + +// Code from: https://github.com/agiledragon/gomonkey/blob/master/jmp_arm64.go + +// Gets the jump function rewrite bytes +// +//go:nosplit +func getJumpFuncBytes(to unsafe.Pointer) ([]byte, error) { + res := make([]byte, 0, 24) + d0d1 := uintptr(to) & 0xFFFF + d2d3 := uintptr(to) >> 16 & 0xFFFF + d4d5 := uintptr(to) >> 32 & 0xFFFF + d6d7 := uintptr(to) >> 48 & 0xFFFF + + res = append(res, movImm(0b10, 0, d0d1)...) // MOVZ x26, double[16:0] + res = append(res, movImm(0b11, 1, d2d3)...) // MOVK x26, double[32:16] + res = append(res, movImm(0b11, 2, d4d5)...) // MOVK x26, double[48:32] + res = append(res, movImm(0b11, 3, d6d7)...) // MOVK x26, double[64:48] + res = append(res, []byte{0x4A, 0x03, 0x40, 0xF9}...) // LDR x10, [x26] + res = append(res, []byte{0x40, 0x01, 0x1F, 0xD6}...) // BR x10 + + return res, nil +} + +func movImm(opc, shift int, val uintptr) []byte { + var m uint32 = 26 // rd + m |= uint32(val) << 5 // imm16 + m |= uint32(shift&3) << 21 // hw + m |= 0b100101 << 23 // const + m |= uint32(opc&0x3) << 29 // opc + m |= 0b1 << 31 // sf + + res := make([]byte, 4) + *(*uint32)(unsafe.Pointer(&res[0])) = m + + return res +} diff --git a/patcher_test.go b/patcher_test.go index dcce394..4997f40 100644 --- a/patcher_test.go +++ b/patcher_test.go @@ -1,15 +1,24 @@ package mpatch import ( + "math/rand" "reflect" "testing" ) //go:noinline -func methodA() int { return 1 } +func methodA() int { + x := rand.Int() >> 48 + y := rand.Int() >> 48 + return x + y +} //go:noinline -func methodB() int { return 2 } +func methodB() int { + x := rand.Int() >> 48 + y := rand.Int() >> 48 + return -(x + y) +} type myStruct struct { } @@ -29,15 +38,14 @@ func TestPatcher(t *testing.T) { if err != nil { t.Fatal(err) } - if methodA() != 2 { + if methodA() > 0 { t.Fatal("The patch did not work") } - err = patch.Unpatch() if err != nil { t.Fatal(err) } - if methodA() != 1 { + if methodA() < 0 { t.Fatal("The unpatch did not work") } } @@ -48,7 +56,7 @@ func TestPatcherUsingReflect(t *testing.T) { if err != nil { t.Fatal(err) } - if methodA() != 2 { + if methodA() > 0 { t.Fatal("The patch did not work") } @@ -56,7 +64,7 @@ func TestPatcherUsingReflect(t *testing.T) { if err != nil { t.Fatal(err) } - if methodA() != 1 { + if methodA() < 0 { t.Fatal("The unpatch did not work") } } @@ -78,7 +86,7 @@ func TestPatcherUsingMakeFunc(t *testing.T) { if err != nil { t.Fatal(err) } - if methodA() != 1 { + if methodA() < 0 { t.Fatal("The unpatch did not work") } } diff --git a/patcher_unix.go b/patcher_unix.go index 0e23a90..9870319 100644 --- a/patcher_unix.go +++ b/patcher_unix.go @@ -1,3 +1,4 @@ +//go:build !windows // +build !windows package mpatch @@ -5,6 +6,7 @@ package mpatch import ( "reflect" "syscall" + "time" "unsafe" ) @@ -41,5 +43,6 @@ func writeDataToPointer(ptr unsafe.Pointer, data []byte) error { if err := callMProtect(ptr, dataLength, readAccess); err != nil { return err } + <-time.After(100 * time.Microsecond) // If we remove this line then it fails to unpatch on ARM64 return nil } diff --git a/patcher_unsupported.go b/patcher_unsupported.go index 0d98196..0e8dd7e 100644 --- a/patcher_unsupported.go +++ b/patcher_unsupported.go @@ -1,5 +1,5 @@ -// +build !386 -// +build !amd64 +//go:build !386 && !amd64 && !arm64 +// +build !386,!amd64,!arm64 package mpatch @@ -11,6 +11,7 @@ import ( ) // Gets the jump function rewrite bytes +// //go:nosplit func getJumpFuncBytes(to unsafe.Pointer) ([]byte, error) { return nil, errors.New(fmt.Sprintf("unsupported architecture: %s", runtime.GOARCH)) diff --git a/patcher_windows.go b/patcher_windows.go index 7cc1784..baccf70 100644 --- a/patcher_windows.go +++ b/patcher_windows.go @@ -1,3 +1,4 @@ +//go:build windows // +build windows package mpatch diff --git a/patcher_x32.go b/patcher_x32.go index 5c1ecb5..af2e201 100644 --- a/patcher_x32.go +++ b/patcher_x32.go @@ -1,12 +1,12 @@ +//go:build 386 // +build 386 package mpatch import "unsafe" -const jumpLength = 7 - // Gets the jump function rewrite bytes +// //go:nosplit func getJumpFuncBytes(to unsafe.Pointer) ([]byte, error) { return []byte{ diff --git a/patcher_x64.go b/patcher_x64.go index 6100c68..9650b55 100644 --- a/patcher_x64.go +++ b/patcher_x64.go @@ -1,12 +1,12 @@ +//go:build amd64 // +build amd64 package mpatch import "unsafe" -const jumpLength = 12 - // Gets the jump function rewrite bytes +// //go:nosplit func getJumpFuncBytes(to unsafe.Pointer) ([]byte, error) { return []byte{