monkey

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 3, 2022 License: MIT Imports: 9 Imported by: 0

README

Monkey

Go Reference Go Test

"Monkey" is a library for monkey-patching functions. This may be useful to get determined test result with functions that have side effect (like time.Now()).

Why does this library exist?

Earlier I found library github.com/bouk/monkey with same name and functionality and sometimes used it in tests. But this library was unstable, and currently it's archived. So I decided to create new one with different approach.

Usage

Monkey-patching time.Now() in tests:

package sometest

import (
	"testing"
	"time"
	
	"github.com/xakep666/monkey"
)

func init() {
	monkey.NewPatcher().
		Apply(func(patcher *monkey.Patcher) {
			monkey.RegisterReplacement(patcher, time.Now, func() time.Time {
				return time.Date(1980, 1, 2, 3, 4, 5, 6, time.UTC)
			})
		}).
		MustPatchAndExec()
}

func TestTime(t *testing.T) {
	if now := time.Now(); !now.Equal(time.Date(1980, 1, 2, 3, 4, 5, 6, time.UTC)) {
		t.Errorf("Time not patched, returned: %s", now)
	}
}

More examples can be found here.

How does it work

  • Developer register own replacements for specified functions.
  • Some checks performed i.e. for cyclic replacements.
  • Current executable copied to temporary directory. Further operations will be performed with new temporary executable.
  • Unconditional jump instructions inserted at the beginning of specified functions.
  • New patched binary executed.

To prevent recursive self-(re)start this library adds special environment variable when it starts patched binary.

Comparison with github.com/bouk/monkey

Unlike mentioned library this performs patching before binary execution. This results in major advantages but has same disadvantages.

Advantages:

  • No unsafe imported.
  • No mprotect-like system calls. Some systems refused to set writeable and executable flag on pages.
  • Process memory (executable code) not modified in runtime.
  • No data-races during patch and call processes. It follows from the previous paragraph.

Disadvantages:

  • Disk activity (writing to temporary folder).
  • Impossible to "unpatch" or somehow call original version of function.
  • Sometimes may fail to locate address of function inside executable.

Here is some points why patch may fail:

  • OS temp directory not available for writing or binaries executing.
  • Unsupported architecture. This library contains binary opcodes of unconditional jump instructions for different architectures.
  • Target function inlined by compiler. To avoid this use //go:noinline pragma or -gcflags=-l compiler flag.
  • Attempt to patch interface method. But sometimes it may work (see example).
  • Missing symbol table and/or PC-Line table needed to locate function address in executable by name. I've seen this only on Windows with under such circumstances:
    • go test without -o
    • go run
    • go build with -ldflags=-s

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrCyclicReplacement returned if you want to perform replacements like "a"->"b" and "b"->"a".
	// Such replacements lead to infinite loop.
	ErrCyclicReplacement = fmt.Errorf("cyclic replacement detected")

	// ErrFunctionNotFound returned when provided function was not found.
	ErrFunctionNotFound = replacer.ErrFunctionNotFound

	// ErrUnsupportedArchitecture returned if cpu architecture currently unsupported.
	ErrUnsupportedArchitecture = replacer.ErrUnsupportedArchitecture

	// ErrShortFunction returned if function too short for trampoline.
	ErrShortFunction = replacer.ErrShortFunction

	// ErrLongDistance returned if functions located too far for trampoline.
	ErrLongDistance = replacer.ErrLongDistance
)

Functions

func RegisterReplacement

func RegisterReplacement[T any](p *Patcher, original, replacement T)

RegisterReplacement registers function replacement in patcher. Defined as function because it's impossible to use different type parameters in methods. Note that arguments must be functions despite "any" used as constraint

because generics doesn't allow to specify that parameter must be "any function".

Types

type PatchAndExecOption

type PatchAndExecOption interface {
	// contains filtered or unexported methods
}

func RemovePatchedExecutable

func RemovePatchedExecutable() PatchAndExecOption

RemovePatchedExecutable enables automatic removal of patched executable.

func WithEnvVarName

func WithEnvVarName(name string) PatchAndExecOption

WithEnvVarName sets a custom environment variable name to determine if code runs inside patched executable or not.

func WithEnvVarValue

func WithEnvVarValue(value string) PatchAndExecOption

WithEnvVarValue sets exact value for environment variable used to determine if code runs inside patched executable or not. Without this option only presence of variable will be checked.

type Patcher

type Patcher struct {
	// contains filtered or unexported fields
}

Patcher is a registry of function replacements applied to executable

func NewPatcher

func NewPatcher() *Patcher

NewPatcher constructs Patcher.

func (*Patcher) Apply

func (p *Patcher) Apply(cb func(patcher *Patcher)) *Patcher

Apply just calls callback with itself. This needed to support "flow-style" interface.

func (*Patcher) MustPatchAndExec

func (p *Patcher) MustPatchAndExec(opts ...PatchAndExecOption)

MustPatchAndExec acts like PatchAndExec but panics on errors.

func (*Patcher) PatchAndExec

func (p *Patcher) PatchAndExec(opts ...PatchAndExecOption) error

PatchAndExec makes patches according to registered replacements and re-runs executable. Algorithm: 0) Check if we are not running patched executable, otherwise go to 1. This made by checking presence (or exact value if specified) of special environment variable. 0.1) Remove itself if such option specified. 1) Copy current executable to temporary file 2) For each replacement: replace beginning of original function with "trampoline" to replacement function. 3) Run patched executable with special environment variable to avoid recursions (this is terminal condition). 'execve' system call used on *nix systems, 'exec.Command' with stdin/out/err attached and 'os.Exit' after termination on others. So on successful run all code after this function in original executable will become unreachable.

Directories

Path Synopsis
internal

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL