Skip to content

Commit f85c404

Browse files
rscgopherbot
authored andcommitted
internal/runtime/exithook: make safe for concurrent os.Exit
Real programs can call os.Exit concurrently from multiple goroutines. Make internal/runtime/exithook not crash in that case. The throw on panic also now runs in the deferred context, so that we will see the full stack trace that led to the panic. That should give us more visibility into the flaky failures on bugs #55167 and #56197 as well. Fixes #67631. Change-Id: Iefdf71b3a3b52a793ca88d89a9c270eb50ece094 Reviewed-on: https://go-review.googlesource.com/c/go/+/588235 Reviewed-by: Than McIntosh <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Auto-Submit: Russ Cox <[email protected]>
1 parent 378c48d commit f85c404

File tree

5 files changed

+82
-44
lines changed

5 files changed

+82
-44
lines changed

src/go/build/deps_test.go

+1-2
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@ var depsRules = `
5858
internal/nettrace,
5959
internal/platform,
6060
internal/profilerecord,
61-
internal/runtime/exithook,
6261
internal/trace/traceviewer/format,
6362
log/internal,
6463
math/bits,
@@ -79,7 +78,6 @@ var depsRules = `
7978
internal/goexperiment,
8079
internal/goos,
8180
internal/profilerecord,
82-
internal/runtime/exithook,
8381
math/bits
8482
< internal/bytealg
8583
< internal/stringslite
@@ -88,6 +86,7 @@ var depsRules = `
8886
< runtime/internal/sys
8987
< internal/runtime/syscall
9088
< internal/runtime/atomic
89+
< internal/runtime/exithook
9190
< runtime/internal/math
9291
< runtime
9392
< sync/atomic

src/internal/runtime/exithook/hooks.go

+37-16
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@
1313
// restricted dialects used for the trickier parts of the runtime.
1414
package exithook
1515

16+
import (
17+
"internal/runtime/atomic"
18+
_ "unsafe" // for linkname
19+
)
20+
1621
// A Hook is a function to be run at program termination
1722
// (when someone invokes os.Exit, or when main.main returns).
1823
// Hooks are run in reverse order of registration:
@@ -23,40 +28,56 @@ type Hook struct {
2328
}
2429

2530
var (
31+
locked atomic.Int32
32+
runGoid atomic.Uint64
2633
hooks []Hook
2734
running bool
35+
36+
// runtime sets these for us
37+
Gosched func()
38+
Goid func() uint64
39+
Throw func(string)
2840
)
2941

3042
// Add adds a new exit hook.
3143
func Add(h Hook) {
44+
for !locked.CompareAndSwap(0, 1) {
45+
Gosched()
46+
}
3247
hooks = append(hooks, h)
48+
locked.Store(0)
3349
}
3450

3551
// Run runs the exit hooks.
36-
// It returns an error if Run is already running or
37-
// if one of the hooks panics.
38-
func Run(code int) (err error) {
39-
if running {
40-
return exitError("exit hook invoked exit")
52+
//
53+
// If an exit hook panics, Run will throw with the panic on the stack.
54+
// If an exit hook invokes exit in the same goroutine, the goroutine will throw.
55+
// If an exit hook invokes exit in another goroutine, that exit will block.
56+
func Run(code int) {
57+
for !locked.CompareAndSwap(0, 1) {
58+
if Goid() == runGoid.Load() {
59+
Throw("exit hook invoked exit")
60+
}
61+
Gosched()
4162
}
42-
running = true
63+
defer locked.Store(0)
64+
runGoid.Store(Goid())
65+
defer runGoid.Store(0)
4366

4467
defer func() {
45-
if x := recover(); x != nil {
46-
err = exitError("exit hook invoked panic")
68+
if e := recover(); e != nil {
69+
Throw("exit hook invoked panic")
4770
}
4871
}()
4972

50-
local := hooks
51-
hooks = nil
52-
for i := len(local) - 1; i >= 0; i-- {
53-
h := local[i]
54-
if code == 0 || h.RunOnFailure {
55-
h.F()
73+
for len(hooks) > 0 {
74+
h := hooks[len(hooks)-1]
75+
hooks = hooks[:len(hooks)-1]
76+
if code != 0 && !h.RunOnFailure {
77+
continue
5678
}
79+
h.F()
5780
}
58-
running = false
59-
return nil
6081
}
6182

6283
type exitError string

src/runtime/ehooks_test.go

+24-22
Original file line numberDiff line numberDiff line change
@@ -28,32 +28,36 @@ func TestExitHooks(t *testing.T) {
2828
scenarios := []struct {
2929
mode string
3030
expected string
31-
musthave string
31+
musthave []string
3232
}{
3333
{
3434
mode: "simple",
3535
expected: "bar foo",
36-
musthave: "",
3736
},
3837
{
3938
mode: "goodexit",
4039
expected: "orange apple",
41-
musthave: "",
4240
},
4341
{
4442
mode: "badexit",
4543
expected: "blub blix",
46-
musthave: "",
4744
},
4845
{
49-
mode: "panics",
50-
expected: "",
51-
musthave: "fatal error: exit hook invoked panic",
46+
mode: "panics",
47+
musthave: []string{
48+
"fatal error: exit hook invoked panic",
49+
"main.testPanics",
50+
},
51+
},
52+
{
53+
mode: "callsexit",
54+
musthave: []string{
55+
"fatal error: exit hook invoked exit",
56+
},
5257
},
5358
{
54-
mode: "callsexit",
59+
mode: "exit2",
5560
expected: "",
56-
musthave: "fatal error: exit hook invoked exit",
5761
},
5862
}
5963

@@ -71,20 +75,18 @@ func TestExitHooks(t *testing.T) {
7175
out, _ := cmd.CombinedOutput()
7276
outs := strings.ReplaceAll(string(out), "\n", " ")
7377
outs = strings.TrimSpace(outs)
74-
if s.expected != "" {
75-
if s.expected != outs {
76-
t.Logf("raw output: %q", outs)
77-
t.Errorf("failed%s mode %s: wanted %q got %q", bt,
78-
s.mode, s.expected, outs)
79-
}
80-
} else if s.musthave != "" {
81-
if !strings.Contains(outs, s.musthave) {
82-
t.Logf("raw output: %q", outs)
83-
t.Errorf("failed mode %s: output does not contain %q",
84-
s.mode, s.musthave)
78+
if s.expected != "" && s.expected != outs {
79+
t.Fatalf("failed%s mode %s: wanted %q\noutput:\n%s", bt,
80+
s.mode, s.expected, outs)
81+
}
82+
for _, need := range s.musthave {
83+
if !strings.Contains(outs, need) {
84+
t.Fatalf("failed mode %s: output does not contain %q\noutput:\n%s",
85+
s.mode, need, outs)
8586
}
86-
} else {
87-
panic("badly written scenario")
87+
}
88+
if s.expected == "" && s.musthave == nil && outs != "" {
89+
t.Errorf("failed mode %s: wanted no output\noutput:\n%s", s.mode, outs)
8890
}
8991
}
9092
}

src/runtime/proc.go

+7-3
Original file line numberDiff line numberDiff line change
@@ -310,10 +310,14 @@ func os_beforeExit(exitCode int) {
310310
}
311311
}
312312

313+
func init() {
314+
exithook.Gosched = Gosched
315+
exithook.Goid = func() uint64 { return getg().goid }
316+
exithook.Throw = throw
317+
}
318+
313319
func runExitHooks(code int) {
314-
if err := exithook.Run(code); err != nil {
315-
throw(err.Error())
316-
}
320+
exithook.Run(code)
317321
}
318322

319323
// start forcegc helper goroutine

src/runtime/testdata/testexithooks/testexithooks.go

+13-1
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ package main
66

77
import (
88
"flag"
9-
"os"
109
"internal/runtime/exithook"
10+
"os"
11+
"time"
1112
_ "unsafe"
1213
)
1314

@@ -26,6 +27,8 @@ func main() {
2627
testPanics()
2728
case "callsexit":
2829
testHookCallsExit()
30+
case "exit2":
31+
testExit2()
2932
default:
3033
panic("unknown mode")
3134
}
@@ -81,3 +84,12 @@ func testHookCallsExit() {
8184
exithook.Add(exithook.Hook{F: f3, RunOnFailure: true})
8285
os.Exit(1)
8386
}
87+
88+
func testExit2() {
89+
f1 := func() { time.Sleep(100 * time.Millisecond) }
90+
exithook.Add(exithook.Hook{F: f1})
91+
for range 10 {
92+
go os.Exit(0)
93+
}
94+
os.Exit(0)
95+
}

0 commit comments

Comments
 (0)