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