Skip to content

Commit

Permalink
Merge pull request #223 from hashicorp/vault-9832/better-plugin-error…
Browse files Browse the repository at this point in the history
…-message

Add more helpful message on plugin setup failure
  • Loading branch information
swenson committed Dec 5, 2022
2 parents b60690f + b616f4e commit 5ecc4fb
Show file tree
Hide file tree
Showing 9 changed files with 158 additions and 7 deletions.
79 changes: 74 additions & 5 deletions client.go
Expand Up @@ -6,6 +6,9 @@ import (
"crypto/subtle"
"crypto/tls"
"crypto/x509"
"debug/elf"
"debug/macho"
"debug/pe"
"encoding/base64"
"errors"
"fmt"
Expand All @@ -15,17 +18,28 @@ import (
"net"
"os"
"os/exec"
"os/user"
"path/filepath"
"runtime"
"strconv"
"strings"
"sync"
"sync/atomic"
"syscall"
"time"

"github.com/hashicorp/go-hclog"
"google.golang.org/grpc"
)

const unrecognizedRemotePluginMessage = `Unrecognized remote plugin message: %s
This usually means
the plugin was not compiled for this architecture,
the plugin is missing dynamic-link libraries necessary to run,
the plugin is not executable by this process due to file permissions, or
the plugin failed to negotiate the initial go-plugin protocol handshake
%s`

// If this is 1, then we've called CleanupClients. This can be used
// by plugin RPC implementations to change error behavior since you
// can expected network connection errors at this point. This should be
Expand Down Expand Up @@ -473,7 +487,65 @@ func (c *Client) Kill() {
c.l.Unlock()
}

// Starts the underlying subprocess, communicating with it to negotiate
// peTypes is a list of Portable Executable (PE) machine types from https://learn.microsoft.com/en-us/windows/win32/debug/pe-format
// mapped to GOARCH types. It is not comprehensive, and only includes machine types that Go supports.
var peTypes = map[uint16]string{
0x14c: "386",
0x1c0: "arm",
0x6264: "loong64",
0x8664: "amd64",
0xaa64: "arm64",
}

// additionalNotesAboutCommand tries to get additional information about a command that might help diagnose
// why it won't run correctly. It runs as a best effort only.
func additionalNotesAboutCommand(path string) string {
notes := ""
stat, err := os.Stat(path)
if err != nil {
return notes
}

notes += "\nAdditional notes about plugin:\n"
notes += fmt.Sprintf(" Path: %s\n", path)
notes += fmt.Sprintf(" Mode: %s\n", stat.Mode())
statT, ok := stat.Sys().(*syscall.Stat_t)
if ok {
currentUsername := "?"
if u, err := user.LookupId(strconv.FormatUint(uint64(os.Getuid()), 10)); err == nil {
currentUsername = u.Username
}
currentGroup := "?"
if g, err := user.LookupGroupId(strconv.FormatUint(uint64(os.Getgid()), 10)); err == nil {
currentGroup = g.Name
}
username := "?"
if u, err := user.LookupId(strconv.FormatUint(uint64(statT.Uid), 10)); err == nil {
username = u.Username
}
group := "?"
if g, err := user.LookupGroupId(strconv.FormatUint(uint64(statT.Gid), 10)); err == nil {
group = g.Name
}
notes += fmt.Sprintf(" Owner: %d [%s] (current: %d [%s])\n", statT.Uid, username, os.Getuid(), currentUsername)
notes += fmt.Sprintf(" Group: %d [%s] (current: %d [%s])\n", statT.Gid, group, os.Getgid(), currentGroup)
}

if elfFile, err := elf.Open(path); err == nil {
notes += fmt.Sprintf(" ELF architecture: %s (current architecture: %s)\n", elfFile.Machine, runtime.GOARCH)
} else if machoFile, err := macho.Open(path); err == nil {
notes += fmt.Sprintf(" MachO architecture: %s (current architecture: %s)\n", machoFile.Cpu, runtime.GOARCH)
} else if peFile, err := pe.Open(path); err == nil {
machine, ok := peTypes[peFile.Machine]
if !ok {
machine = "unknown"
}
notes += fmt.Sprintf(" PE architecture: %s (current architecture: %s)\n", machine, runtime.GOARCH)
}
return notes
}

// Start the underlying subprocess, communicating with it to negotiate
// a port for RPC connections, and returning the address to connect via RPC.
//
// This method is safe to call multiple times. Subsequent calls have no effect.
Expand Down Expand Up @@ -697,10 +769,7 @@ func (c *Client) Start() (addr net.Addr, err error) {
line = strings.TrimSpace(line)
parts := strings.SplitN(line, "|", 6)
if len(parts) < 4 {
err = fmt.Errorf(
"Unrecognized remote plugin message: %s\n\n"+
"This usually means that the plugin is either invalid or simply\n"+
"needs to be recompiled to support the latest protocol.", line)
err = fmt.Errorf(unrecognizedRemotePluginMessage, line, additionalNotesAboutCommand(cmd.Path))
return
}

Expand Down
42 changes: 41 additions & 1 deletion client_test.go
Expand Up @@ -10,13 +10,14 @@ import (
"net"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
"sync"
"testing"
"time"

hclog "github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-hclog"
)

func TestClient(t *testing.T) {
Expand Down Expand Up @@ -1395,3 +1396,42 @@ this line is short
t.Fatalf("\nexpected output: %q\ngot output: %q", msg, read)
}
}

func TestAdditionalNotesAboutCommand(t *testing.T) {
files := []string{
"windows-amd64.exe",
"windows-386.exe",
"linux-amd64",
"darwin-amd64",
"darwin-arm64",
}
for _, file := range files {
fullFile := path.Join("testdata", file)
if _, err := os.Stat(fullFile); os.IsNotExist(err) {
t.Skipf("testdata executables not present; please run 'make' in testdata/ directory for this test")
}

notes := additionalNotesAboutCommand(fullFile)
if strings.Contains(file, "windows") && !strings.Contains(notes, "PE") {
t.Errorf("Expected notes to contain Windows information:\n%s", notes)
}
if strings.Contains(file, "linux") && !strings.Contains(notes, "ELF") {
t.Errorf("Expected notes to contain Linux information:\n%s", notes)
}
if strings.Contains(file, "darwin") && !strings.Contains(notes, "MachO") {
t.Errorf("Expected notes to contain macOS information:\n%s", notes)
}

if strings.Contains(file, "amd64") && !(strings.Contains(notes, "amd64") || strings.Contains(notes, "EM_X86_64") || strings.Contains(notes, "CpuAmd64")) {
t.Errorf("Expected notes to contain amd64 information:\n%s", notes)
}

if strings.Contains(file, "arm64") && !strings.Contains(notes, "CpuArm64") {
t.Errorf("Expected notes to contain arm64 information:\n%s", notes)
}
if strings.Contains(file, "386") && !strings.Contains(notes, "386") {
t.Errorf("Expected notes to contain 386 information:\n%s", notes)
}

}
}
2 changes: 1 addition & 1 deletion plugin_test.go
Expand Up @@ -15,7 +15,7 @@ import (
"time"

"github.com/golang/protobuf/ptypes/empty"
hclog "github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-hclog"
grpctest "github.com/hashicorp/go-plugin/test/grpc"
"golang.org/x/net/context"
"google.golang.org/grpc"
Expand Down
5 changes: 5 additions & 0 deletions testdata/.gitignore
@@ -0,0 +1,5 @@
windows-amd64.exe
windows-386.exe
linux-amd64
darwin-amd64
darwin-arm64
29 changes: 29 additions & 0 deletions testdata/Makefile
@@ -0,0 +1,29 @@
default: all
.PHONY: default

.PHONY: clean
clean:
rm -f windows-amd64.exe windows-386.exe linux-amd64 darwin-amd64 darwin-arm64

.PHONY: all
all: windows-amd64.exe windows-386.exe linux-amd64 darwin-amd64 darwin-arm64

.PHONY: windows-amd64.exe
windows-amd64.exe:
GOOS=windows GOARCH=amd64 go build -o $@

.PHONY: windows-386.exe
windows-386.exe:
GOOS=windows GOARCH=386 go build -o $@

.PHONY: linux-amd64
linux-amd64:
GOOS=linux GOARCH=amd64 go build -o $@

.PHONY: darwin-amd64
darwin-amd64:
GOOS=darwin GOARCH=amd64 go build -o $@

.PHONY: darwin-arm64
darwin-arm64:
GOOS=darwin GOARCH=arm64 go build -o $@
1 change: 1 addition & 0 deletions testdata/README.md
@@ -0,0 +1 @@
This folder contains a minimal Go program so that we can obtain example binaries of programs for various architectures for use in tests.
3 changes: 3 additions & 0 deletions testdata/go.mod
@@ -0,0 +1,3 @@
module example.com/testdata

go 1.19
Empty file added testdata/go.sum
Empty file.
4 changes: 4 additions & 0 deletions testdata/minimal.go
@@ -0,0 +1,4 @@
package main

func main() {
}

0 comments on commit 5ecc4fb

Please sign in to comment.