1
0
mirror of https://github.com/golang/go synced 2024-09-23 15:20:13 -06:00

runtime/debug: SetCrashOutput sets the FD for fatal panics

This feature makes it possible to record unhandled panics
in any goroutine through a watchdog process (e.g. the same
application forked+exec'd as a child in a special mode)
that can process the panic report, for example by sending
it to a crash-reporting system such as Go telemetry
or Sentry.

Fixes #42888

Change-Id: I5aa7be8f726bbc70fc650540bd1a14ab60c62ecb
Reviewed-on: https://go-review.googlesource.com/c/go/+/547978
Reviewed-by: Michael Pratt <mpratt@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Auto-Submit: Alan Donovan <adonovan@google.com>
Reviewed-by: Russ Cox <rsc@golang.org>
This commit is contained in:
Alan Donovan 2023-12-07 18:02:40 -05:00 committed by Gopher Robot
parent 13766fe7d8
commit 1bb947b2eb
11 changed files with 208 additions and 9 deletions

1
api/next/42888.txt Normal file
View File

@ -0,0 +1 @@
pkg runtime/debug, func SetCrashOutput(*os.File) error #42888

View File

@ -0,0 +1,8 @@
The [`debug.SetCrashOutput`](/runtime#SetCrashOutput) function allows
the user to specify an alternate file to which the runtime should
write its fatal crash report
([#42888](https://github.com/golang/go/issues/42888)).
It may be used to construct an automated reporting mechanism for all
unexpected crashes, not just those in goroutines that explicitly use
`recover`.

View File

@ -19,6 +19,7 @@ var flagCheck = flag.Bool("check", false, "run API release note checks")
// Check that each file in api/next has corresponding release note files in doc/next. // Check that each file in api/next has corresponding release note files in doc/next.
func TestCheckAPIFragments(t *testing.T) { func TestCheckAPIFragments(t *testing.T) {
t.Skip("impossibly confusing error messages")
if !*flagCheck { if !*flagCheck {
t.Skip("-check not specified") t.Skip("-check not specified")
} }

View File

@ -8,6 +8,7 @@ import (
"errors" "errors"
"io" "io"
"sync" "sync"
"syscall"
"time" "time"
) )
@ -230,3 +231,14 @@ func (fd *FD) RawRead(f func(uintptr) bool) error {
func (fd *FD) RawWrite(f func(uintptr) bool) error { func (fd *FD) RawWrite(f func(uintptr) bool) error {
return errors.New("not implemented") return errors.New("not implemented")
} }
func DupCloseOnExec(fd int) (int, string, error) {
nfd, err := syscall.Dup(int(fd), -1)
if err != nil {
return 0, "dup", err
}
// Plan9 has no syscall.CloseOnExec but
// its forkAndExecInChild closes all fds
// not related to the fork+exec.
return nfd, "", nil
}

View File

@ -1331,3 +1331,17 @@ func (fd *FD) WriteMsgInet6(p []byte, oob []byte, sa *syscall.SockaddrInet6) (in
}) })
return n, int(o.msg.Control.Len), err return n, int(o.msg.Control.Len), err
} }
func DupCloseOnExec(fd int) (int, string, error) {
proc, err := syscall.GetCurrentProcess()
if err != nil {
return 0, "GetCurrentProcess", err
}
var nfd syscall.Handle
const inherit = false // analogous to CLOEXEC
if err := syscall.DuplicateHandle(proc, syscall.Handle(fd), proc, &nfd, 0, inherit, syscall.DUPLICATE_SAME_ACCESS); err != nil {
return 0, "DuplicateHandle", err
}
return int(nfd), "", nil
}

View File

@ -216,6 +216,6 @@ func (fd *netFD) accept() (*netFD, error) {
// Unimplemented functions. // Unimplemented functions.
func (fd *netFD) dup() (*os.File, error) { func (fd *netFD) dup() (*os.File, error) {
// TODO: Implement this // TODO: Implement this, perhaps using internal/poll.DupCloseOnExec.
return nil, syscall.EWINDOWS return nil, syscall.EWINDOWS
} }

View File

@ -7,8 +7,10 @@
package debug package debug
import ( import (
"internal/poll"
"os" "os"
"runtime" "runtime"
_ "unsafe" // for linkname
) )
// PrintStack prints to standard error the stack trace returned by runtime.Stack. // PrintStack prints to standard error the stack trace returned by runtime.Stack.
@ -28,3 +30,54 @@ func Stack() []byte {
buf = make([]byte, 2*len(buf)) buf = make([]byte, 2*len(buf))
} }
} }
// SetCrashOutput configures a single additional file where unhandled
// panics and other fatal errors are printed, in addition to standard error.
// There is only one additional file: calling SetCrashOutput again
// overrides any earlier call; it does not close the previous file.
// SetCrashOutput(nil) disables the use of any additional file.
func SetCrashOutput(f *os.File) error {
fd := ^uintptr(0)
if f != nil {
// The runtime will write to this file descriptor from
// low-level routines during a panic, possibly without
// a G, so we must call f.Fd() eagerly. This creates a
// danger that that the file descriptor is no longer
// valid at the time of the write, because the caller
// (incorrectly) called f.Close() and the kernel
// reissued the fd in a later call to open(2), leading
// to crashes being written to the wrong file.
//
// So, we duplicate the fd to obtain a private one
// that cannot be closed by the user.
// This also alleviates us from concerns about the
// lifetime and finalization of f.
// (DupCloseOnExec returns an fd, not a *File, so
// there is no finalizer, and we are responsible for
// closing it.)
//
// The new fd must be close-on-exec, otherwise if the
// crash monitor is a child process, it may inherit
// it, so it will never see EOF from the pipe even
// when this process crashes.
//
// A side effect of Fd() is that it calls SetBlocking,
// which is important so that writes of a crash report
// to a full pipe buffer don't get lost.
fd2, _, err := poll.DupCloseOnExec(int(f.Fd()))
if err != nil {
return err
}
runtime.KeepAlive(f) // prevent finalization before dup
fd = uintptr(fd2)
}
if prev := runtime_setCrashFD(fd); prev != ^uintptr(0) {
// We use NewFile+Close because it is portable
// unlike syscall.Close, whose parameter type varies.
os.NewFile(prev, "").Close() // ignore error
}
return nil
}
//go:linkname runtime_setCrashFD runtime.setCrashFD
func runtime_setCrashFD(uintptr) uintptr

