| // 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)) | |
| } |