Source file src/cmd/cgo/internal/testerrors/badsym_test.go

     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  	"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  
    22  const magicInput = "abcdefghijklmnopqrstuvwxyz0123"
    23  const magicReplace = "\n//go:cgo_ldflag \"-badflag\"\n//"
    24  
    25  const cSymbol = "BadSymbol" + magicInput + "Name"
    26  const cDefSource = "int " + cSymbol + " = 1;"
    27  const 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.
    31  const goSource = `
    32  package main
    33  
    34  // #cgo LDFLAGS: TMPDIR/cbad.o TMPDIR/cbad.so
    35  // extern int F();
    36  import "C"
    37  
    38  func main() {
    39  	println(C.F())
    40  }
    41  `
    42  
    43  func 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  
   174  func 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  
   223  func 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  

View as plain text