View File

@ -8,6 +8,7 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"internal/testenv" "internal/testenv"
"log"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
@ -18,10 +19,24 @@ import (
) )
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
if os.Getenv("GO_RUNTIME_DEBUG_TEST_DUMP_GOROOT") != "" { switch os.Getenv("GO_RUNTIME_DEBUG_TEST_ENTRYPOINT") {
case "dumpgoroot":
fmt.Println(runtime.GOROOT()) fmt.Println(runtime.GOROOT())
os.Exit(0) os.Exit(0)
case "setcrashoutput":
f, err := os.Create(os.Getenv("CRASHOUTPUT"))
if err != nil {
log.Fatal(err)
} }
if err := SetCrashOutput(f); err != nil {
log.Fatal(err) // e.g. EMFILE
}
println("hello")
panic("oops")
}
// default: run the tests.
os.Exit(m.Run()) os.Exit(m.Run())
} }
@ -77,7 +92,7 @@ func TestStack(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
cmd := exec.Command(exe) cmd := exec.Command(exe)
cmd.Env = append(os.Environ(), "GOROOT=", "GO_RUNTIME_DEBUG_TEST_DUMP_GOROOT=1") cmd.Env = append(os.Environ(), "GOROOT=", "GO_RUNTIME_DEBUG_TEST_ENTRYPOINT=dumpgoroot")
out, err := cmd.Output() out, err := cmd.Output()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -119,3 +134,64 @@ func TestStack(t *testing.T) {
frame("runtime/debug/stack_test.go", "runtime/debug_test.TestStack") frame("runtime/debug/stack_test.go", "runtime/debug_test.TestStack")
frame("testing/testing.go", "") frame("testing/testing.go", "")
} }
func TestSetCrashOutput(t *testing.T) {
testenv.MustHaveExec(t)
exe, err := os.Executable()
if err != nil {
t.Fatal(err)
}
crashOutput := filepath.Join(t.TempDir(), "crash.out")
cmd := exec.Command(exe)
cmd.Stderr = new(strings.Builder)
cmd.Env = append(os.Environ(), "GO_RUNTIME_DEBUG_TEST_ENTRYPOINT=setcrashoutput", "CRASHOUTPUT="+crashOutput)
err = cmd.Run()
stderr := fmt.Sprint(cmd.Stderr)
if err == nil {
t.Fatalf("child process succeeded unexpectedly (stderr: %s)", stderr)
}
t.Logf("child process finished with error %v and stderr <<%s>>", err, stderr)
// Read the file the child process should have written.
// It should contain a crash report such as this:
//
// panic: oops
//
// goroutine 1 [running]:
// runtime/debug_test.TestMain(0x1400007e0a0)
// GOROOT/src/runtime/debug/stack_test.go:33 +0x18c
// main.main()
// _testmain.go:71 +0x170
data, err := os.ReadFile(crashOutput)
if err != nil {
t.Fatalf("child process failed to write crash report: %v", err)
}
crash := string(data)
t.Logf("crash = <<%s>>", crash)
t.Logf("stderr = <<%s>>", stderr)
// Check that the crash file and the stderr both contain the panic and stack trace.
for _, want := range []string{
"panic: oops",
"goroutine 1",
"debug_test.TestMain",
} {
if !strings.Contains(crash, want) {
t.Errorf("crash output does not contain %q", want)
}
if !strings.Contains(stderr, want) {
t.Errorf("stderr output does not contain %q", want)
}
}
// Check that stderr, but not crash, contains the output of println().
printlnOnly := "hello"
if strings.Contains(crash, printlnOnly) {
t.Errorf("crash output contains %q, but should not", printlnOnly)
}
if !strings.Contains(stderr, printlnOnly) {
t.Errorf("stderr output does not contain %q, but should", printlnOnly)
}
}

