1// run
2
3//go:build !nacl && !js && !aix && !wasip1 && !gcflags_noopt && gc
4
5// Copyright 2014 The Go Authors. All rights reserved.
6// Use of this source code is governed by a BSD-style
7// license that can be found in the LICENSE file.
8
9package main
10
11import (
12	"bytes"
13	"fmt"
14	"io/ioutil"
15	"log"
16	"os"
17	"os/exec"
18	"path/filepath"
19	"regexp"
20	"runtime"
21	"strconv"
22	"strings"
23)
24
25const debug = false
26
27var tests = `
28# These are test cases for the linker analysis that detects chains of
29# nosplit functions that would cause a stack overflow.
30#
31# Lines beginning with # are comments.
32#
33# Each test case describes a sequence of functions, one per line.
34# Each function definition is the function name, then the frame size,
35# then optionally the keyword 'nosplit', then the body of the function.
36# The body is assembly code, with some shorthands.
37# The shorthand 'call x' stands for CALL x(SB).
38# The shorthand 'callind' stands for 'CALL R0', where R0 is a register.
39# Each test case must define a function named start, and it must be first.
40# That is, a line beginning "start " indicates the start of a new test case.
41# Within a stanza, ; can be used instead of \n to separate lines.
42#
43# After the function definition, the test case ends with an optional
44# REJECT line, specifying the architectures on which the case should
45# be rejected. "REJECT" without any architectures means reject on all architectures.
46# The linker should accept the test case on systems not explicitly rejected.
47#
48# 64-bit systems do not attempt to execute test cases with frame sizes
49# that are only 32-bit aligned.
50
51# Ordinary function should work
52start 0
53
54# Large frame marked nosplit is always wrong.
55# Frame is so large it overflows cmd/link's int16.
56start 100000 nosplit
57REJECT
58
59# Calling a large frame is okay.
60start 0 call big
61big 10000
62
63# But not if the frame is nosplit.
64start 0 call big
65big 10000 nosplit
66REJECT
67
68# Recursion is okay.
69start 0 call start
70
71# Recursive nosplit runs out of space.
72start 0 nosplit call start
73REJECT
74
75# Non-trivial recursion runs out of space.
76start 0 call f1
77f1 0 nosplit call f2
78f2 0 nosplit call f1
79REJECT
80# Same but cycle starts below nosplit entry.
81start 0 call f1
82f1 0 nosplit call f2
83f2 0 nosplit call f3
84f3 0 nosplit call f2
85REJECT
86
87# Chains of ordinary functions okay.
88start 0 call f1
89f1 80 call f2
90f2 80
91
92# Chains of nosplit must fit in the stack limit, 128 bytes.
93start 0 call f1
94f1 80 nosplit call f2
95f2 80 nosplit
96REJECT
97
98# Larger chains.
99start 0 call f1
100f1 16 call f2
101f2 16 call f3
102f3 16 call f4
103f4 16 call f5
104f5 16 call f6
105f6 16 call f7
106f7 16 call f8
107f8 16 call end
108end 1000
109
110start 0 call f1
111f1 16 nosplit call f2
112f2 16 nosplit call f3
113f3 16 nosplit call f4
114f4 16 nosplit call f5
115f5 16 nosplit call f6
116f6 16 nosplit call f7
117f7 16 nosplit call f8
118f8 16 nosplit call end
119end 1000
120REJECT
121
122# Two paths both go over the stack limit.
123start 0 call f1
124f1 80 nosplit call f2 call f3
125f2 40 nosplit call f4
126f3 96 nosplit
127f4 40 nosplit
128REJECT
129
130# Test cases near the 128-byte limit.
131
132# Ordinary stack split frame is always okay.
133start 112
134start 116
135start 120
136start 124
137start 128
138start 132
139start 136
140
141# A nosplit leaf can use the whole 128-CallSize bytes available on entry.
142# (CallSize is 32 on ppc64, 8 on amd64 for frame pointer.)
143start 96 nosplit
144start 100 nosplit; REJECT ppc64 ppc64le
145start 104 nosplit; REJECT ppc64 ppc64le arm64
146start 108 nosplit; REJECT ppc64 ppc64le
147start 112 nosplit; REJECT ppc64 ppc64le arm64
148start 116 nosplit; REJECT ppc64 ppc64le
149start 120 nosplit; REJECT ppc64 ppc64le amd64 arm64
150start 124 nosplit; REJECT ppc64 ppc64le amd64
151start 128 nosplit; REJECT
152start 132 nosplit; REJECT
153start 136 nosplit; REJECT
154
155# Calling a nosplit function from a nosplit function requires
156# having room for the saved caller PC and the called frame.
157# Because ARM doesn't save LR in the leaf, it gets an extra 4 bytes.
158# Because arm64 doesn't save LR in the leaf, it gets an extra 8 bytes.
159# ppc64 doesn't save LR in the leaf, but CallSize is 32, so it gets 24 bytes.
160# Because AMD64 uses frame pointer, it has 8 fewer bytes.
161start 96 nosplit call f; f 0 nosplit
162start 100 nosplit call f; f 0 nosplit; REJECT ppc64 ppc64le
163start 104 nosplit call f; f 0 nosplit; REJECT ppc64 ppc64le arm64
164start 108 nosplit call f; f 0 nosplit; REJECT ppc64 ppc64le
165start 112 nosplit call f; f 0 nosplit; REJECT ppc64 ppc64le amd64 arm64
166start 116 nosplit call f; f 0 nosplit; REJECT ppc64 ppc64le amd64
167start 120 nosplit call f; f 0 nosplit; REJECT ppc64 ppc64le amd64 arm64
168start 124 nosplit call f; f 0 nosplit; REJECT ppc64 ppc64le amd64 386
169start 128 nosplit call f; f 0 nosplit; REJECT
170start 132 nosplit call f; f 0 nosplit; REJECT
171start 136 nosplit call f; f 0 nosplit; REJECT
172
173# Calling a splitting function from a nosplit function requires
174# having room for the saved caller PC of the call but also the
175# saved caller PC for the call to morestack.
176# Architectures differ in the same way as before.
177start 96 nosplit call f; f 0 call f
178start 100 nosplit call f; f 0 call f; REJECT ppc64 ppc64le
179start 104 nosplit call f; f 0 call f; REJECT ppc64 ppc64le amd64 arm64
180start 108 nosplit call f; f 0 call f; REJECT ppc64 ppc64le amd64
181start 112 nosplit call f; f 0 call f; REJECT ppc64 ppc64le amd64 arm64
182start 116 nosplit call f; f 0 call f; REJECT ppc64 ppc64le amd64
183start 120 nosplit call f; f 0 call f; REJECT ppc64 ppc64le amd64 386 arm64
184start 124 nosplit call f; f 0 call f; REJECT ppc64 ppc64le amd64 386
185start 128 nosplit call f; f 0 call f; REJECT
186start 132 nosplit call f; f 0 call f; REJECT
187start 136 nosplit call f; f 0 call f; REJECT
188
189# Indirect calls are assumed to be splitting functions.
190start 96 nosplit callind
191start 100 nosplit callind; REJECT ppc64 ppc64le
192start 104 nosplit callind; REJECT ppc64 ppc64le amd64 arm64
193start 108 nosplit callind; REJECT ppc64 ppc64le amd64
194start 112 nosplit callind; REJECT ppc64 ppc64le amd64 arm64
195start 116 nosplit callind; REJECT ppc64 ppc64le amd64
196start 120 nosplit callind; REJECT ppc64 ppc64le amd64 386 arm64
197start 124 nosplit callind; REJECT ppc64 ppc64le amd64 386
198start 128 nosplit callind; REJECT
199start 132 nosplit callind; REJECT
200start 136 nosplit callind; REJECT
201
202# Issue 7623
203start 0 call f; f 112
204start 0 call f; f 116
205start 0 call f; f 120
206start 0 call f; f 124
207start 0 call f; f 128
208start 0 call f; f 132
209start 0 call f; f 136
210`
211
212var (
213	commentRE = regexp.MustCompile(`(?m)^#.*`)
214	rejectRE  = regexp.MustCompile(`(?s)\A(.+?)((\n|; *)REJECT(.*))?\z`)
215	lineRE    = regexp.MustCompile(`(\w+) (\d+)( nosplit)?(.*)`)
216	callRE    = regexp.MustCompile(`\bcall (\w+)\b`)
217	callindRE = regexp.MustCompile(`\bcallind\b`)
218)
219
220func main() {
221	goarch := os.Getenv("GOARCH")
222	if goarch == "" {
223		goarch = runtime.GOARCH
224	}
225
226	dir, err := ioutil.TempDir("", "go-test-nosplit")
227	if err != nil {
228		bug()
229		fmt.Printf("creating temp dir: %v\n", err)
230		return
231	}
232	defer os.RemoveAll(dir)
233	os.Setenv("GOPATH", filepath.Join(dir, "_gopath"))
234
235	if err := ioutil.WriteFile(filepath.Join(dir, "go.mod"), []byte("module go-test-nosplit\n"), 0666); err != nil {
236		log.Panic(err)
237	}
238
239	tests = strings.Replace(tests, "\t", " ", -1)
240	tests = commentRE.ReplaceAllString(tests, "")
241
242	nok := 0
243	nfail := 0
244TestCases:
245	for len(tests) > 0 {
246		var stanza string
247		i := strings.Index(tests, "\nstart ")
248		if i < 0 {
249			stanza, tests = tests, ""
250		} else {
251			stanza, tests = tests[:i], tests[i+1:]
252		}
253
254		m := rejectRE.FindStringSubmatch(stanza)
255		if m == nil {
256			bug()
257			fmt.Printf("invalid stanza:\n\t%s\n", indent(stanza))
258			continue
259		}
260		lines := strings.TrimSpace(m[1])
261		reject := false
262		if m[2] != "" {
263			if strings.TrimSpace(m[4]) == "" {
264				reject = true
265			} else {
266				for _, rej := range strings.Fields(m[4]) {
267					if rej == goarch {
268						reject = true
269					}
270				}
271			}
272		}
273		if lines == "" && !reject {
274			continue
275		}
276
277		var gobuf bytes.Buffer
278		fmt.Fprintf(&gobuf, "package main\n")
279
280		var buf bytes.Buffer
281		ptrSize := 4
282		switch goarch {
283		case "mips", "mipsle":
284			fmt.Fprintf(&buf, "#define REGISTER (R0)\n")
285		case "mips64", "mips64le":
286			ptrSize = 8
287			fmt.Fprintf(&buf, "#define REGISTER (R0)\n")
288		case "loong64":
289			ptrSize = 8
290			fmt.Fprintf(&buf, "#define REGISTER (R0)\n")
291		case "ppc64", "ppc64le":
292			ptrSize = 8
293			fmt.Fprintf(&buf, "#define REGISTER (CTR)\n")
294		case "arm":
295			fmt.Fprintf(&buf, "#define REGISTER (R0)\n")
296		case "arm64":
297			ptrSize = 8
298			fmt.Fprintf(&buf, "#define REGISTER (R0)\n")
299		case "amd64":
300			ptrSize = 8
301			fmt.Fprintf(&buf, "#define REGISTER AX\n")
302		case "riscv64":
303			ptrSize = 8
304			fmt.Fprintf(&buf, "#define REGISTER A0\n")
305		case "s390x":
306			ptrSize = 8
307			fmt.Fprintf(&buf, "#define REGISTER R10\n")
308		default:
309			fmt.Fprintf(&buf, "#define REGISTER AX\n")
310		}
311
312		// Since all of the functions we're generating are
313		// ABI0, first enter ABI0 via a splittable function
314		// and then go to the chain we're testing. This way we
315		// don't have to account for ABI wrappers in the chain.
316		fmt.Fprintf(&gobuf, "func main0()\n")
317		fmt.Fprintf(&gobuf, "func main() { main0() }\n")
318		fmt.Fprintf(&buf, "TEXT ·main0(SB),0,$0-0\n\tCALL ·start(SB)\n")
319
320		adjusted := false
321		for _, line := range strings.Split(lines, "\n") {
322			line = strings.TrimSpace(line)
323			if line == "" {
324				continue
325			}
326			for _, subline := range strings.Split(line, ";") {
327				subline = strings.TrimSpace(subline)
328				if subline == "" {
329					continue
330				}
331				m := lineRE.FindStringSubmatch(subline)
332				if m == nil {
333					bug()
334					fmt.Printf("invalid function line: %s\n", subline)
335					continue TestCases
336				}
337				name := m[1]
338				size, _ := strconv.Atoi(m[2])
339
340				if size%ptrSize == 4 {
341					continue TestCases
342				}
343				nosplit := m[3]
344				body := m[4]
345
346				// The limit was originally 128 but is now 800.
347				// Instead of rewriting the test cases above, adjust
348				// the first nosplit frame to use up the extra bytes.
349				// This isn't exactly right because we could have
350				// nosplit -> split -> nosplit, but it's good enough.
351				if !adjusted && nosplit != "" {
352					const stackNosplitBase = 800 // internal/abi.StackNosplitBase
353					adjusted = true
354					size += stackNosplitBase - 128
355				}
356
357				if nosplit != "" {
358					nosplit = ",7"
359				} else {
360					nosplit = ",0"
361				}
362				body = callRE.ReplaceAllString(body, "CALL ·$1(SB);")
363				body = callindRE.ReplaceAllString(body, "CALL REGISTER;")
364
365				fmt.Fprintf(&gobuf, "func %s()\n", name)
366				fmt.Fprintf(&buf, "TEXT ·%s(SB)%s,$%d-0\n\t%s\n\tRET\n\n", name, nosplit, size, body)
367			}
368		}
369
370		if debug {
371			fmt.Printf("===\n%s\n", strings.TrimSpace(stanza))
372			fmt.Printf("-- main.go --\n%s", gobuf.String())
373			fmt.Printf("-- asm.s --\n%s", buf.String())
374		}
375
376		if err := ioutil.WriteFile(filepath.Join(dir, "asm.s"), buf.Bytes(), 0666); err != nil {
377			log.Fatal(err)
378		}
379		if err := ioutil.WriteFile(filepath.Join(dir, "main.go"), gobuf.Bytes(), 0666); err != nil {
380			log.Fatal(err)
381		}
382
383		cmd := exec.Command("go", "build")
384		cmd.Dir = dir
385		output, err := cmd.CombinedOutput()
386		if err == nil {
387			nok++
388			if reject {
389				bug()
390				fmt.Printf("accepted incorrectly:\n\t%s\n", indent(strings.TrimSpace(stanza)))
391			}
392		} else {
393			nfail++
394			if !reject {
395				bug()
396				fmt.Printf("rejected incorrectly:\n\t%s\n", indent(strings.TrimSpace(stanza)))
397				fmt.Printf("\n\tlinker output:\n\t%s\n", indent(string(output)))
398			}
399		}
400	}
401
402	if !bugged && (nok == 0 || nfail == 0) {
403		bug()
404		fmt.Printf("not enough test cases run\n")
405	}
406}
407
408func indent(s string) string {
409	return strings.Replace(s, "\n", "\n\t", -1)
410}
411
412var bugged = false
413
414func bug() {
415	if !bugged {
416		bugged = true
417		fmt.Printf("BUG\n")
418	}
419}
420