Skip to content

Commit

Permalink
internal/runtime/exithook: make safe for concurrent os.Exit
Browse files Browse the repository at this point in the history
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 golang#55167 and golang#56197 as well.

Fixes golang#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]>
  • Loading branch information
rsc authored and gopherbot committed May 24, 2024
1 parent 378c48d commit f85c404
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 44 deletions.
3 changes: 1 addition & 2 deletions src/go/build/deps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ var depsRules = `
internal/nettrace,
internal/platform,
internal/profilerecord,
internal/runtime/exithook,
internal/trace/traceviewer/format,
log/internal,
math/bits,
Expand All @@ -79,7 +78,6 @@ var depsRules = `
internal/goexperiment,
internal/goos,
internal/profilerecord,
internal/runtime/exithook,
math/bits
< internal/bytealg
< internal/stringslite
Expand All @@ -88,6 +86,7 @@ var depsRules = `
< runtime/internal/sys
< internal/runtime/syscall
< internal/runtime/atomic
< internal/runtime/exithook
< runtime/internal/math
< runtime
< sync/atomic
Expand Down
53 changes: 37 additions & 16 deletions src/internal/runtime/exithook/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@
// restricted dialects used for the trickier parts of the runtime.
package exithook

import (
"internal/runtime/atomic"
_ "unsafe" // for linkname
)

// A Hook is a function to be run at program termination
// (when someone invokes os.Exit, or when main.main returns).
// Hooks are run in reverse order of registration:
Expand All @@ -23,40 +28,56 @@ type Hook struct {
}

var (
locked atomic.Int32
runGoid atomic.Uint64
hooks []Hook
running bool

// runtime sets these for us
Gosched func()
Goid func() uint64
Throw func(string)
)

// Add adds a new exit hook.
func Add(h Hook) {
for !locked.CompareAndSwap(0, 1) {
Gosched()
}
hooks = append(hooks, h)
locked.Store(0)
}

// Run runs the exit hooks.
// It returns an error if Run is already running or
// if one of the hooks panics.
func Run(code int) (err error) {
if running {
return exitError("exit hook invoked exit")
//
// If an exit hook panics, Run will throw with the panic on the stack.
// If an exit hook invokes exit in the same goroutine, the goroutine will throw.
// If an exit hook invokes exit in another goroutine, that exit will block.
func Run(code int) {
for !locked.CompareAndSwap(0, 1) {
if Goid() == runGoid.Load() {
Throw("exit hook invoked exit")
}
Gosched()
}
running = true
defer locked.Store(0)
runGoid.Store(Goid())
defer runGoid.Store(0)

defer func() {
if x := recover(); x != nil {
err = exitError("exit hook invoked panic")
if e := recover(); e != nil {
Throw("exit hook invoked panic")
}
}()

local := hooks
hooks = nil
for i := len(local) - 1; i >= 0; i-- {
h := local[i]
if code == 0 || h.RunOnFailure {
h.F()
for len(hooks) > 0 {
h := hooks[len(hooks)-1]
hooks = hooks[:len(hooks)-1]
if code != 0 && !h.RunOnFailure {
continue
}
h.F()
}
running = false
return nil
}

type exitError string
Expand Down
46 changes: 24 additions & 22 deletions src/runtime/ehooks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,32 +28,36 @@ func TestExitHooks(t *testing.T) {
scenarios := []struct {
mode string
expected string
musthave string
musthave []string
}{
{
mode: "simple",
expected: "bar foo",
musthave: "",
},
{
mode: "goodexit",
expected: "orange apple",
musthave: "",
},
{
mode: "badexit",
expected: "blub blix",
musthave: "",
},
{
mode: "panics",
expected: "",
musthave: "fatal error: exit hook invoked panic",
mode: "panics",
musthave: []string{
"fatal error: exit hook invoked panic",
"main.testPanics",
},
},
{
mode: "callsexit",
musthave: []string{
"fatal error: exit hook invoked exit",
},
},
{
mode: "callsexit",
mode: "exit2",
expected: "",
musthave: "fatal error: exit hook invoked exit",
},
}

Expand All @@ -71,20 +75,18 @@ func TestExitHooks(t *testing.T) {
out, _ := cmd.CombinedOutput()
outs := strings.ReplaceAll(string(out), "\n", " ")
outs = strings.TrimSpace(outs)
if s.expected != "" {
if s.expected != outs {
t.Logf("raw output: %q", outs)
t.Errorf("failed%s mode %s: wanted %q got %q", bt,
s.mode, s.expected, outs)
}
} else if s.musthave != "" {
if !strings.Contains(outs, s.musthave) {
t.Logf("raw output: %q", outs)
t.Errorf("failed mode %s: output does not contain %q",
s.mode, s.musthave)
if s.expected != "" && s.expected != outs {
t.Fatalf("failed%s mode %s: wanted %q\noutput:\n%s", bt,
s.mode, s.expected, outs)
}
for _, need := range s.musthave {
if !strings.Contains(outs, need) {
t.Fatalf("failed mode %s: output does not contain %q\noutput:\n%s",
s.mode, need, outs)
}
} else {
panic("badly written scenario")
}
if s.expected == "" && s.musthave == nil && outs != "" {
t.Errorf("failed mode %s: wanted no output\noutput:\n%s", s.mode, outs)
}
}
}
Expand Down
10 changes: 7 additions & 3 deletions src/runtime/proc.go
Original file line number Diff line number Diff line change
Expand Up @@ -310,10 +310,14 @@ func os_beforeExit(exitCode int) {
}
}

func init() {
exithook.Gosched = Gosched
exithook.Goid = func() uint64 { return getg().goid }
exithook.Throw = throw
}

func runExitHooks(code int) {
if err := exithook.Run(code); err != nil {
throw(err.Error())
}
exithook.Run(code)
}

// start forcegc helper goroutine
Expand Down
14 changes: 13 additions & 1 deletion src/runtime/testdata/testexithooks/testexithooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ package main

import (
"flag"
"os"
"internal/runtime/exithook"
"os"
"time"
_ "unsafe"
)

Expand All @@ -26,6 +27,8 @@ func main() {
testPanics()
case "callsexit":
testHookCallsExit()
case "exit2":
testExit2()
default:
panic("unknown mode")
}
Expand Down Expand Up @@ -81,3 +84,12 @@ func testHookCallsExit() {
exithook.Add(exithook.Hook{F: f3, RunOnFailure: true})
os.Exit(1)
}

func testExit2() {
f1 := func() { time.Sleep(100 * time.Millisecond) }
exithook.Add(exithook.Hook{F: f1})
for range 10 {
go os.Exit(0)
}
os.Exit(0)
}

0 comments on commit f85c404

Please sign in to comment.