Skip to content

Commit 3215982

Browse files
ianlancetaylorkatiehockman
authored andcommitted
[release-branch.go1.15-security] 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 Chris Brown and Tempus Ex for reporting this. Fixes CVE-2020-28366 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]> (cherry picked from commit 6bc814dd2bbfeaafa41d314dd4cc591b575dfbf6) Reviewed-on: https://team-review.git.corp.google.com/c/golang/go-private/+/901056 Reviewed-by: Filippo Valsorda <[email protected]> Reviewed-by: Roland Shoemaker <[email protected]>
1 parent 0e953ad commit 3215982

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
@@ -336,6 +336,8 @@ func dynimport(obj string) {
336336
if s.Version != "" {
337337
targ += "#" + s.Version
338338
}
339+
checkImportSymName(s.Name)
340+
checkImportSymName(targ)
339341
fmt.Fprintf(stdout, "//go:cgo_import_dynamic %s %s %q\n", s.Name, targ, s.Library)
340342
}
341343
lib, _ := f.ImportedLibraries()
@@ -351,6 +353,7 @@ func dynimport(obj string) {
351353
if len(s) > 0 && s[0] == '_' {
352354
s = s[1:]
353355
}
356+
checkImportSymName(s)
354357
fmt.Fprintf(stdout, "//go:cgo_import_dynamic %s %s %q\n", s, s, "")
355358
}
356359
lib, _ := f.ImportedLibraries()
@@ -365,6 +368,8 @@ func dynimport(obj string) {
365368
for _, s := range sym {
366369
ss := strings.Split(s, ":")
367370
name := strings.Split(ss[0], "@")[0]
371+
checkImportSymName(name)
372+
checkImportSymName(ss[0])
368373
fmt.Fprintf(stdout, "//go:cgo_import_dynamic %s %s %q\n", name, ss[0], strings.ToLower(ss[1]))
369374
}
370375
return
@@ -382,6 +387,7 @@ func dynimport(obj string) {
382387
// Go symbols.
383388
continue
384389
}
390+
checkImportSymName(s.Name)
385391
fmt.Fprintf(stdout, "//go:cgo_import_dynamic %s %s %q\n", s.Name, s.Name, s.Library)
386392
}
387393
lib, err := f.ImportedLibraries()
@@ -397,6 +403,23 @@ func dynimport(obj string) {
397403
fatalf("cannot parse %s as ELF, Mach-O, PE or XCOFF", obj)
398404
}
399405

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

2714+
// Double check the //go:cgo_ldflag comments in the generated files.
2715+
// The compiler only permits such comments in files whose base name
2716+
// starts with "_cgo_". Make sure that the comments in those files
2717+
// are safe. This is a backstop against people somehow smuggling
2718+
// such a comment into a file generated by cgo.
2719+
if cfg.BuildToolchainName == "gc" && !cfg.BuildN {
2720+
var flags []string
2721+
for _, f := range outGo {
2722+
if !strings.HasPrefix(filepath.Base(f), "_cgo_") {
2723+
continue
2724+
}
2725+
2726+
src, err := ioutil.ReadFile(f)
2727+
if err != nil {
2728+
return nil, nil, err
2729+
}
2730+
2731+
const cgoLdflag = "//go:cgo_ldflag"
2732+
idx := bytes.Index(src, []byte(cgoLdflag))
2733+
for idx >= 0 {
2734+
// We are looking at //go:cgo_ldflag.
2735+
// Find start of line.
2736+
start := bytes.LastIndex(src[:idx], []byte("\n"))
2737+
if start == -1 {
2738+
start = 0
2739+
}
2740+
2741+
// Find end of line.
2742+
end := bytes.Index(src[idx:], []byte("\n"))
2743+
if end == -1 {
2744+
end = len(src)
2745+
} else {
2746+
end += idx
2747+
}
2748+
2749+
// Check for first line comment in line.
2750+
// We don't worry about /* */ comments,
2751+
// which normally won't appear in files
2752+
// generated by cgo.
2753+
commentStart := bytes.Index(src[start:], []byte("//"))
2754+
commentStart += start
2755+
// If that line comment is //go:cgo_ldflag,
2756+
// it's a match.
2757+
if bytes.HasPrefix(src[commentStart:], []byte(cgoLdflag)) {
2758+
// Pull out the flag, and unquote it.
2759+
// This is what the compiler does.
2760+
flag := string(src[idx+len(cgoLdflag) : end])
2761+
flag = strings.TrimSpace(flag)
2762+
flag = strings.Trim(flag, `"`)
2763+
flags = append(flags, flag)
2764+
}
2765+
src = src[end:]
2766+
idx = bytes.Index(src, []byte(cgoLdflag))
2767+
}
2768+
}
2769+
if err := checkLinkerFlags("LDFLAGS", "go:cgo_ldflag", flags); err != nil {
2770+
return nil, nil, err
2771+
}
2772+
}
2773+
27142774
return outGo, outObj, nil
27152775
}
27162776

0 commit comments

Comments
 (0)