Source file src/runtime/unsafepoint_test.go

     1  // Copyright 2023 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 runtime_test
     6  
     7  import (
     8  	"internal/abi"
     9  	"internal/testenv"
    10  	"os"
    11  	"os/exec"
    12  	"reflect"
    13  	"regexp"
    14  	"runtime"
    15  	"strconv"
    16  	"strings"
    17  	"testing"
    18  	"unsafe"
    19  )
    20  
    21  // This is the function we'll be testing.
    22  // It has a simple write barrier in it.
    23  func setGlobalPointer() {
    24  	globalPointer = nil
    25  }
    26  
    27  var globalPointer *int
    28  
    29  func TestUnsafePoint(t *testing.T) {
    30  	testenv.MustHaveExec(t)
    31  	switch runtime.GOARCH {
    32  	case "amd64", "arm64", "loong64":
    33  	default:
    34  		t.Skipf("test not enabled for %s", runtime.GOARCH)
    35  	}
    36  
    37  	// Get a reference we can use to ask the runtime about
    38  	// which of its instructions are unsafe preemption points.
    39  	f := runtime.FuncForPC(reflect.ValueOf(setGlobalPointer).Pointer())
    40  
    41  	// Disassemble the test function.
    42  	// Note that normally "go test runtime" would strip symbols
    43  	// and prevent this step from working. So there's a hack in
    44  	// cmd/go/internal/test that exempts runtime tests from
    45  	// symbol stripping.
    46  	cmd := exec.Command(testenv.GoToolPath(t), "tool", "objdump", "-s", "setGlobalPointer", os.Args[0])
    47  	out, err := cmd.CombinedOutput()
    48  	if err != nil {
    49  		t.Fatalf("can't objdump %v:\n%s", err, out)
    50  	}
    51  	lines := strings.Split(string(out), "\n")[1:]
    52  
    53  	// Walk through assembly instructions, checking preemptible flags.
    54  	var entry uint64
    55  	var startedWB bool
    56  	var doneWB bool
    57  	instructionCount := 0
    58  	unsafeCount := 0
    59  	for _, line := range lines {
    60  		line = strings.TrimSpace(line)
    61  		t.Logf("%s", line)
    62  		parts := strings.Fields(line)
    63  		if len(parts) < 4 {
    64  			continue
    65  		}
    66  		if !strings.HasPrefix(parts[0], "unsafepoint_test.go:") {
    67  			continue
    68  		}
    69  		pc, err := strconv.ParseUint(parts[1][2:], 16, 64)
    70  		if err != nil {
    71  			t.Fatalf("can't parse pc %s: %v", parts[1], err)
    72  		}
    73  		if entry == 0 {
    74  			entry = pc
    75  		}
    76  		// Note that some platforms do ASLR, so the PCs in the disassembly
    77  		// don't match PCs in the address space. Only offsets from function
    78  		// entry make sense.
    79  		unsafe := runtime.UnsafePoint(f.Entry() + uintptr(pc-entry))
    80  		t.Logf("unsafe: %v\n", unsafe)
    81  		instructionCount++
    82  		if unsafe {
    83  			unsafeCount++
    84  		}
    85  
    86  		// All the instructions inside the write barrier must be unpreemptible.
    87  		if startedWB && !doneWB && !unsafe {
    88  			t.Errorf("instruction %s must be marked unsafe, but isn't", parts[1])
    89  		}
    90  
    91  		// Detect whether we're in the write barrier.
    92  		switch runtime.GOARCH {
    93  		case "arm64":
    94  			if parts[3] == "MOVWU" {
    95  				// The unpreemptible region starts after the
    96  				// load of runtime.writeBarrier.
    97  				startedWB = true
    98  			}
    99  			if parts[3] == "MOVD" && parts[4] == "ZR," {
   100  				// The unpreemptible region ends after the
   101  				// write of nil.
   102  				doneWB = true
   103  			}
   104  		case "amd64":
   105  			if parts[3] == "CMPL" {
   106  				startedWB = true
   107  			}
   108  			if parts[3] == "MOVQ" && (parts[4] == "$0x0," || parts[4] == "X15,") {
   109  				doneWB = true
   110  			}
   111  		case "loong64":
   112  			if parts[3] == "MOVWU" {
   113  				// The unpreemptible region starts after the
   114  				// load of runtime.writeBarrier.enabled.
   115  				startedWB = true
   116  			}
   117  			if parts[3] == "MOVV" && parts[4] == "R0," {
   118  				// The unpreemptible region ends after the
   119  				// write of nil (R0 is the zero register).
   120  				doneWB = true
   121  			}
   122  		}
   123  	}
   124  
   125  	if instructionCount == 0 {
   126  		t.Errorf("no instructions")
   127  	}
   128  	if unsafeCount == instructionCount {
   129  		t.Errorf("no interruptible instructions")
   130  	}
   131  	// Note that there are other instructions marked unpreemptible besides
   132  	// just the ones required by the write barrier. Those include possibly
   133  	// the preamble and postamble, as well as bleeding out from the
   134  	// write barrier proper into adjacent instructions (in both directions).
   135  	// Hopefully we can clean up the latter at some point.
   136  }
   137  
   138  // tailCallOuter embeds an interface, so the compiler generates a wrapper for
   139  // the promoted method M. On ppc64 that wrapper ends in a tail call:
   140  //
   141  //	MOVD Rx, CTR
   142  //	BR   (CTR)
   143  //
   144  // runtime.asyncPreempt does not preserve CTR, and its resume sequence leaves
   145  // CTR holding the resume PC, so a goroutine preempted at that branch would
   146  // resume by branching to that very instruction and spin there forever. The
   147  // branch must therefore be marked as an unsafe point. See go.dev/issue/78576.
   148  type tailCallInner interface{ M() int }
   149  
   150  type tailCallImpl struct{}
   151  
   152  func (tailCallImpl) M() int { return 42 }
   153  
   154  type tailCallOuter struct{ tailCallInner }
   155  
   156  var tailCallValue tailCallInner = tailCallOuter{tailCallImpl{}}
   157  
   158  func TestUnsafePointTailCall(t *testing.T) {
   159  	switch runtime.GOARCH {
   160  	case "ppc64", "ppc64le":
   161  	default:
   162  		t.Skipf("test not enabled for %s", runtime.GOARCH)
   163  	}
   164  	testenv.MustHaveExec(t)
   165  
   166  	if got := tailCallValue.M(); got != 42 {
   167  		t.Fatalf("tailCallValue.M() = %d, want 42", got)
   168  	}
   169  
   170  	// The itab's first method slot holds the generated wrapper for
   171  	// tailCallOuter.M, which is the function containing the tail call.
   172  	iface := (*struct {
   173  		tab  *abi.ITab
   174  		data unsafe.Pointer
   175  	})(unsafe.Pointer(&tailCallValue))
   176  	f := runtime.FuncForPC(iface.tab.Fun[0])
   177  	if f == nil {
   178  		t.Fatal("no func for the tailCallOuter.M wrapper")
   179  	}
   180  
   181  	// See TestUnsafePoint for why objdump works here.
   182  	cmd := exec.Command(testenv.GoToolPath(t), "tool", "objdump", "-s", "^"+regexp.QuoteMeta(f.Name())+"$", os.Args[0])
   183  	out, err := cmd.CombinedOutput()
   184  	if err != nil {
   185  		t.Fatalf("can't objdump %v:\n%s", err, out)
   186  	}
   187  
   188  	// Walk the disassembly and check the branch through CTR. As in
   189  	// TestUnsafePoint, only offsets from the function entry are meaningful.
   190  	var entry uint64
   191  	branches := 0
   192  	for _, line := range strings.Split(string(out), "\n")[1:] {
   193  		parts := strings.Fields(strings.TrimSpace(line))
   194  		if len(parts) < 4 || !strings.HasPrefix(parts[0], "<autogenerated>:") {
   195  			continue
   196  		}
   197  		pc, err := strconv.ParseUint(parts[1][2:], 16, 64)
   198  		if err != nil {
   199  			t.Fatalf("can't parse pc %s: %v", parts[1], err)
   200  		}
   201  		if entry == 0 {
   202  			entry = pc
   203  		}
   204  		t.Logf("%s", strings.TrimSpace(line))
   205  		if parts[3] != "BR" || parts[4] != "(CTR)" {
   206  			continue
   207  		}
   208  		branches++
   209  		if !runtime.UnsafePoint(f.Entry() + uintptr(pc-entry)) {
   210  			t.Errorf("%s\n\tbranch through CTR must be marked unsafe, but isn't", strings.TrimSpace(line))
   211  		}
   212  	}
   213  	if branches != 1 {
   214  		t.Errorf("found %d branches through CTR in %s, want 1; output:\n%s", branches, f.Name(), out)
   215  	}
   216  }
   217  

View as plain text