From 9f67e7f9860c8c80f8edbff93bd6484fc9459fbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Sun, 23 Aug 2026 19:23:48 +0100 Subject: [PATCH 1/2] testdata: cover GOTRACEBACK=crash in tiny mode Tiny mode empties runtime.setTraceback, so debug.SetTraceback("crash") no longer makes a fatal error crash the process. The test asserts that broken behavior for now; the default build already gets it right. The program re-execs itself rather than adding another script, so this reuses the tiny build tiny.txtar already pays for. --- testdata/script/tiny.txtar | 43 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/testdata/script/tiny.txtar b/testdata/script/tiny.txtar index 7ad64d33..51da3675 100644 --- a/testdata/script/tiny.txtar +++ b/testdata/script/tiny.txtar @@ -28,6 +28,10 @@ stderr '^makefunc.name reflect\.makeFuncStub value 107$' stderr '^methodvalue.name reflect\.methodValueCall value 213$' stderr '^wrapper.name _\.\(\*_\)\._ recovered true$' stderr '^runtime.sentinels.goexit true gopanic true$' +# GOTRACEBACK controls more than traceback output, which tiny mode hides: +# "crash" must still crash the process rather than exit with status 2. +# TODO: should be "crash aborts: true"; tiny mode empties runtime.setTraceback. +stderr '^single aborts: false crash aborts: false$' [short] stop # no need to verify this with -short @@ -48,20 +52,40 @@ stderr '^makefunc.name reflect\.makeFuncStub value 107$' stderr '^methodvalue.name reflect\.methodValueCall value 213$' stderr '^wrapper.name .* recovered true$' stderr '^runtime.sentinels.goexit true gopanic true$' +stderr '^single aborts: false crash aborts: true$' -- go.mod -- module test/main go 1.23 +-- main_linux.go -- +package main + +import "syscall" + +// Keep the crashing children from writing core dumps. +// Note that syscall.Rlimit is only declared in some unix GOOSes. +func init() { + var lim syscall.Rlimit + if syscall.Getrlimit(syscall.RLIMIT_CORE, &lim) == nil { + lim.Cur = 0 + syscall.Setrlimit(syscall.RLIMIT_CORE, &lim) + } +} -- garble_main.go -- package main import ( cryptorand "crypto/rand" + "os" + "os/exec" "reflect" "runtime" + "runtime/debug" "sync" ) +var nilPtr *int + type testStruct struct{} func (testStruct) unexportedFunc() { println("dummy") } @@ -156,7 +180,26 @@ func checkRuntimeConsumedNames() { println("runtime.sentinels.goexit", <-goexit, "gopanic", gopanic) } +// childAborts reports whether a nil dereference at the given GOTRACEBACK level +// kills this program rather than exiting with the usual status 2. Only +// runtime.setTraceback records that setting, so tiny mode must not empty it. +func childAborts(level string) bool { + self, err := os.Executable() + if err != nil { + panic(err) + } + cmd := exec.Command(self, level) + cmd.Run() + return cmd.ProcessState.ExitCode() != 2 +} + func main() { + if len(os.Args) > 1 { + debug.SetTraceback(os.Args[1]) + println(*nilPtr) + } + println("single aborts:", childAborts("single"), "crash aborts:", childAborts("crash")) + checkRuntimeConsumedNames() // Retain fatal paths which Go 1.26 moved into runtime-linked standard From 27f4f7c6c6d0a65c239ac40920641970113824f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Sun, 23 Aug 2026 19:23:57 +0100 Subject: [PATCH 2/2] don't strip setTraceback in tiny mode runtime1.go's setTraceback was emptied on the grounds that tiny mode hides tracebacks anyway, but the function does more than pick how much gets printed. It stores traceback_cache, which gotraceback reads to decide whether a fatal error crashes the process instead of exiting with status 2, and on Windows GOTRACEBACK=wer enables Windows Error Reporting. Since finishDebugVarsSetup routes GOTRACEBACK through it, tiny builds lost the env var too, along with the crash-on-fatal rule for c-archive and c-shared. Keeping it costs nothing measurable: a hello-world tiny build is 1343612 bytes either way, since the linker drops the body unless something references debug.SetTraceback. --- runtime_patch.go | 10 ++++------ testdata/script/tiny.txtar | 3 +-- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/runtime_patch.go b/runtime_patch.go index a5d13ab2..be842dfe 100644 --- a/runtime_patch.go +++ b/runtime_patch.go @@ -267,12 +267,10 @@ func stripRuntime(basename string, file *ast.File) (strippedFunctions map[string funcDecl.Body.List = nil } case "runtime1.go": - switch funcDecl.Name.Name { - case "setTraceback": - // tracebacks are completely hidden, no - // sense keeping this function - funcDecl.Body.List = nil - } + // setTraceback is deliberately left alone. Besides selecting how + // much of a traceback is printed, it decides whether fatal errors + // crash the process rather than exiting with status 2, and it + // enables Windows Error Reporting for GOTRACEBACK=wer. case "runtime.go": // writeErrStr bypasses the print builtins and writes fixed fatal // diagnostics straight to stderr (and SetCrashOutput). Tiny mode diff --git a/testdata/script/tiny.txtar b/testdata/script/tiny.txtar index 51da3675..ad5a6a39 100644 --- a/testdata/script/tiny.txtar +++ b/testdata/script/tiny.txtar @@ -30,8 +30,7 @@ stderr '^wrapper.name _\.\(\*_\)\._ recovered true$' stderr '^runtime.sentinels.goexit true gopanic true$' # GOTRACEBACK controls more than traceback output, which tiny mode hides: # "crash" must still crash the process rather than exit with status 2. -# TODO: should be "crash aborts: true"; tiny mode empties runtime.setTraceback. -stderr '^single aborts: false crash aborts: false$' +stderr '^single aborts: false crash aborts: true$' [short] stop # no need to verify this with -short