Go library for monkey patching
- Go version: tested from
go1.7
togo1.21
- Architectures:
x86
,amd64
,arm64
(ARM64 not supported on macos) - Operating systems: tested in
macos
,linux
andwindows
.
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 callingsyscall.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.
- Can patch package functions, instance functions (by pointer or by value), and create new functions from scratch.
-
Target functions could be inlined, making those functions unpatcheables. You can use
//go:noinline
directive or build with thegcflags=-l
to disable inlining at compiler level. -
Write permission to memory pages containing executable code is needed, some operating systems could restrict this access.
-
Not thread safe.
//go:noinline
func methodA() int { return 1 }
//go:noinline
func methodB() int { return 2 }
func TestPatcher(t *testing.T) {
patch, err := mpatch.PatchMethod(methodA, methodB)
if err != nil {
t.Fatal(err)
}
if methodA() != 2 {
t.Fatal("The patch did not work")
}
err = patch.Unpatch()
if err != nil {
t.Fatal(err)
}
if methodA() != 1 {
t.Fatal("The unpatch did not work")
}
}
//go:noinline
func methodA() int { return 1 }
//go:noinline
func methodB() int { return 2 }
func TestPatcherUsingReflect(t *testing.T) {
reflectA := reflect.ValueOf(methodA)
patch, err := mPatch.PatchMethodByReflectValue(reflectA, methodB)
if err != nil {
t.Fatal(err)
}
if methodA() != 2 {
t.Fatal("The patch did not work")
}
err = patch.Unpatch()
if err != nil {
t.Fatal(err)
}
if methodA() != 1 {
t.Fatal("The unpatch did not work")
}
}
//go:noinline
func methodA() int { return 1 }
func TestPatcherUsingMakeFunc(t *testing.T) {
reflectA := reflect.ValueOf(methodA)
patch, err := PatchMethodWithMakeFuncValue(reflectA,
func(args []reflect.Value) (results []reflect.Value) {
return []reflect.Value{reflect.ValueOf(42)}
})
if err != nil {
t.Fatal(err)
}
if methodA() != 42 {
t.Fatal("The patch did not work")
}
err = patch.Unpatch()
if err != nil {
t.Fatal(err)
}
if methodA() != 1 {
t.Fatal("The unpatch did not work")
}
}
type myStruct struct {
}
//go:noinline
func (s *myStruct) Method() int {
return 1
}
func TestInstancePatcher(t *testing.T) {
mStruct := myStruct{}
var patch *Patch
var err error
patch, err = PatchInstanceMethodByName(reflect.TypeOf(mStruct), "Method", func(m *myStruct) int {
patch.Unpatch()
defer patch.Patch()
return 41 + m.Method()
})
if err != nil {
t.Fatal(err)
}
if mStruct.Method() != 42 {
t.Fatal("The patch did not work")
}
err = patch.Unpatch()
if err != nil {
t.Fatal(err)
}
if mStruct.Method() != 1 {
t.Fatal("The unpatch did not work")
}
}
type myStruct struct {
}
//go:noinline
func (s myStruct) ValueMethod() int {
return 1
}
func TestInstanceValuePatcher(t *testing.T) {
mStruct := myStruct{}
var patch *Patch
var err error
patch, err = PatchInstanceMethodByName(reflect.TypeOf(mStruct), "ValueMethod", func(m myStruct) int {
patch.Unpatch()
defer patch.Patch()
return 41 + m.Method()
})
if err != nil {
t.Fatal(err)
}
if mStruct.ValueMethod() != 42 {
t.Fatal("The patch did not work")
}
err = patch.Unpatch()
if err != nil {
t.Fatal(err)
}
if mStruct.ValueMethod() != 1 {
t.Fatal("The unpatch did not work")
}
}
Library inspired by the blog post: https://bou.ke/blog/monkey-patching-in-go/