1// Copyright 2020 The Go Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style 3// license that can be found in the LICENSE file. 4 5package errorstest 6 7import ( 8 "bytes" 9 "cmd/internal/quoted" 10 "internal/testenv" 11 "os" 12 "os/exec" 13 "path/filepath" 14 "strings" 15 "testing" 16 "unicode" 17) 18 19// A manually modified object file could pass unexpected characters 20// into the files generated by cgo. 21 22const magicInput = "abcdefghijklmnopqrstuvwxyz0123" 23const magicReplace = "\n//go:cgo_ldflag \"-badflag\"\n//" 24 25const cSymbol = "BadSymbol" + magicInput + "Name" 26const cDefSource = "int " + cSymbol + " = 1;" 27const cRefSource = "extern int " + cSymbol + "; int F() { return " + cSymbol + "; }" 28 29// goSource is the source code for the trivial Go file we use. 30// We will replace TMPDIR with the temporary directory name. 31const goSource = ` 32package main 33 34// #cgo LDFLAGS: TMPDIR/cbad.o TMPDIR/cbad.so 35// extern int F(); 36import "C" 37 38func main() { 39 println(C.F()) 40} 41` 42 43func TestBadSymbol(t *testing.T) { 44 testenv.MustHaveGoBuild(t) 45 testenv.MustHaveCGO(t) 46 47 dir := t.TempDir() 48 49 mkdir := func(base string) string { 50 ret := filepath.Join(dir, base) 51 if err := os.Mkdir(ret, 0755); err != nil { 52 t.Fatal(err) 53 } 54 return ret 55 } 56 57 cdir := mkdir("c") 58 godir := mkdir("go") 59 60 makeFile := func(mdir, base, source string) string { 61 ret := filepath.Join(mdir, base) 62 if err := os.WriteFile(ret, []byte(source), 0644); err != nil { 63 t.Fatal(err) 64 } 65 return ret 66 } 67 68 cDefFile := makeFile(cdir, "cdef.c", cDefSource) 69 cRefFile := makeFile(cdir, "cref.c", cRefSource) 70 71 ccCmd := cCompilerCmd(t) 72 73 cCompile := func(arg, base, src string) string { 74 out := filepath.Join(cdir, base) 75 run := append(ccCmd, arg, "-o", out, src) 76 output, err := exec.Command(run[0], run[1:]...).CombinedOutput() 77 if err != nil { 78 t.Log(run) 79 t.Logf("%s", output) 80 t.Fatal(err) 81 } 82 if err := os.Remove(src); err != nil { 83 t.Fatal(err) 84 } 85 return out 86 } 87 88 // Build a shared library that defines a symbol whose name 89 // contains magicInput. 90 91 cShared := cCompile("-shared", "c.so", cDefFile) 92 93 // Build an object file that refers to the symbol whose name 94 // contains magicInput. 95 96 cObj := cCompile("-c", "c.o", cRefFile) 97 98 // Rewrite the shared library and the object file, replacing 99 // magicInput with magicReplace. This will have the effect of 100 // introducing a symbol whose name looks like a cgo command. 101 // The cgo tool will use that name when it generates the 102 // _cgo_import.go file, thus smuggling a magic //go:cgo_ldflag 103 // pragma into a Go file. We used to not check the pragmas in 104 // _cgo_import.go. 105 106 rewrite := func(from, to string) { 107 obj, err := os.ReadFile(from) 108 if err != nil { 109 t.Fatal(err) 110 } 111 112 if bytes.Count(obj, []byte(magicInput)) == 0 { 113 t.Fatalf("%s: did not find magic string", from) 114 } 115 116 if len(magicInput) != len(magicReplace) { 117 t.Fatalf("internal test error: different magic lengths: %d != %d", len(magicInput), len(magicReplace)) 118 } 119 120 obj = bytes.ReplaceAll(obj, []byte(magicInput), []byte(magicReplace)) 121 122 if err := os.WriteFile(to, obj, 0644); err != nil { 123 t.Fatal(err) 124 } 125 } 126 127 cBadShared := filepath.Join(godir, "cbad.so") 128 rewrite(cShared, cBadShared) 129 130 cBadObj := filepath.Join(godir, "cbad.o") 131 rewrite(cObj, cBadObj) 132 133 goSourceBadObject := strings.ReplaceAll(goSource, "TMPDIR", godir) 134 makeFile(godir, "go.go", goSourceBadObject) 135 136 makeFile(godir, "go.mod", "module badsym") 137 138 // Try to build our little package. 139 cmd := exec.Command("go", "build", "-ldflags=-v") 140 cmd.Dir = godir 141 output, err := cmd.CombinedOutput() 142 143 // The build should fail, but we want it to fail because we 144 // detected the error, not because we passed a bad flag to the 145 // C linker. 146 147 if err == nil { 148 t.Errorf("go build succeeded unexpectedly") 149 } 150 151 t.Logf("%s", output) 152 153 for _, line := range bytes.Split(output, []byte("\n")) { 154 if bytes.Contains(line, []byte("dynamic symbol")) && bytes.Contains(line, []byte("contains unsupported character")) { 155 // This is the error from cgo. 156 continue 157 } 158 159 // We passed -ldflags=-v to see the external linker invocation, 160 // which should not include -badflag. 161 if bytes.Contains(line, []byte("-badflag")) { 162 t.Error("output should not mention -badflag") 163 } 164 165 // Also check for compiler errors, just in case. 166 // GCC says "unrecognized command line option". 167 // clang says "unknown argument". 168 if bytes.Contains(line, []byte("unrecognized")) || bytes.Contains(output, []byte("unknown")) { 169 t.Error("problem should have been caught before invoking C linker") 170 } 171 } 172} 173 174func cCompilerCmd(t *testing.T) []string { 175 cc, err := quoted.Split(goEnv(t, "CC")) 176 if err != nil { 177 t.Skipf("parsing go env CC: %s", err) 178 } 179 if len(cc) == 0 { 180 t.Skipf("no C compiler") 181 } 182 testenv.MustHaveExecPath(t, cc[0]) 183 184 out := goEnv(t, "GOGCCFLAGS") 185 quote := '\000' 186 start := 0 187 lastSpace := true 188 backslash := false 189 s := string(out) 190 for i, c := range s { 191 if quote == '\000' && unicode.IsSpace(c) { 192 if !lastSpace { 193 cc = append(cc, s[start:i]) 194 lastSpace = true 195 } 196 } else { 197 if lastSpace { 198 start = i 199 lastSpace = false 200 } 201 if quote == '\000' && !backslash && (c == '"' || c == '\'') { 202 quote = c 203 backslash = false 204 } else if !backslash && quote == c { 205 quote = '\000' 206 } else if (quote == '\000' || quote == '"') && !backslash && c == '\\' { 207 backslash = true 208 } else { 209 backslash = false 210 } 211 } 212 } 213 if !lastSpace { 214 cc = append(cc, s[start:]) 215 } 216 217 // Force reallocation (and avoid aliasing bugs) for tests that append to cc. 218 cc = cc[:len(cc):len(cc)] 219 220 return cc 221} 222 223func goEnv(t *testing.T, key string) string { 224 out, err := exec.Command("go", "env", key).CombinedOutput() 225 if err != nil { 226 t.Logf("go env %s\n", key) 227 t.Logf("%s", out) 228 t.Fatal(err) 229 } 230 return strings.TrimSpace(string(out)) 231} 232