Skip to content

Commit d6fa3ce

Browse files
rsceric
authored andcommitted
runtime: replace panic(nil) with panic(new(runtime.PanicNilError))
Long ago we decided that panic(nil) was too unlikely to bother making a special case for purposes of recover. Unfortunately, it has turned out not to be a special case. There are many examples of code in the Go ecosystem where an author has written panic(nil) because they want to panic and don't care about the panic value. Using panic(nil) in this case has the unfortunate behavior of making recover behave as though the goroutine isn't panicking. As a result, code like: func f() { defer func() { if err := recover(); err != nil { log.Fatalf("panicked! %v", err) } }() call1() call2() } looks like it guarantees that call2 has been run any time f returns, but that turns out not to be strictly true. If call1 does panic(nil), then f returns "successfully", having recovered the panic, but without calling call2. Instead you have to write something like: func f() { done := false defer func() { if err := recover(); !done { log.Fatalf("panicked! %v", err) } }() call1() call2() done = true } which defeats nearly the whole point of recover. No one does this, with the result that almost all uses of recover are subtly broken. One specific broken use along these lines is in net/http, which recovers from panics in handlers and sends back an HTTP error. Users discovered in the early days of Go that panic(nil) was a convenient way to jump out of a handler up to the serving loop without sending back an HTTP error. This was a bug, not a feature. Go 1.8 added panic(http.ErrAbortHandler) as a better way to access the feature. Any lingering code that uses panic(nil) to abort an HTTP handler without a failure message should be changed to use http.ErrAbortHandler. Programs that need the old, unintended behavior from net/http or other packages can set GODEBUG=panicnil=1 to stop the run-time error. Uses of recover that want to detect panic(nil) in new programs can check for recover returning a value of type *runtime.PanicNilError. Because the new GODEBUG is used inside the runtime, we can't import internal/godebug, so there is some new machinery to cross-connect those in this CL, to allow a mutable GODEBUG setting. That won't be necessary if we add any other mutable GODEBUG settings in the future. The CL also corrects the handling of defaulted GODEBUG values in the runtime, for golang#56986. Fixes golang#25448. Change-Id: I2b39c7e83e4f7aa308777dabf2edae54773e03f5 Reviewed-on: https://go-review.googlesource.com/c/go/+/461956 Reviewed-by: Robert Griesemer <gri@google.com> Run-TryBot: Russ Cox <rsc@golang.org> TryBot-Result: Gopher Robot <gobot@golang.org> Auto-Submit: Russ Cox <rsc@golang.org>
1 parent 1e65e04 commit d6fa3ce

File tree

9 files changed

+207
-67
lines changed

9 files changed

+207
-67
lines changed

api/next/25448.txt

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
pkg runtime, method (*PanicNilError) Error() string #25448
2+
pkg runtime, method (*PanicNilError) RuntimeError() #25448
3+
pkg runtime, type PanicNilError struct #25448

doc/go_spec.html

+7-13
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<!--{
22
"Title": "The Go Programming Language Specification",
3-
"Subtitle": "Version of December 15, 2022",
3+
"Subtitle": "Version of January 19, 2022",
44
"Path": "/ref/spec"
55
}-->
66

@@ -7574,19 +7574,13 @@ <h3 id="Handling_panics">Handling panics</h3>
75747574
</p>
75757575

75767576
<p>
7577-
The return value of <code>recover</code> is <code>nil</code> if any of the following conditions holds:
7577+
The return value of <code>recover</code> is <code>nil</code> when the
7578+
goroutine is not panicking or <code>recover</code> was not called directly by a deferred function.
7579+
Conversely, if a goroutine is panicking and <code>recover</code> was called directly by a deferred function,
7580+
the return value of <code>recover</code> is guaranteed not to be <code>nil</code>.
7581+
To ensure this, calling <code>panic</code> with a <code>nil</code> interface value (or an untyped <code>nil</code>)
7582+
causes a <a href="#Run_time_panics">run-time panic</a>.
75787583
</p>
7579-
<ul>
7580-
<li>
7581-
<code>panic</code>'s argument was <code>nil</code>;
7582-
</li>
7583-
<li>
7584-
the goroutine is not panicking;
7585-
</li>
7586-
<li>
7587-
<code>recover</code> was not called directly by a deferred function.
7588-
</li>
7589-
</ul>
75907584

75917585
<p>
75927586
The <code>protect</code> function in the example below invokes

src/builtin/builtin.go

+4
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,10 @@ func close(c chan<- Type)
249249
// that point, the program is terminated with a non-zero exit code. This
250250
// termination sequence is called panicking and can be controlled by the
251251
// built-in function recover.
252+
//
253+
// Starting in Go 1.21, calling panic with a nil interface value or an
254+
// untyped nil causes a run-time error (a different panic).
255+
// The GODEBUG setting panicnil=1 disables the run-time error.
252256
func panic(v any)
253257

