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

View as plain text