From aa51c40b1cc62d53603d7b7aea3232969aa40afe Mon Sep 17 00:00:00 2001
From: Russ Cox
-The return value of recover
is nil
if any of the following conditions holds:
+The return value of recover
is nil
when the
+goroutine is not panicking or recover
was not called directly by a deferred function.
+Conversely, if a goroutine is panicking and recover
was called directly by a deferred function,
+the return value of recover
is guaranteed not to be nil
.
+To ensure this, calling panic
with a nil
interface value (or an untyped nil
)
+causes a run-time panic.
panic
's argument was nil
;
-recover
was not called directly by a deferred function.
-
The protect
function in the example below invokes
diff --git a/src/builtin/builtin.go b/src/builtin/builtin.go
index 7feb209bb4..d3637584fe 100644
--- a/src/builtin/builtin.go
+++ b/src/builtin/builtin.go
@@ -249,6 +249,10 @@ func close(c chan<- Type)
// that point, the program is terminated with a non-zero exit code. This
// termination sequence is called panicking and can be controlled by the
// built-in function recover.
+//
+// Starting in Go 1.21, calling panic with a nil interface value or an
+// untyped nil causes a run-time error (a different panic).
+// The GODEBUG setting panicnil=1 disables the run-time error.
func panic(v any)
// The recover built-in function allows a program to manage behavior of a
diff --git a/src/net/http/clientserver_test.go b/src/net/http/clientserver_test.go
index da5671d9b9..e49bed113a 100644
--- a/src/net/http/clientserver_test.go
+++ b/src/net/http/clientserver_test.go
@@ -1239,9 +1239,9 @@ func testTransportRejectsInvalidHeaders(t *testing.T, mode testMode) {
func TestInterruptWithPanic(t *testing.T) {
run(t, func(t *testing.T, mode testMode) {
t.Run("boom", func(t *testing.T) { testInterruptWithPanic(t, mode, "boom") })
- t.Run("nil", func(t *testing.T) { testInterruptWithPanic(t, mode, nil) })
+ t.Run("nil", func(t *testing.T) { t.Setenv("GODEBUG", "panicnil=1"); testInterruptWithPanic(t, mode, nil) })
t.Run("ErrAbortHandler", func(t *testing.T) { testInterruptWithPanic(t, mode, ErrAbortHandler) })
- })
+ }, testNotParallel)
}
func testInterruptWithPanic(t *testing.T, mode testMode, panicValue any) {
const msg = "hello"
diff --git a/src/runtime/panic.go b/src/runtime/panic.go
index 26618db7ce..905515f822 100644
--- a/src/runtime/panic.go
+++ b/src/runtime/panic.go
@@ -800,8 +800,29 @@ func deferCallSave(p *_panic, fn func()) {
}
}
+// A PanicNilError happens when code calls panic(nil).
+//
+// Before Go 1.21, programs that called panic(nil) observed recover returning nil.
+// Starting in Go 1.21, programs that call panic(nil) observe recover returning a *PanicNilError.
+// Programs can change back to the old behavior by setting GODEBUG=panicnil=1.
+type PanicNilError struct {
+ // This field makes PanicNilError structurally different from
+ // any other struct in this package, and the _ makes it different
+ // from any struct in other packages too.
+ // This avoids any accidental conversions being possible
+ // between this struct and some other struct sharing the same fields,
+ // like happened in go.dev/issue/56603.
+ _ [0]*PanicNilError
+}
+
+func (*PanicNilError) Error() string { return "panic called with nil argument" }
+func (*PanicNilError) RuntimeError() {}
+
// The implementation of the predeclared function panic.
func gopanic(e any) {
+ if e == nil && debug.panicnil.Load() != 1 {
+ e = new(PanicNilError)
+ }
gp := getg()
if gp.m.curg != gp {
print("panic: ")
diff --git a/src/runtime/panicnil_test.go b/src/runtime/panicnil_test.go
new file mode 100644
index 0000000000..441bef3b07
--- /dev/null
+++ b/src/runtime/panicnil_test.go
@@ -0,0 +1,36 @@
+// Copyright 2023 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package runtime_test
+
+import (
+ "reflect"
+ "runtime"
+ "testing"
+)
+
+func TestPanicNil(t *testing.T) {
+ t.Run("default", func(t *testing.T) {
+ checkPanicNil(t, new(runtime.PanicNilError))
+ })
+ t.Run("GODEBUG=panicnil=0", func(t *testing.T) {
+ t.Setenv("GODEBUG", "panicnil=0")
+ checkPanicNil(t, new(runtime.PanicNilError))
+ })
+ t.Run("GODEBUG=panicnil=1", func(t *testing.T) {
+ t.Setenv("GODEBUG", "panicnil=1")
+ checkPanicNil(t, nil)
+ })
+}
+
+func checkPanicNil(t *testing.T, want any) {
+ defer func() {
+ e := recover()
+ if reflect.TypeOf(e) != reflect.TypeOf(want) {
+ println(e, want)
+ t.Errorf("recover() = %v, want %v", e, want)
+ }
+ }()
+ panic(nil)
+}
diff --git a/src/runtime/runtime.go b/src/runtime/runtime.go
index 9f68738aa7..ab2a54f00b 100644
--- a/src/runtime/runtime.go
+++ b/src/runtime/runtime.go
@@ -75,15 +75,19 @@ func godebug_setUpdate(update func(string, string)) {
p := new(func(string, string))
*p = update
godebugUpdate.Store(p)
- godebugNotify()
+ godebugNotify(false)
}
-func godebugNotify() {
- if update := godebugUpdate.Load(); update != nil {
- var env string
- if p := godebugEnv.Load(); p != nil {
- env = *p
- }
+func godebugNotify(envChanged bool) {
+ update := godebugUpdate.Load()
+ var env string
+ if p := godebugEnv.Load(); p != nil {
+ env = *p
+ }
+ if envChanged {
+ reparsedebugvars(env)
+ }
+ if update != nil {
(*update)(godebugDefault, env)
}
}
@@ -95,7 +99,7 @@ func syscall_runtimeSetenv(key, value string) {
p := new(string)
*p = value
godebugEnv.Store(p)
- godebugNotify()
+ godebugNotify(true)
}
}
@@ -104,7 +108,7 @@ func syscall_runtimeUnsetenv(key string) {
unsetenv_c(key)
if key == "GODEBUG" {
godebugEnv.Store(nil)
- godebugNotify()
+ godebugNotify(true)
}
}
diff --git a/src/runtime/runtime1.go b/src/runtime/runtime1.go
index 277f18a5a6..5f9555e404 100644
--- a/src/runtime/runtime1.go
+++ b/src/runtime/runtime1.go
@@ -296,8 +296,10 @@ func check() {
}
type dbgVar struct {
- name string
- value *int32
+ name string
+ value *int32 // for variables that can only be set at startup
+ atomic *atomic.Int32 // for variables that can be changed during execution
+ def int32 // default value (ideally zero)
}
// Holds variables parsed from GODEBUG env var,
@@ -330,33 +332,34 @@ var debug struct {
allocfreetrace int32
inittrace int32
sbrk int32
+
+ panicnil atomic.Int32
}
-var dbgvars = []dbgVar{
- {"allocfreetrace", &debug.allocfreetrace},
- {"clobberfree", &debug.clobberfree},
- {"cgocheck", &debug.cgocheck},
- {"efence", &debug.efence},
- {"gccheckmark", &debug.gccheckmark},
- {"gcpacertrace", &debug.gcpacertrace},
- {"gcshrinkstackoff", &debug.gcshrinkstackoff},
- {"gcstoptheworld", &debug.gcstoptheworld},
- {"gctrace", &debug.gctrace},
- {"invalidptr", &debug.invalidptr},
- {"madvdontneed", &debug.madvdontneed},
- {"sbrk", &debug.sbrk},
- {"scavtrace", &debug.scavtrace},
- {"scheddetail", &debug.scheddetail},
- {"schedtrace", &debug.schedtrace},
- {"tracebackancestors", &debug.tracebackancestors},
- {"asyncpreemptoff", &debug.asyncpreemptoff},
- {"inittrace", &debug.inittrace},
- {"harddecommit", &debug.harddecommit},
- {"adaptivestackstart", &debug.adaptivestackstart},
+var dbgvars = []*dbgVar{
+ {name: "allocfreetrace", value: &debug.allocfreetrace},
+ {name: "clobberfree", value: &debug.clobberfree},
+ {name: "cgocheck", value: &debug.cgocheck},
+ {name: "efence", value: &debug.efence},
+ {name: "gccheckmark", value: &debug.gccheckmark},
+ {name: "gcpacertrace", value: &debug.gcpacertrace},
+ {name: "gcshrinkstackoff", value: &debug.gcshrinkstackoff},
+ {name: "gcstoptheworld", value: &debug.gcstoptheworld},
+ {name: "gctrace", value: &debug.gctrace},
+ {name: "invalidptr", value: &debug.invalidptr},
+ {name: "madvdontneed", value: &debug.madvdontneed},
+ {name: "sbrk", value: &debug.sbrk},
+ {name: "scavtrace", value: &debug.scavtrace},
+ {name: "scheddetail", value: &debug.scheddetail},
+ {name: "schedtrace", value: &debug.schedtrace},
+ {name: "tracebackancestors", value: &debug.tracebackancestors},
+ {name: "asyncpreemptoff", value: &debug.asyncpreemptoff},
+ {name: "inittrace", value: &debug.inittrace},
+ {name: "harddecommit", value: &debug.harddecommit},
+ {name: "adaptivestackstart", value: &debug.adaptivestackstart},
+ {name: "panicnil", atomic: &debug.panicnil},
}
-var globalGODEBUG string
-
func parsedebugvars() {
// defaults
debug.cgocheck = 1
@@ -374,26 +377,101 @@ func parsedebugvars() {
debug.madvdontneed = 1
}
- globalGODEBUG = gogetenv("GODEBUG")
- godebugEnv.StoreNoWB(&globalGODEBUG)
- for p := globalGODEBUG; p != ""; {
- field := ""
- i := bytealg.IndexByteString(p, ',')
- if i < 0 {
- field, p = p, ""
- } else {
- field, p = p[:i], p[i+1:]
+ godebug := gogetenv("GODEBUG")
+
+ p := new(string)
+ *p = godebug
+ godebugEnv.Store(p)
+
+ // apply runtime defaults, if any
+ for _, v := range dbgvars {
+ if v.def != 0 {
+ // Every var should have either v.value or v.atomic set.
+ if v.value != nil {
+ *v.value = v.def
+ } else if v.atomic != nil {
+ v.atomic.Store(v.def)
+ }
}
- i = bytealg.IndexByteString(field, '=')
+ }
+
+ // apply compile-time GODEBUG settings
+ parsegodebug(godebugDefault, nil)
+
+ // apply environment settings
+ parsegodebug(godebug, nil)
+
+ debug.malloc = (debug.allocfreetrace | debug.inittrace | debug.sbrk) != 0
+
+ setTraceback(gogetenv("GOTRACEBACK"))
+ traceback_env = traceback_cache
+}
+
+// reparsedebugvars reparses the runtime's debug variables
+// because the environment variable has been changed to env.
+func reparsedebugvars(env string) {
+ seen := make(map[string]bool)
+ // apply environment settings
+ parsegodebug(env, seen)
+ // apply compile-time GODEBUG settings for as-yet-unseen variables
+ parsegodebug(godebugDefault, seen)
+ // apply defaults for as-yet-unseen variables
+ for _, v := range dbgvars {
+ if v.atomic != nil && !seen[v.name] {
+ v.atomic.Store(0)
+ }
+ }
+}
+
+// parsegodebug parses the godebug string, updating variables listed in dbgvars.
+// If seen == nil, this is startup time and we process the string left to right
+// overwriting older settings with newer ones.
+// If seen != nil, $GODEBUG has changed and we are doing an
+// incremental update. To avoid flapping in the case where a value is
+// set multiple times (perhaps in the default and the environment,
+// or perhaps twice in the environment), we process the string right-to-left
+// and only change values not already seen. After doing this for both
+// the environment and the default settings, the caller must also call
+// cleargodebug(seen) to reset any now-unset values back to their defaults.
+func parsegodebug(godebug string, seen map[string]bool) {
+ for p := godebug; p != ""; {
+ var field string
+ if seen == nil {
+ // startup: process left to right, overwriting older settings with newer
+ i := bytealg.IndexByteString(p, ',')
+ if i < 0 {
+ field, p = p, ""
+ } else {
+ field, p = p[:i], p[i+1:]
+ }
+ } else {
+ // incremental update: process right to left, updating and skipping seen
+ i := len(p) - 1
+ for i >= 0 && p[i] != ',' {
+ i--
+ }
+ if i < 0 {
+ p, field = "", p
+ } else {
+ p, field = p[:i], p[i+1:]
+ }
+ }
+ i := bytealg.IndexByteString(field, '=')
if i < 0 {
continue
}
key, value := field[:i], field[i+1:]
+ if seen[key] {
+ continue
+ }
+ if seen != nil {
+ seen[key] = true
+ }
// Update MemProfileRate directly here since it
// is int, not int32, and should only be updated
// if specified in GODEBUG.
- if key == "memprofilerate" {
+ if seen == nil && key == "memprofilerate" {
if n, ok := atoi(value); ok {
MemProfileRate = n
}
@@ -401,17 +479,16 @@ func parsedebugvars() {
for _, v := range dbgvars {
if v.name == key {
if n, ok := atoi32(value); ok {
- *v.value = n
+ if seen == nil && v.value != nil {
+ *v.value = n
+ } else if v.atomic != nil {
+ v.atomic.Store(n)
+ }
}
}
}
}
}
-
- debug.malloc = (debug.allocfreetrace | debug.inittrace | debug.sbrk) != 0
-
- setTraceback(gogetenv("GOTRACEBACK"))
- traceback_env = traceback_cache
}
//go:linkname setTraceback runtime/debug.SetTraceback
diff --git a/test/fixedbugs/issue19658.go b/test/fixedbugs/issue19658.go
index bab409c6c0..70fa3a65c3 100644
--- a/test/fixedbugs/issue19658.go
+++ b/test/fixedbugs/issue19658.go
@@ -1,5 +1,5 @@
-// +build !nacl,!js,!gccgo
// run
+//go:build !nacl && !js && !gccgo
// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
@@ -46,7 +46,8 @@ func main() {
Type string
Input string
Expect string
- }{{"", "nil", "panic: nil"},
+ }{
+ {"", "nil", "panic: panic called with nil argument"},
{"errors.New", `"test"`, "panic: test"},
{"S", "S{}", "panic: s-stringer"},
{"byte", "8", "panic: 8"},