Monkey patching in Go

Overview

Go monkeypatching ๐Ÿต ๐Ÿ’

Actual arbitrary monkeypatching for Go. Yes really.

Read this blogpost for an explanation on how it works: https://bou.ke/blog/monkey-patching-in-go/

I thought that monkeypatching in Go is impossible?

It's not possible through regular language constructs, but we can always bend computers to our will! Monkey implements monkeypatching by rewriting the running executable at runtime and inserting a jump to the function you want called instead. This is as unsafe as it sounds and I don't recommend anyone do it outside of a testing environment.

Make sure you read the notes at the bottom of the README if you intend to use this library.

Using monkey

Monkey's API is very simple and straightfoward. Call monkey.Patch(<target function>, <replacement function>) to replace a function. For example:

package main

import (
	"fmt"
	"os"
	"strings"

	"bou.ke/monkey"
)

func main() {
	monkey.Patch(fmt.Println, func(a ...interface{}) (n int, err error) {
		s := make([]interface{}, len(a))
		for i, v := range a {
			s[i] = strings.Replace(fmt.Sprint(v), "hell", "*bleep*", -1)
		}
		return fmt.Fprintln(os.Stdout, s...)
	})
	fmt.Println("what the hell?") // what the *bleep*?
}

You can then call monkey.Unpatch(<target function>) to unpatch the method again. The replacement function can be any function value, whether it's anonymous, bound or otherwise.

If you want to patch an instance method you need to use monkey.PatchInstanceMethod(<type>, <name>, <replacement>). You get the type by using reflect.TypeOf, and your replacement function simply takes the instance as the first argument. To disable all network connections, you can do as follows for example:

package main

import (
	"fmt"
	"net"
	"net/http"
	"reflect"

	"bou.ke/monkey"
)

func main() {
	var d *net.Dialer // Has to be a pointer to because `Dial` has a pointer receiver
	monkey.PatchInstanceMethod(reflect.TypeOf(d), "Dial", func(_ *net.Dialer, _, _ string) (net.Conn, error) {
		return nil, fmt.Errorf("no dialing allowed")
	})
	_, err := http.Get("http://google.com")
	fmt.Println(err) // Get http://google.com: no dialing allowed
}

Note that patching the method for just one instance is currently not possible, PatchInstanceMethod will patch it for all instances. Don't bother trying monkey.Patch(instance.Method, replacement), it won't work. monkey.UnpatchInstanceMethod(<type>, <name>) will undo PatchInstanceMethod.

If you want to remove all currently applied monkeypatches simply call monkey.UnpatchAll. This could be useful in a test teardown function.

If you want to call the original function from within the replacement you need to use a monkey.PatchGuard. A patchguard allows you to easily remove and restore the patch so you can call the original function. For example:

package main

import (
	"fmt"
	"net/http"
	"reflect"
	"strings"

	"bou.ke/monkey"
)

func main() {
	var guard *monkey.PatchGuard
	guard = monkey.PatchInstanceMethod(reflect.TypeOf(http.DefaultClient), "Get", func(c *http.Client, url string) (*http.Response, error) {
		guard.Unpatch()
		defer guard.Restore()

		if !strings.HasPrefix(url, "https://") {
			return nil, fmt.Errorf("only https requests allowed")
		}

		return c.Get(url)
	})

	_, err := http.Get("http://google.com")
	fmt.Println(err) // only https requests allowed
	resp, err := http.Get("https://google.com")
	fmt.Println(resp.Status, err) // 200 OK <nil>
}

Notes

  1. Monkey sometimes fails to patch a function if inlining is enabled. Try running your tests with inlining disabled, for example: go test -gcflags=-l. The same command line argument can also be used for build.
  2. Monkey won't work on some security-oriented operating system that don't allow memory pages to be both write and execute at the same time. With the current approach there's not really a reliable fix for this.
  3. Monkey is not threadsafe. Or any kind of safe.
  4. I've tested monkey on OSX 10.10.2 and Ubuntu 14.04. It should work on any unix-based x86 or x86-64 system.

