diff options
Diffstat (limited to 'misc/cgo/errors/badsym_test.go')
-rw-r--r-- | misc/cgo/errors/badsym_test.go | 219 |
1 files changed, 219 insertions, 0 deletions
diff --git a/misc/cgo/errors/badsym_test.go b/misc/cgo/errors/badsym_test.go new file mode 100644 index 0000000..bc3ba2b --- /dev/null +++ b/misc/cgo/errors/badsym_test.go @@ -0,0 +1,219 @@ +// 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)) +} |