254258
// The recover built-in function allows a program to manage behavior of a

src/net/http/clientserver_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -1239,9 +1239,9 @@ func testTransportRejectsInvalidHeaders(t *testing.T, mode testMode) {
12391239
func TestInterruptWithPanic(t *testing.T) {
12401240
run(t, func(t *testing.T, mode testMode) {
12411241
t.Run("boom", func(t *testing.T) { testInterruptWithPanic(t, mode, "boom") })
1242-
t.Run("nil", func(t *testing.T) { testInterruptWithPanic(t, mode, nil) })
1242+
t.Run("nil", func(t *testing.T) { t.Setenv("GODEBUG", "panicnil=1"); testInterruptWithPanic(t, mode, nil) })
12431243
t.Run("ErrAbortHandler", func(t *testing.T) { testInterruptWithPanic(t, mode, ErrAbortHandler) })
1244-
})
1244+
}, testNotParallel)
12451245
}
12461246
func testInterruptWithPanic(t *testing.T, mode testMode, panicValue any) {
12471247
const msg = "hello"

src/runtime/panic.go

+21
Original file line numberDiff line numberDiff line change
@@ -800,8 +800,29 @@ func deferCallSave(p *_panic, fn func()) {
800800
}
801801
}
802802

803+
// A PanicNilError happens when code calls panic(nil).
804+
//
805+
// Before Go 1.21, programs that called panic(nil) observed recover returning nil.
806+
// Starting in Go 1.21, programs that call panic(nil) observe recover returning a *PanicNilError.
807+
// Programs can change back to the old behavior by setting GODEBUG=panicnil=1.
808+
type PanicNilError struct {
809+
// This field makes PanicNilError structurally different from
810+
// any other struct in this package, and the _ makes it different
811+
// from any struct in other packages too.
812+
// This avoids any accidental conversions being possible
813+
// between this struct and some other struct sharing the same fields,
814+
// like happened in go.dev/issue/56603.
815+
_ [0]*PanicNilError
816+
}
817+
818+
func (*PanicNilError) Error() string { return "panic called with nil argument" }
819+
func (*PanicNilError) RuntimeError() {}
820+
803821
// The implementation of the predeclared function panic.
804822
func gopanic(e any) {
823+
if e == nil && debug.panicnil.Load() != 1 {
824+
e = new(PanicNilError)
825+
}
805826
gp := getg()
806827
if gp.m.curg != gp {
807828
print("panic: ")

src/runtime/panicnil_test.go

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright 2023 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package runtime_test
6+
7+
import (
8+
"reflect"
9+
"runtime"
10+
"testing"
11+
)
12+
13+
func TestPanicNil(t *testing.T) {
14+
t.Run("default", func(t *testing.T) {
15+
checkPanicNil(t, new(runtime.PanicNilError))
16+
})
17+
t.Run("GODEBUG=panicnil=0", func(t *testing.T) {
18+
t.Setenv("GODEBUG", "panicnil=0")
19+
checkPanicNil(t, new(runtime.PanicNilError))
20+
})
21+
t.Run("GODEBUG=panicnil=1", func(t *testing.T) {
22+
t.Setenv("GODEBUG", "panicnil=1")
23+
checkPanicNil(t, nil)
24+
})
25+
}
26+
27+
func checkPanicNil(t *testing.T, want any) {
28+
defer func() {
29+
e := recover()
30+
if reflect.TypeOf(e) != reflect.TypeOf(want) {
31+
println(e, want)
32+
t.Errorf("recover() = %v, want %v", e, want)
33+
}
34+
}()
35+
panic(nil)
36+
}

src/runtime/runtime.go

+13-9
Original file line numberDiff line numberDiff line change
@@ -75,15 +75,19 @@ func godebug_setUpdate(update func(string, string)) {
7575
p := new(func(string, string))
7676
*p = update
7777
godebugUpdate.Store(p)
78-
godebugNotify()
78+
godebugNotify(false)
7979
}
8080

81-
func godebugNotify() {
82-
if update := godebugUpdate.Load(); update != nil {
83-
var env string
84-
if p := godebugEnv.Load(); p != nil {
85-
env = *p
86-
}
81+
func godebugNotify(envChanged bool) {
82+
update := godebugUpdate.Load()
83+
var env string
84+
if p := godebugEnv.Load(); p != nil {
85+
env = *p
86+
}
87+
if envChanged {
88+
reparsedebugvars(env)
89+
}
90+
if update != nil {
8791
(*update)(godebugDefault, env)
8892
}
8993
}
@@ -95,7 +99,7 @@ func syscall_runtimeSetenv(key, value string) {
9599
p := new(string)
96100
*p = value
97101
godebugEnv.Store(p)
98-
godebugNotify()
102+
godebugNotify(true)
99103
}
100104
}
101105

@@ -104,7 +108,7 @@ func syscall_runtimeUnsetenv(key string) {
104108
unsetenv_c(key)
105109
if key == "GODEBUG" {
106110
godebugEnv.Store(nil)
107-
godebugNotify()
111+
godebugNotify(true)
108112
}
109113
}
110114

src/runtime/runtime1.go

+118-41
Original file line numberDiff line numberDiff line change
@@ -296,8 +296,10 @@ func check() {
296296
}
297297

298298
type dbgVar struct {
299-
name string
300-
value *int32
299+
name string
300+
value *int32 // for variables that can only be set at startup
301+
atomic *atomic.Int32 // for variables that can be changed during execution
302+
def int32 // default value (ideally zero)
301303
}
302304

303305
// Holds variables parsed from GODEBUG env var,
@@ -330,32 +332,33 @@ var debug struct {
330332
allocfreetrace int32
331333
inittrace int32
332334
sbrk int32
333-
}
334335

335-
var dbgvars = []dbgVar{
336-
{"allocfreetrace", &debug.allocfreetrace},
337-
{"clobberfree", &debug.clobberfree},
338-
{"cgocheck", &debug.cgocheck},
339-
{"efence", &debug.efence},
340-
{"gccheckmark", &debug.gccheckmark},
341-
{"gcpacertrace", &debug.gcpacertrace},
342-
{"gcshrinkstackoff", &debug.gcshrinkstackoff},
343-
{"gcstoptheworld", &debug.gcstoptheworld},
344-
{"gctrace", &debug.gctrace},
345-
{"invalidptr", &debug.invalidptr},
346-
{"madvdontneed", &debug.madvdontneed},
347-
{"sbrk", &debug.sbrk},
348-
{"scavtrace", &debug.scavtrace},
349-
{"scheddetail", &debug.scheddetail},
350-
{"schedtrace", &debug.schedtrace},
351-
{"tracebackancestors", &debug.tracebackancestors},
352-
{"asyncpreemptoff", &debug.asyncpreemptoff},
353-
{"inittrace", &debug.inittrace},
354-
{"harddecommit", &debug.harddecommit},
355-
{"adaptivestackstart", &debug.adaptivestackstart},
336+
panicnil atomic.Int32
356337
}
357338

358-
var globalGODEBUG string
339+
var dbgvars = []*dbgVar{
340+
{name: "allocfreetrace", value: &debug.allocfreetrace},
341+
{name: "clobberfree", value: &debug.clobberfree},
342+
{name: "cgocheck", value: &debug.cgocheck},
343+
{name: "efence", value: &debug.efence},
344+
{name: "gccheckmark", value: &debug.gccheckmark},
345+
{name: "gcpacertrace", value: &debug.gcpacertrace},
346+
{name: "gcshrinkstackoff", value: &debug.gcshrinkstackoff},
347+
{name: "gcstoptheworld", value: &debug.gcstoptheworld},
348+
{name: "gctrace", value: &debug.gctrace},
349+
{name: "invalidptr", value: &debug.invalidptr},
350+
{name: "madvdontneed", value: &debug.madvdontneed},
351+
{name: "sbrk", value: &debug.sbrk},
352+
{name: "scavtrace", value: &debug.scavtrace},
353+
{name: "scheddetail", value: &debug.scheddetail},
354+
{name: "schedtrace", value: &debug.schedtrace},
355+
{name: "tracebackancestors", value: &debug.tracebackancestors},
356+
{name: "asyncpreemptoff", value: &debug.asyncpreemptoff},
357+
{name: "inittrace", value: &debug.inittrace},
358+
{name: "harddecommit", value: &debug.harddecommit},
359+
{name: "adaptivestackstart", value: &debug.adaptivestackstart},
360+
{name: "panicnil", atomic: &debug.panicnil},
361+
}
359362

360363
func parsedebugvars() {
361364
// defaults
@@ -374,44 +377,118 @@ func parsedebugvars() {
374377
debug.madvdontneed = 1
375378
}
376379

377-
globalGODEBUG = gogetenv("GODEBUG")
378-
godebugEnv.StoreNoWB(&globalGODEBUG)
379-
for p := globalGODEBUG; p != ""; {
380-
field := ""
381-
i := bytealg.IndexByteString(p, ',')
382-
if i < 0 {
383-
field, p = p, ""
380+
godebug := gogetenv("GODEBUG")
381+
382+
p := new(string)
383+
*p = godebug
384+
godebugEnv.Store(p)
385+
386+
// apply runtime defaults, if any
387+
for _, v := range dbgvars {
388+
if v.def != 0 {
389+
// Every var should have either v.value or v.atomic set.
390+
if v.value != nil {
391+
*v.value = v.def
392+
} else if v.atomic != nil {
393+
v.atomic.Store(v.def)
394+
}
395+
}
396+
}
397+
398+
// apply compile-time GODEBUG settings
399+
parsegodebug(godebugDefault, nil)
400+
401+
// apply environment settings
402+
parsegodebug(godebug, nil)
403+
404+
debug.malloc = (debug.allocfreetrace | debug.inittrace | debug.sbrk) != 0
405+
406+
setTraceback(gogetenv("GOTRACEBACK"))
407+
traceback_env = traceback_cache
408+
}
409+
410+
// reparsedebugvars reparses the runtime's debug variables
411+
// because the environment variable has been changed to env.
412+
func reparsedebugvars(env string) {
413+
seen := make(map[string]bool)
414+
// apply environment settings
415+
parsegodebug(env, seen)
416+
// apply compile-time GODEBUG settings for as-yet-unseen variables
417+
parsegodebug(godebugDefault, seen)
418+
// apply defaults for as-yet-unseen variables
419+
for _, v := range dbgvars {
420+
if v.atomic != nil && !seen[v.name] {
421+
v.atomic.Store(0)
422+
}
423+
}
424+
}
425+
426+
// parsegodebug parses the godebug string, updating variables listed in dbgvars.
427+
// If seen == nil, this is startup time and we process the string left to right
428+
// overwriting older settings with newer ones.
429+
// If seen != nil, $GODEBUG has changed and we are doing an
430+
// incremental update. To avoid flapping in the case where a value is
431+
// set multiple times (perhaps in the default and the environment,
432+
// or perhaps twice in the environment), we process the string right-to-left
433+
// and only change values not already seen. After doing this for both
434+
// the environment and the default settings, the caller must also call
435+
// cleargodebug(seen) to reset any now-unset values back to their defaults.
436+
func parsegodebug(godebug string, seen map[string]bool) {
437+
for p := godebug; p != ""; {
438+
var field string
439+
if seen == nil {
440+
// startup: process left to right, overwriting older settings with newer
441+
i := bytealg.IndexByteString(p, ',')
442+
if i < 0 {
443+
field, p = p, ""
444+
} else {
445+
field, p = p[:i], p[i+1:]
446+
}
384447
} else {
385-
field, p = p[:i], p[i+1:]
448+
// incremental update: process right to left, updating and skipping seen
449+
i := len(p) - 1
450+
for i >= 0 && p[i] != ',' {
451+
i--
452+
}
453+
if i < 0 {
454+
p, field = "", p
455+
} else {
456+
p, field = p[:i], p[i+1:]
457+
}
386458
}
387-
i = bytealg.IndexByteString(field, '=')
459+
i := bytealg.IndexByteString(field, '=')
388460
if i < 0 {
389461
continue
390462
}
391463
key, value := field[:i], field[i+1:]
464+
if seen[key] {
465+
continue
466+
}
467+
if seen != nil {
468+
seen[key] = true
469+
}
392470

393471
// Update MemProfileRate directly here since it
394472
// is int, not int32, and should only be updated
395473
// if specified in GODEBUG.
396-
if key == "memprofilerate" {
474+
if seen == nil && key == "memprofilerate" {
397475
if n, ok := atoi(value); ok {
398476
MemProfileRate = n
399477
}
400478
} else {
401479
for _, v := range dbgvars {
402480
if v.name == key {
403481
if n, ok := atoi32(value); ok {
404-
*v.value = n
482+
if seen == nil && v.value != nil {
483+
*v.value = n
484+
} else if v.atomic != nil {
485+
v.atomic.Store(n)
486+
}
405487
}
406488
}
407489
}
408490
}
409491
}
410-
411-
debug.malloc = (debug.allocfreetrace | debug.inittrace | debug.sbrk) != 0
412-
413-
setTraceback(gogetenv("GOTRACEBACK"))
414-
traceback_env = traceback_cache
415492
}
416493

417494
//go:linkname setTraceback runtime/debug.SetTraceback

test/fixedbugs/issue19658.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
// +build !nacl,!js,!gccgo
21
// run
2+
//go:build !nacl && !js && !gccgo
33

44
// Copyright 2017 The Go Authors. All rights reserved.
55
// Use of this source code is governed by a BSD-style
@@ -46,7 +46,8 @@ func main() {
4646
Type string
4747
Input string
4848
Expect string
49-
}{{"", "nil", "panic: nil"},
49+
}{
50+
{"", "nil", "panic: panic called with nil argument"},
5051
{"errors.New", `"test"`, "panic: test"},
5152
{"S", "S{}", "panic: s-stringer"},
5253
{"byte", "8", "panic: 8"},

0 commit comments

Comments
 (0)