ยฉ Bouke van der Bijl

Issues
  • Fix problems with go1.7+

    Fix problems with go1.7+

    There were failures with go1.7 and higher in the TestOnInstanceMethod test.

    First was:

    panic: unknown method no [recovered]
            panic: unknown method no
    

    This appears to be due to https://github.com/golang/go/issues/15673 where unexported methods are made even harder to get to via reflection.

    With that addressed, tests still failed:

    --- FAIL: TestOnInstanceMethod (0.00s)
            Error Trace:    monkey_test.go:94
            Error:          Should be true
            Error Trace:    monkey_test.go:95
            Error:          Should be false
    

    This appears to be due to reflect.Value no longer being usable to uniquely identify a method. Using .Pointer() on the reflect.Value gives a workable ID.

    opened by allenluce 5
  • Question about license

    Question about license

    Hi, Bouke!

    As you surely know, current license of your wonderful project looks like:

    Copyright Bouke van der Bijl
    I do not give anyone permissions to use this tool for any purpose. Don't use it.
    

    This certainly unique :)

    Actually, this license don't give a permissions to use the project, but people actually want use it pretty much. Could you might be consider to update the license to something more common, as MIT or BSD (link)?

    Thank you in advance! Artiom Giza

    opened by artiomgiza 4
  • Hold onto replacement references

    Hold onto replacement references

    This is to prevent the garbage collector from freeing the replacement. Doing so appears to cause a crash when the patched function is called:

    unexpected fault address 0xc82041c740
    fatal error: fault
    [signal 0xb code=0x2 addr=0xc82041c740 pc=0xc82041c740]
    
    goroutine 4 [running]:
    runtime.throw(0xa1b1c8, 0x5)
            /home/aluce/.gvm/gos/go1.6.2/src/runtime/panic.go:547 +0x90 fp=0xc820037d08 sp=0xc820037cf0
    runtime.sigpanic()
            /home/aluce/.gvm/gos/go1.6.2/src/runtime/sigpanic_unix.go:27 +0x2ab fp=0xc820037d58 sp=0xc820037d08
    github.com/lyfe-mobile/receiver.StartMovers.func1(0xc8204090e0, 0xc820409140, 0x1312d00, 0xa33b30, 0xa)
            /home/aluce/.gvm/pkgsets/go1.6.2/global/src/github.com/lyfe-mobile/receiver/movers.go:37 +0x531 fp=0xc820037f88 sp=0xc20037d58
    
    opened by allenluce 3
  • Add windows support.

    Add windows support.

    Fixes https://github.com/bouk/monkey/issues/11

    Tested on Windows 7 x64, but AFAIK it should work on all versions of windows.

    I've tested my fork on both Windows and Ubuntu by running: gotest -v -gcflags=-l. All tests pass except TestOnInstanceMethod which seems to be broken on Go 1.7.4 Linux/amd64 regardless of my changes.

    opened by Andoryuuta 2
  • fix import for go.mod

    fix import for go.mod

    Hi This PR fix issue when trying to install this module using go mod.

     github.com/bouk/monkey: github.com/bouk/[email protected]: parsing go.mod:
            module declares its path as: bou.ke/monkey
                    but was required as: github.com/bouk/monkey
    
    opened by aldor007 1
  • reflect.Value should not be used as map key

    reflect.Value should not be used as map key

    type Value struct {
    	typ *rtype
    	ptr unsafe.Pointer
    	flag
    }
    var v reflect.Value
    

    rtype.MethodByName() returns a new Value object with a different ptr, so we should use its underlying value as map key instead. I suggest to use reflect.Pointer() to get that value and changed a bit of the testing code. @allenluce has done a great job, however there is still room for some improvement...(actually I want to be merged cuz I also fixed this issue independently๐Ÿ˜‚)

    opened by daichao1997 1
  • FreeBSD doesn't have syscall.Mprotect implementation

    FreeBSD doesn't have syscall.Mprotect implementation

    Golang's FreeBSD implementation lacks syscall.Mprotect() but this syscall exists in "golang.org/x/sys/unix" for all Unix flavors having it implemented.

    opened by dkorunic 1
  • add PatchReturning function

    add PatchReturning function

    Usually, we only focus on the return values of the function. PatchReturning function allows us to define replacement func without input parameters. funcReturning func uses reflect to generate a function, is inspired by gostub.

    Usage

    func Hello(name string) string {
        return "hello " + name
    }
    
    func main() {
        fmt.Println(Hello("monkey")) // hello monkey
    
        monkey.PatchReturning(Hello, "hey gay")
    
        fmt.Println(Hello("monkey")) // hey gay
    }
    
    opened by liyifeng1994 1
  • Add patch instance method with flexible params

    Add patch instance method with flexible params

    Extension to PatchInstanceMethod functionality.

    If we want to patch an instance method without support input parameters:

    • must when we don't have access to the private receiver type
    • might be useful when we interested only in return value

    Added PatchInstanceMethodFlexible, similar to PatchInstanceMethod, but doesn't require input parameters be the same type as patched method input parameters.

    For example could be used as:

        monkey.PatchInstanceMethodFlexible(reflect.TypeOf(d), "Dial", func( /*NO INPUT PARAMETERS*/) (net.Conn, error) {
            ...
        })
    
        // or if we are interested in some input parameter
        monkey.PatchInstanceMethodFlexible(reflect.TypeOf(d), "Dial", func(_, _ interface{}, a string) (net.Conn, error) {
            // can use "a"
        })
    

    In both examples above, the first input param in the replacement func is the "receiver". If it's private - widely used to expose "d" as interface and initiate it with "private" implementation - patch can't be done :/ PatchInstanceMethodFlexible allows us to define replacement func without specify real inputs type, so we can patch also functions with "private" receiver

    opened by artiomgiza 1
  • add PatchInstanceMethodFlexible function

    add PatchInstanceMethodFlexible function

    Extension to PatchInstanceMethod functionality.

    If we want to patch an instance method without support input parameters:

    • must when we don't have access to the private receiver type
    • might be useful when we interested only in return value

    Added PatchInstanceMethodFlexible, similar to PatchInstanceMethod, but doesn't require input parameters be the same type as patched method input parameters.

    For example could be used as:

        monkey.PatchInstanceMethodFlexible(reflect.TypeOf(d), "Dial", func( /*NO INPUT PARAMETERS*/) (net.Conn, error) {
            ...
        })
    
        // or if we are interested in some input parameter
        monkey.PatchInstanceMethodFlexible(reflect.TypeOf(d), "Dial", func(_, _ interface{}, a string) (net.Conn, error) {
            // can use "a"
        })
    

    In both examples above, the first input param in the replacement func is the "receiver". If it's private - widely used to expose "d" as interface and initiate it with "private" implementation - patch can't be done :/ PatchInstanceMethodFlexible allows us to define replacement func without specify real inputs type, so we can patch also functions with "private" receiver

    opened by artiomgiza 1
Owner
Bouke van der Bijl
Bouke van der Bijl
gomonkey is a library to make monkey patching in unit tests easy

gomonkey is a library to make monkey patching in unit tests easy, and the core idea of monkey patching comes from Bouke, you can read this blogpost for an explanation on how it works.

Zhang Xiaolong 1.1k Jun 29, 2022
Extending the Monkey (programming) Lang from

?? Mellang ?? Mellang, an interpreted programming language Mellang VSCode Extension You can download it on https://marketplace.visualstudio.com/items?

Antonio Mello Babo 16 May 20, 2022
Monkey programming language project from 'Writing An Interpreter In Go'and 'Writing A Compiler In Go' Books

Monkey Monkey programming language ?? project from "Writing An Interpreter In Go

Amr Hesham 1 Dec 16, 2021
Monkey-go - Writing An Interpreter In Golang

monkey-go Learning how to write an Interpreter called Monkey using Go! If you're

Saad Guessous 3 Jun 12, 2022