3

I've tried to come up with a simple, minimal example which reproduces this bug, but wasn't able to (it only occurs in one private repo), but I'll start by showing my attempt. Suppose we have a Go module with the following structure:

.
├── command
│   ├── command.go
│   └── command_test.go
├── go.mod
└── go.sum

where go.mod reads

module github.com/kurtpeek/monkeypatching

go 1.12

require (
    bou.ke/monkey v1.0.2
    github.com/google/go-cmp v0.3.1 // indirect
    github.com/pkg/errors v0.8.1 // indirect
    github.com/stretchr/testify v1.4.0
    gotest.tools v2.2.0+incompatible
)

and command.go reads

package command

import "os/exec"

// RunCommand runs a command
func RunCommand() ([]byte, error) {
    return exec.Command("profiles", "list", "-all").Output()
}

and command_test.go

package command

import (
    "os/exec"
    "reflect"
    "testing"

    "bou.ke/monkey"
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
)

func TestRunCommand(t *testing.T) {
    var cmd *exec.Cmd
    patchGuard := monkey.PatchInstanceMethod(reflect.TypeOf(cmd), "Output", func(_ *exec.Cmd) ([]byte, error) {
        return []byte("foobar"), nil
    })
    defer patchGuard.Unpatch()

    output, err := RunCommand()
    require.NoError(t, err)
    assert.Equal(t, []byte("foobar"), output)
}

This test passes.

Now, in my 'real' repo I have a similar unit test

func TestFindIdentity(t *testing.T) {
    certPEM, err := ioutil.ReadFile("testdata/6dc9bf91-37c6-4882-bfaf-301f118f7fac.pem")
    require.NoError(t, err)

    var cmd *exec.Cmd
    patchGuard := monkey.PatchInstanceMethod(reflect.TypeOf(cmd), "Output", func(_ *exec.Cmd) ([]byte, error) {
        output, err := ioutil.ReadFile("testdata/find_identity_match.txt")
        require.NoError(t, err)
        return output, nil
    })
    defer patchGuard.Unpatch()

    found, err := FindIdentity(certPEM)

    assert.True(t, found)
}

where FindIdentity() reads

// FindIdentity checks whether there is an identity (certificate + private key) for the given certificate in the system keychain
func FindIdentity(certPEM []byte) (bool, error) {
    ctx, cancel := context.WithTimeout(context.TODO(), time.Second*5)
    defer cancel()

    fingerprint, err := GetFingerprint(certPEM)
    if err != nil {
        return false, fmt.Errorf("get cert fingerprint: %v", err)
    }

    output, err := exec.CommandContext(ctx, cmdSecurity, "find-identity", systemKeychain).Output()
    if err != nil {
        return false, fmt.Errorf("find identity: %v", err)
    }

    return strings.Contains(string(output), fingerprint), nil
}

// GetFingerprint generates a SHA-1 fingerprint of a certificate, which is how it can be identified from the `security` command
func GetFingerprint(certPEM []byte) (string, error) {
    block, _ := pem.Decode(certPEM)
    if block == nil {
        return "", errors.New("failed to decode cert PEM")
    }

    cert, err := x509.ParseCertificate(block.Bytes)
    if err != nil {
        return "", fmt.Errorf("parse certificate: %v", err)
    }

    fingerprint := fmt.Sprintf("%x", sha1.Sum(cert.Raw))
    fingerprint = strings.Replace(fingerprint, " ", "", -1)
    return strings.ToUpper(fingerprint), nil
}

So similarly, it uses a Command which is patched in the unit test. However, if I try to run the unit test I get this error:

Running tool: /usr/local/opt/go@1.12/bin/go test -timeout 30s github.com/fleetsmith/agent/agent/auth/defaultauth -run ^(TestFindIdentity)$

--- FAIL: TestFindIdentity (0.00s)
panic: permission denied [recovered]
    panic: permission denied

goroutine 25 [running]:
testing.tRunner.func1(0xc000494100)
    /usr/local/Cellar/go@1.12/1.12.12/libexec/src/testing/testing.go:830 +0x392
panic(0x48fede0, 0xc000554730)
    /usr/local/Cellar/go@1.12/1.12.12/libexec/src/runtime/panic.go:522 +0x1b5
bou.ke/monkey.mprotectCrossPage(0x41c20e0, 0xc, 0x7)
    /Users/kurt/go/pkg/mod/bou.ke/monkey@v1.0.2/replace_unix.go:15 +0xe6
