Source file src/runtime/runtime-seh_windows_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/runtime/sys"
    10  	"internal/syscall/windows"
    11  	"runtime"
    12  	"slices"
    13  	"testing"
    14  	"unsafe"
    15  )
    16  
    17  func sehf1() int {
    18  	return sehf1()
    19  }
    20  
    21  func sehf2() {}
    22  
    23  func TestSehLookupFunctionEntry(t *testing.T) {
    24  	if runtime.GOARCH != "amd64" {
    25  		t.Skip("skipping amd64-only test")
    26  	}
    27  	// This test checks that Win32 is able to retrieve
    28  	// function metadata stored in the .pdata section
    29  	// by the Go linker.
    30  	// Win32 unwinding will fail if this test fails,
    31  	// as RtlUnwindEx uses RtlLookupFunctionEntry internally.
    32  	// If that's the case, don't bother investigating further,
    33  	// first fix the .pdata generation.
    34  	sehf1pc := abi.FuncPCABIInternal(sehf1)
    35  	var fnwithframe func()
    36  	fnwithframe = func() {
    37  		fnwithframe()
    38  	}
    39  	fnwithoutframe := func() {}
    40  	tests := []struct {
    41  		name     string
    42  		pc       uintptr
    43  		hasframe bool
    44  	}{
    45  		{"no frame func", abi.FuncPCABIInternal(sehf2), false},
    46  		{"no func", sehf1pc - 1, false},
    47  		{"func at entry", sehf1pc, true},
    48  		{"func in prologue", sehf1pc + 1, true},
    49  		{"anonymous func with frame", abi.FuncPCABIInternal(fnwithframe), true},
    50  		{"anonymous func without frame", abi.FuncPCABIInternal(fnwithoutframe), false},
    51  		{"pc at func body", sys.GetCallerPC(), true},
    52  	}
    53  	for _, tt := range tests {
    54  		var base uintptr
    55  		fn := windows.RtlLookupFunctionEntry(tt.pc, &base, nil)
    56  		if !tt.hasframe {
    57  			if fn != nil {
    58  				t.Errorf("%s: unexpected frame", tt.name)
    59  			}
    60  			continue
    61  		}
    62  		if fn == nil {
    63  			t.Errorf("%s: missing frame", tt.name)
    64  		}
    65  	}
    66  }
    67  
    68  //go:noinline
    69  func newCtx() *windows.Context {
    70  	var ctx windows.Context
    71  	ctx.SetPC(sys.GetCallerPC())
    72  	ctx.SetSP(sys.GetCallerSP())
    73  	ctx.SetFP(runtime.GetCallerFp())
    74  	return &ctx
    75  }
    76  
    77  func sehCallers() []uintptr {
    78  	// We don't need a real context,
    79  	// RtlVirtualUnwind just needs a context with
    80  	// valid a pc, sp and fp (aka bp).
    81  	ctx := newCtx()
    82  
    83  	pcs := make([]uintptr, 15)
    84  	var base, frame uintptr
    85  	var n int
    86  	for i := 0; i < len(pcs); i++ {
    87  		fn := windows.RtlLookupFunctionEntry(ctx.PC(), &base, nil)
    88  		if fn == nil {
    89  			break
    90  		}
    91  		pcs[i] = ctx.PC()
    92  		n++
    93  		windows.RtlVirtualUnwind(0, base, ctx.PC(), fn, unsafe.Pointer(ctx), nil, &frame, nil)
    94  	}
    95  	return pcs[:n]
    96  }
    97  
    98  // SEH unwinding does not report inlined frames.
    99  //
   100  //go:noinline
   101  func sehf3(pan bool) []uintptr {
   102  	return sehf4(pan)
   103  }
   104  
   105  //go:noinline
   106  func sehf4(pan bool) []uintptr {
   107  	var pcs []uintptr
   108  	if pan {
   109  		panic("sehf4")
   110  	}
   111  	pcs = sehCallers()
   112  	return pcs
   113  }
   114  
   115  func testSehCallersEqual(t *testing.T, pcs []uintptr, want []string) {
   116  	t.Helper()
   117  	got := make([]string, 0, len(want))
   118  	for _, pc := range pcs {
   119  		fn := runtime.FuncForPC(pc)
   120  		if fn == nil || len(got) >= len(want) {
   121  			break
   122  		}
   123  		name := fn.Name()
   124  		switch name {
   125  		case "runtime.panicmem":
   126  			// These functions are skipped as they appear inconsistently depending
   127  			// whether inlining is on or off.
   128  			continue
   129  		case "runtime_test.sehCallers":
   130  			// This is an artifact of the implementation of sehCallers.
   131  			continue
   132  		}
   133  		got = append(got, name)
   134  	}
   135  	if !slices.Equal(want, got) {
   136  		t.Fatalf("wanted %v, got %v", want, got)
   137  	}
   138  }
   139  
   140  func TestSehUnwind(t *testing.T) {
   141  	if runtime.GOARCH != "amd64" {
   142  		t.Skip("skipping amd64-only test")
   143  	}
   144  	pcs := sehf3(false)
   145  	testSehCallersEqual(t, pcs, []string{"runtime_test.sehf4", "runtime_test.sehf3", "runtime_test.TestSehUnwind"})
   146  }
   147  
   148  func TestSehUnwindPanic(t *testing.T) {
   149  	if runtime.GOARCH != "amd64" {
   150  		t.Skip("skipping amd64-only test")
   151  	}
   152  	want := []string{"runtime_test.TestSehUnwindPanic.func1", "runtime.gopanic",
   153  		"runtime_test.sehf4", "runtime_test.sehf3", "runtime_test.TestSehUnwindPanic"}
   154  	defer func() {
   155  		if r := recover(); r == nil {
   156  			t.Fatal("did not panic")
   157  		}
   158  		pcs := sehCallers()
   159  		testSehCallersEqual(t, pcs, want)
   160  	}()
   161  	sehf3(true)
   162  }
   163  
   164  func TestSehUnwindDoublePanic(t *testing.T) {
   165  	if runtime.GOARCH != "amd64" {
   166  		t.Skip("skipping amd64-only test")
   167  	}
   168  	want := []string{"runtime_test.TestSehUnwindDoublePanic.func1.1", "runtime.gopanic",
   169  		"runtime_test.TestSehUnwindDoublePanic.func1", "runtime.gopanic", "runtime_test.TestSehUnwindDoublePanic"}
   170  	defer func() {
   171  		defer func() {
   172  			if recover() == nil {
   173  				t.Fatal("did not panic")
   174  			}
   175  			pcs := sehCallers()
   176  			testSehCallersEqual(t, pcs, want)
   177  		}()
   178  		if recover() == nil {
   179  			t.Fatal("did not panic")
   180  		}
   181  		panic(2)
   182  	}()
   183  	panic(1)
   184  }
   185  
   186  func TestSehUnwindNilPointerPanic(t *testing.T) {
   187  	if runtime.GOARCH != "amd64" {
   188  		t.Skip("skipping amd64-only test")
   189  	}
   190  	want := []string{"runtime_test.TestSehUnwindNilPointerPanic.func1", "runtime.gopanic",
   191  		"runtime.sigpanic", "runtime_test.TestSehUnwindNilPointerPanic"}
   192  	defer func() {
   193  		if r := recover(); r == nil {
   194  			t.Fatal("did not panic")
   195  		}
   196  		pcs := sehCallers()
   197  		testSehCallersEqual(t, pcs, want)
   198  	}()
   199  	var p *int
   200  	if *p == 3 {
   201  		t.Fatal("did not see nil pointer panic")
   202  	}
   203  }
   204  

View as plain text