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