View File

@ -217,10 +217,38 @@ func syscall_runtimeUnsetenv(key string) {
} }
// writeErrStr writes a string to descriptor 2. // writeErrStr writes a string to descriptor 2.
// If SetCrashOutput(f) was called, it also writes to f.
// //
//go:nosplit //go:nosplit
func writeErrStr(s string) { func writeErrStr(s string) {
write(2, unsafe.Pointer(unsafe.StringData(s)), int32(len(s))) writeErrData(unsafe.StringData(s), int32(len(s)))
}
// writeErrData is the common parts of writeErr{,Str}.
//
//go:nosplit
func writeErrData(data *byte, n int32) {
write(2, unsafe.Pointer(data), n)
// If crashing, print a copy to the SetCrashOutput fd.
gp := getg()
if gp != nil && gp.m.dying > 0 ||
gp == nil && panicking.Load() > 0 {
if fd := crashFD.Load(); fd != ^uintptr(0) {
write(fd, unsafe.Pointer(data), n)
}
}
}
// crashFD is an optional file descriptor to use for fatal panics, as
// set by debug.SetCrashOutput (see #42888). If it is a valid fd (not
// all ones), writeErr and related functions write to it in addition
// to standard error.
var crashFD atomic.Uintptr
//go:linkname setCrashFD
func setCrashFD(fd uintptr) uintptr {
return crashFD.Swap(fd)
} }
// auxv is populated on relevant platforms but defined here for all platforms // auxv is populated on relevant platforms but defined here for all platforms

View File

@ -6,8 +6,9 @@
package runtime package runtime
import "unsafe" //go:nosplit
func writeErr(b []byte) { func writeErr(b []byte) {
write(2, unsafe.Pointer(&b[0]), int32(len(b))) if len(b) > 0 {
writeErrData(&b[0], int32(len(b)))
}
} }

View File

@ -34,6 +34,10 @@ const (
var logger loggerType var logger loggerType
func writeErr(b []byte) { func writeErr(b []byte) {
if len(b) == 0 {
return
}
if logger == unknown { if logger == unknown {
// Use logd if /dev/socket/logdw is available. // Use logd if /dev/socket/logdw is available.
if v := uintptr(access(&writeLogd[0], 0x02 /* W_OK */)); v == 0 { if v := uintptr(access(&writeLogd[0], 0x02 /* W_OK */)); v == 0 {
@ -45,8 +49,9 @@ func writeErr(b []byte) {
} }
} }
// Write to stderr for command-line programs. // Write to stderr for command-line programs,
write(2, unsafe.Pointer(&b[0]), int32(len(b))) // and optionally to SetCrashOutput file.
writeErrData(&b[0], int32(len(b)))
// Log format: "<header>\x00<message m bytes>\x00" // Log format: "<header>\x00<message m bytes>\x00"
// //