| // 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" |
| "io/ioutil" |
| "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 := ioutil.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 := ioutil.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 := ioutil.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:]) |
| } |
| 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)) |
| } |