• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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