Skip to content

Commit 062e0e5

Browse files
ianlancetaylorkatiehockman
authored andcommitted
cmd/go, cmd/cgo: don't let bogus symbol set cgo_ldflag
A hand-edited object file can have a symbol name that uses newline and other normally invalid characters. The cgo tool will generate Go files containing symbol names, unquoted. That can permit those symbol names to inject Go code into a cgo-generated file. If that Go code uses the //go:cgo_ldflag pragma, it can cause the C linker to run arbitrary code when building a package. If you build an imported package we permit arbitrary code at run time, but we don't want to permit it at package build time. This CL prevents this in two ways. In cgo, reject invalid symbols that contain non-printable or space characters, or that contain anything that looks like a Go comment. In the go tool, double check all //go:cgo_ldflag directives in generated code, to make sure they follow the existing LDFLAG restrictions. Thanks to Imre Rad / https://www.linkedin.com/in/imre-rad-2358749b for reporting this. Fixes CVE-2020-28367 Change-Id: Ia1ad8f3791ea79612690fa7d26ac451d0f6df7c1 Reviewed-on: https://team-review.git.corp.google.com/c/golang/go-private/+/895832 Reviewed-by: Than McIntosh <[email protected]> Reviewed-by: Cherry Zhang <[email protected]> Reviewed-on: https://go-review.googlesource.com/c/go/+/269658 Trust: Katie Hockman <[email protected]> Trust: Roland Shoemaker <[email protected]> Run-TryBot: Katie Hockman <[email protected]> TryBot-Result: Go Bot <[email protected]> Reviewed-by: Roland Shoemaker <[email protected]>
1 parent 1e1fa59 commit 062e0e5

File tree

3 files changed

+299
-0
lines changed

3 files changed

+299
-0
lines changed

misc/cgo/errors/badsym_test.go

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

src/cmd/cgo/out.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,8 @@ func dynimport(obj string) {
337337
if s.Version != "" {
338338
targ += "#" + s.Version
339339
}
340+
checkImportSymName(s.Name)
341+
checkImportSymName(targ)
340342
fmt.Fprintf(stdout, "//go:cgo_import_dynamic %s %s %q\n", s.Name, targ, s.Library)
341343
}
342344
lib, _ := f.ImportedLibraries()
@@ -352,6 +354,7 @@ func dynimport(obj string) {
352354
if len(s) > 0 && s[0] == '_' {
353355
s = s[1:]
354356
}
357+
checkImportSymName(s)
355358
fmt.Fprintf(stdout, "//go:cgo_import_dynamic %s %s %q\n", s, s, "")
356359
}
357360
lib, _ := f.ImportedLibraries()
@@ -366,6 +369,8 @@ func dynimport(obj string) {
366369
for _, s := range sym {
367370
ss := strings.Split(s, ":")
368371
name := strings.Split(ss[0], "@")[0]
372+
checkImportSymName(name)
373+
checkImportSymName(ss[0])
369374
fmt.Fprintf(stdout, "//go:cgo_import_dynamic %s %s %q\n", name, ss[0], strings.ToLower(ss[1]))
370375
}
371376
return
@@ -383,6 +388,7 @@ func dynimport(obj string) {
383388
// Go symbols.
384389
continue
385390
}
391+
checkImportSymName(s.Name)
386392
fmt.Fprintf(stdout, "//go:cgo_import_dynamic %s %s %q\n", s.Name, s.Name, s.Library)
387393
}
388394
lib, err := f.ImportedLibraries()
@@ -398,6 +404,23 @@ func dynimport(obj string) {
398404
fatalf("cannot parse %s as ELF, Mach-O, PE or XCOFF", obj)
399405
}
400406

407+
// checkImportSymName checks a symbol name we are going to emit as part
408+
// of a //go:cgo_import_dynamic pragma. These names come from object
409+
// files, so they may be corrupt. We are going to emit them unquoted,
410+
// so while they don't need to be valid symbol names (and in some cases,
411+
// involving symbol versions, they won't be) they must contain only
412+
// graphic characters and must not contain Go comments.
413+
func checkImportSymName(s string) {
414+
for _, c := range s {
415+
if !unicode.IsGraphic(c) || unicode.IsSpace(c) {
416+
fatalf("dynamic symbol %q contains unsupported character", s)
417+
}
418+
}
419+
if strings.Index(s, "//") >= 0 || strings.Index(s, "/*") >= 0 {
420+
fatalf("dynamic symbol %q contains Go comment")
421+
}
422+
}
423+
401424
// Construct a gcc struct matching the gc argument frame.
402425
// Assumes that in gcc, char is 1 byte, short 2 bytes, int 4 bytes, long long 8 bytes.
403426
// These assumptions are checked by the gccProlog.

src/cmd/go/internal/work/exec.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2827,6 +2827,66 @@ OverlayLoop:
28272827
noCompiler()
28282828
}
28292829

2830+
// Double check the //go:cgo_ldflag comments in the generated files.
2831+
// The compiler only permits such comments in files whose base name
2832+
// starts with "_cgo_". Make sure that the comments in those files
2833+
// are safe. This is a backstop against people somehow smuggling
2834+
// such a comment into a file generated by cgo.
2835+
if cfg.BuildToolchainName == "gc" && !cfg.BuildN {
2836+
var flags []string
2837+
for _, f := range outGo {
2838+
if !strings.HasPrefix(filepath.Base(f), "_cgo_") {
2839+
continue
2840+
}
2841+
2842+
src, err := ioutil.ReadFile(f)
2843+
if err != nil {
2844+
return nil, nil, err
2845+
}
2846+
2847+
const cgoLdflag = "//go:cgo_ldflag"
2848+
idx := bytes.Index(src, []byte(cgoLdflag))
2849+
for idx >= 0 {
2850+
// We are looking at //go:cgo_ldflag.
2851+
// Find start of line.
2852+
start := bytes.LastIndex(src[:idx], []byte("\n"))
2853+
if start == -1 {
2854+
start = 0
2855+
}
2856+
2857+
// Find end of line.
2858+
end := bytes.Index(src[idx:], []byte("\n"))
2859+
if end == -1 {
2860+
end = len(src)
2861+
} else {
2862+
end += idx
2863+
}
2864+
2865+
// Check for first line comment in line.
2866+
// We don't worry about /* */ comments,
2867+
// which normally won't appear in files
2868+
// generated by cgo.
2869+
commentStart := bytes.Index(src[start:], []byte("//"))
2870+
commentStart += start
2871+
// If that line comment is //go:cgo_ldflag,
2872+
// it's a match.
2873+
if bytes.HasPrefix(src[commentStart:], []byte(cgoLdflag)) {
2874+
// Pull out the flag, and unquote it.
2875+
// This is what the compiler does.
2876+
flag := string(src[idx+len(cgoLdflag) : end])
2877+
flag = strings.TrimSpace(flag)
2878+
flag = strings.Trim(flag, `"`)
2879+
flags = append(flags, flag)
2880+
}
2881+
src = src[end:]
2882+
idx = bytes.Index(src, []byte(cgoLdflag))
2883+
}
2884+
}
2885+
if err := checkLinkerFlags("LDFLAGS", "go:cgo_ldflag", flags); err != nil {
2886+
return nil, nil, err
2887+
}
2888+
}
2889+
28302890
return outGo, outObj, nil
28312891
}
28322892

0 commit comments

Comments
 (0)