220 lines
5.4 KiB
Go
220 lines
5.4 KiB
Go
|
// Copyright 2020 The Go Authors. All rights reserved.
|
||
|
// Use of this source code is governed by a BSD-style
|
||
|
// license that can be found in the LICENSE file.
|
||
|
|
||
|
package errorstest
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"os"
|
||
|
"os/exec"
|
||
|
"path/filepath"
|
||
|
"strings"
|
||
|
"testing"
|
||
|
"unicode"
|
||
|
)
|
||
|
|
||
|
// A manually modified object file could pass unexpected characters
|
||
|
// into the files generated by cgo.
|
||
|
|
||
|
const magicInput = "abcdefghijklmnopqrstuvwxyz0123"
|
||
|
const magicReplace = "\n//go:cgo_ldflag \"-badflag\"\n//"
|
||
|
|
||
|
const cSymbol = "BadSymbol" + magicInput + "Name"
|
||
|
const cDefSource = "int " + cSymbol + " = 1;"
|
||
|
const cRefSource = "extern int " + cSymbol + "; int F() { return " + cSymbol + "; }"
|
||
|
|
||
|
// goSource is the source code for the trivial Go file we use.
|
||
|
// We will replace TMPDIR with the temporary directory name.
|
||
|
const goSource = `
|
||
|
package main
|
||
|
|
||
|
// #cgo LDFLAGS: TMPDIR/cbad.o TMPDIR/cbad.so
|
||
|
// extern int F();
|
||
|
import "C"
|
||
|
|
||
|
func main() {
|
||
|
println(C.F())
|
||
|
}
|
||
|
`
|
||
|
|
||
|
func TestBadSymbol(t *testing.T) {
|
||
|
dir := t.TempDir()
|
||
|
|
||
|
mkdir := func(base string) string {
|
||
|
ret := filepath.Join(dir, base)
|
||
|
if err := os.Mkdir(ret, 0755); err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
return ret
|
||
|
}
|
||
|
|
||
|
cdir := mkdir("c")
|
||
|
godir := mkdir("go")
|
||
|
|
||
|
makeFile := func(mdir, base, source string) string {
|
||
|
ret := filepath.Join(mdir, base)
|
||
|
if err := os.WriteFile(ret, []byte(source), 0644); err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
return ret
|
||
|
}
|
||
|
|
||
|
cDefFile := makeFile(cdir, "cdef.c", cDefSource)
|
||
|
cRefFile := makeFile(cdir, "cref.c", cRefSource)
|
||
|
|
||
|
ccCmd := cCompilerCmd(t)
|
||
|
|
||
|
cCompile := func(arg, base, src string) string {
|
||
|
out := filepath.Join(cdir, base)
|
||
|
run := append(ccCmd, arg, "-o", out, src)
|
||
|
output, err := exec.Command(run[0], run[1:]...).CombinedOutput()
|
||
|
if err != nil {
|
||
|
t.Log(run)
|
||
|
t.Logf("%s", output)
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
if err := os.Remove(src); err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
return out
|
||
|
}
|
||
|
|
||
|
// Build a shared library that defines a symbol whose name
|
||
|
// contains magicInput.
|
||
|
|
||
|
cShared := cCompile("-shared", "c.so", cDefFile)
|
||
|
|
||
|
// Build an object file that refers to the symbol whose name
|
||
|
// contains magicInput.
|
||
|
|
||
|
cObj := cCompile("-c", "c.o", cRefFile)
|
||
|
|
||
|
// Rewrite the shared library and the object file, replacing
|
||
|
// magicInput with magicReplace. This will have the effect of
|
||
|
// introducing a symbol whose name looks like a cgo command.
|
||
|
// The cgo tool will use that name when it generates the
|
||
|
// _cgo_import.go file, thus smuggling a magic //go:cgo_ldflag
|
||
|
// pragma into a Go file. We used to not check the pragmas in
|
||
|
// _cgo_import.go.
|
||
|
|
||
|
rewrite := func(from, to string) {
|
||
|
obj, err := os.ReadFile(from)
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
|
||
|
if bytes.Count(obj, []byte(magicInput)) == 0 {
|
||
|
t.Fatalf("%s: did not find magic string", from)
|
||
|
}
|
||
|
|
||
|
if len(magicInput) != len(magicReplace) {
|
||
|
t.Fatalf("internal test error: different magic lengths: %d != %d", len(magicInput), len(magicReplace))
|
||
|
}
|
||
|
|
||
|
obj = bytes.ReplaceAll(obj, []byte(magicInput), []byte(magicReplace))
|
||
|
|
||
|
if err := os.WriteFile(to, obj, 0644); err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
cBadShared := filepath.Join(godir, "cbad.so")
|
||
|
rewrite(cShared, cBadShared)
|
||
|
|
||
|
cBadObj := filepath.Join(godir, "cbad.o")
|
||
|
rewrite(cObj, cBadObj)
|
||
|
|
||
|
goSourceBadObject := strings.ReplaceAll(goSource, "TMPDIR", godir)
|
||
|
makeFile(godir, "go.go", goSourceBadObject)
|
||
|
|
||
|
makeFile(godir, "go.mod", "module badsym")
|
||
|
|
||
|
// Try to build our little package.
|
||
|
cmd := exec.Command("go", "build", "-ldflags=-v")
|
||
|
cmd.Dir = godir
|
||
|
output, err := cmd.CombinedOutput()
|
||
|
|
||
|
// The build should fail, but we want it to fail because we
|
||
|
// detected the error, not because we passed a bad flag to the
|
||
|
// C linker.
|
||
|
|
||
|
if err == nil {
|
||
|
t.Errorf("go build succeeded unexpectedly")
|
||
|
}
|
||
|
|
||
|
t.Logf("%s", output)
|
||
|
|
||
|
for _, line := range bytes.Split(output, []byte("\n")) {
|
||
|
if bytes.Contains(line, []byte("dynamic symbol")) && bytes.Contains(line, []byte("contains unsupported character")) {
|
||
|
// This is the error from cgo.
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
// We passed -ldflags=-v to see the external linker invocation,
|
||
|
// which should not include -badflag.
|
||
|
if bytes.Contains(line, []byte("-badflag")) {
|
||
|
t.Error("output should not mention -badflag")
|
||
|
}
|
||
|
|
||
|
// Also check for compiler errors, just in case.
|
||
|
// GCC says "unrecognized command line option".
|
||
|
// clang says "unknown argument".
|
||
|
if bytes.Contains(line, []byte("unrecognized")) || bytes.Contains(output, []byte("unknown")) {
|
||
|
t.Error("problem should have been caught before invoking C linker")
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func cCompilerCmd(t *testing.T) []string {
|
||
|
cc := []string{goEnv(t, "CC")}
|
||
|
|
||
|
out := goEnv(t, "GOGCCFLAGS")
|
||
|
quote := '\000'
|
||
|
start := 0
|
||
|
lastSpace := true
|
||
|
backslash := false
|
||
|
s := string(out)
|
||
|
for i, c := range s {
|
||
|
if quote == '\000' && unicode.IsSpace(c) {
|
||
|
if !lastSpace {
|
||
|
cc = append(cc, s[start:i])
|
||
|
lastSpace = true
|
||
|
}
|
||
|
} else {
|
||
|
if lastSpace {
|
||
|
start = i
|
||
|
lastSpace = false
|
||
|
}
|
||
|
if quote == '\000' && !backslash && (c == '"' || c == '\'') {
|
||
|
quote = c
|
||
|
backslash = false
|
||
|
} else if !backslash && quote == c {
|
||
|
quote = '\000'
|
||
|
} else if (quote == '\000' || quote == '"') && !backslash && c == '\\' {
|
||
|
backslash = true
|
||
|
} else {
|
||
|
backslash = false
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
if !lastSpace {
|
||
|
cc = append(cc, s[start:])
|
||
|
}
|
||
|
|
||
|
// Force reallocation (and avoid aliasing bugs) for tests that append to cc.
|
||
|
cc = cc[:len(cc):len(cc)]
|
||
|
|
||
|
return cc
|
||
|
}
|
||
|
|
||
|
func goEnv(t *testing.T, key string) string {
|
||
|
out, err := exec.Command("go", "env", key).CombinedOutput()
|
||
|
if err != nil {
|
||
|
t.Logf("go env %s\n", key)
|
||
|
t.Logf("%s", out)
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
return strings.TrimSpace(string(out))
|
||
|
}
|