bou.ke/monkey.copyToLocation(0x41c20e0, 0xc0000ebd2c, 0xc, 0xc)
    /Users/kurt/go/pkg/mod/bou.ke/monkey@v1.0.2/replace_unix.go:26 +0x6d
bou.ke/monkey.replaceFunction(0x41c20e0, 0xc0001a2510, 0x13, 0x41c20e0, 0x48c3b00)
    /Users/kurt/go/pkg/mod/bou.ke/monkey@v1.0.2/replace.go:29 +0xe6
bou.ke/monkey.patchValue(0x48c3b60, 0xc0000bc078, 0x13, 0x48c3b60, 0xc0001a2510, 0x13)
    /Users/kurt/go/pkg/mod/bou.ke/monkey@v1.0.2/monkey.go:87 +0x22f
bou.ke/monkey.PatchInstanceMethod(0x4b359a0, 0x4996280, 0x49d0699, 0x6, 0x48c3b60, 0xc0001a2510, 0x0)
    /Users/kurt/go/pkg/mod/bou.ke/monkey@v1.0.2/monkey.go:62 +0x160
github.com/fleetsmith/agent/agent/auth/defaultauth.TestFindIdentity(0xc000494100)
    /Users/kurt/go/src/github.com/fleetsmith/agent/agent/auth/defaultauth/keychain_test.go:46 +0x146
testing.tRunner(0xc000494100, 0x4a30260)
    /usr/local/Cellar/go@1.12/1.12.12/libexec/src/testing/testing.go:865 +0xc0
created by testing.(*T).Run
    /usr/local/Cellar/go@1.12/1.12.12/libexec/src/testing/testing.go:916 +0x35a
FAIL    github.com/fleetsmith/agent/agent/auth/defaultauth  0.390s
Error: Tests failed.

Concretely, I get a permission denied panic upon calling monkey.PatchInstanceMethod at this line:

patchGuard := monkey.PatchInstanceMethod(reflect.TypeOf(cmd), "Output", func(_ *exec.Cmd) ([]byte, error) {

})

Any idea what could cause this? It must be some difference between my 'real' repo and my scratch repo.

Kurt Peek
  • 52,165
  • 91
  • 301
  • 526
  • 1
    Is `find-identity`actually executable? – Markus W Mahlberg Dec 10 '19 at 06:36
  • 3
    The `mprotect` syscall is failing for some reason. Read the [package notes](https://github.com/bouk/monkey#notes) which may be relevant here. IMHO I would highly suggest trying to find a testing strategy that doesn't use this package. – JimB Dec 10 '19 at 15:18
  • @MarkusWMahlberg Sorry for the lack of context here; the full command is actually `/usr/bin/security find-identity /Library/Keychains/System.keychain`, which is executable on MacOS systems. – Kurt Peek Dec 10 '19 at 18:47
  • Getting this on Big Sur as well, the failing `mprotect` syscall theory seems correct – Alex Mapley Dec 02 '21 at 23:31

2 Answers2

2

mprotect syscall fails on MacOS Catalina, more explanation on it: Using mprotect to make text segment writable on macOS

If you are using a go monkey library in go test. Then you can not run go test directly. Actually go test do the followings:

  • compile the test functions into a temporary binary file
  • execute the binary
  • delete the temporary binary file

To work around this issue, we need to first generate the test binary, the modify the compiled binary with dd. e.g.

$ go test -c -o test-bin mytest/abc

With -c and -o option, go test will generate a binary named test-bin. Then modify the binary by dd command to set __TEXT(max_prot) to 0x7 after linking:

$  printf '\x07' | dd of=test-bin bs=1 seek=160 count=1 conv=notrunc

Finally, you can run the test binary:

./test-bin
Paco
  • 411
  • 3
  • 9
2

You could try this: https://github.com/eisenxp/macos-golink-wrapper

It is a solution to "syscall.Mprotect panic: permission denied" in Golang on macOS Catalina 10.15.x when using gomonkey or gohook.

  1. Download the tool.
cd `go env GOPATH`
git clone https://github.com/eisenxp/macos-golink-wrapper.git
  1. Rename file link to original_link
mv `go env GOTOOLDIR`/link `go env GOTOOLDIR`/original_link
  1. Copy the tool to GOTOOLDIR
cp `go env GOPATH`/macos-golink-wrapper/link  `go env GOTOOLDIR`/link
  1. Add execution permission to link
chmod +x `go env GOTOOLDIR`/link

This solved my problem, hope it helps you.

zeno Zhang
  • 31
  • 1