badsym_test.go 5.4 KB

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