From 40272cb0458004bfecdf0ca15ca12816c01275f4 Mon Sep 17 00:00:00 2001 From: Derek Parker Date: Fri, 4 Aug 2023 09:50:49 -0700 Subject: [PATCH] Backport CVE-2023-29403 fix to 1.18 --- src/runtime/extern.go | 19 +++ .../internal/syscall/defs_linux_386.go | 9 ++ .../internal/syscall/defs_linux_amd64.go | 9 ++ .../internal/syscall/defs_linux_arm.go | 9 ++ .../internal/syscall/defs_linux_arm64.go | 9 ++ .../internal/syscall/defs_linux_mips64x.go | 11 ++ .../internal/syscall/defs_linux_mipsx.go | 11 ++ .../internal/syscall/defs_linux_ppc64x.go | 11 ++ .../internal/syscall/defs_linux_riscv64.go | 9 ++ .../internal/syscall/defs_linux_s390x.go | 9 ++ src/runtime/os2_aix.go | 12 ++ src/runtime/os_aix.go | 40 +++++ src/runtime/os_dragonfly.go | 2 + src/runtime/os_freebsd.go | 2 + src/runtime/os_linux.go | 25 +++ src/runtime/os_netbsd.go | 2 + src/runtime/os_openbsd_syscall2.go | 2 + src/runtime/os_solaris.go | 4 + src/runtime/panic.go | 4 + src/runtime/proc.go | 1 + src/runtime/security_aix.go | 17 +++ src/runtime/security_issetugid.go | 19 +++ src/runtime/security_linux.go | 15 ++ src/runtime/security_nonunix.go | 13 ++ src/runtime/security_test.go | 143 ++++++++++++++++++ src/runtime/security_unix.go | 72 +++++++++ src/runtime/signal_unix.go | 4 + src/runtime/sys_darwin.go | 22 ++- src/runtime/sys_darwin_amd64.s | 4 + src/runtime/sys_darwin_arm64.s | 4 + src/runtime/sys_dragonfly_amd64.s | 10 ++ src/runtime/sys_freebsd_386.s | 7 + src/runtime/sys_freebsd_amd64.s | 10 ++ src/runtime/sys_freebsd_arm.s | 8 + src/runtime/sys_freebsd_arm64.s | 8 + src/runtime/sys_netbsd_386.s | 8 + src/runtime/sys_netbsd_amd64.s | 11 ++ src/runtime/sys_netbsd_arm.s | 7 + src/runtime/sys_netbsd_arm64.s | 7 + src/runtime/sys_openbsd2.go | 10 ++ src/runtime/sys_openbsd_386.s | 9 ++ src/runtime/sys_openbsd_amd64.s | 6 + src/runtime/sys_openbsd_arm.s | 9 ++ src/runtime/sys_openbsd_arm64.s | 6 + src/runtime/sys_openbsd_mips64.s | 7 + src/runtime/syscall2_solaris.go | 2 + src/runtime/syscall_solaris.go | 1 + src/runtime/testdata/testsuid/main.go | 25 +++ 48 files changed, 659 insertions(+), 5 deletions(-) create mode 100644 src/runtime/internal/syscall/defs_linux_386.go create mode 100644 src/runtime/internal/syscall/defs_linux_amd64.go create mode 100644 src/runtime/internal/syscall/defs_linux_arm.go create mode 100644 src/runtime/internal/syscall/defs_linux_arm64.go create mode 100644 src/runtime/internal/syscall/defs_linux_mips64x.go create mode 100644 src/runtime/internal/syscall/defs_linux_mipsx.go create mode 100644 src/runtime/internal/syscall/defs_linux_ppc64x.go create mode 100644 src/runtime/internal/syscall/defs_linux_riscv64.go create mode 100644 src/runtime/internal/syscall/defs_linux_s390x.go create mode 100644 src/runtime/security_aix.go create mode 100644 src/runtime/security_issetugid.go create mode 100644 src/runtime/security_linux.go create mode 100644 src/runtime/security_nonunix.go create mode 100644 src/runtime/security_test.go create mode 100644 src/runtime/security_unix.go create mode 100644 src/runtime/testdata/testsuid/main.go diff --git a/src/runtime/extern.go b/src/runtime/extern.go index f1f6ea51231..b64fbcbab53 100644 --- a/src/runtime/extern.go +++ b/src/runtime/extern.go @@ -188,6 +188,25 @@ the set of Go environment variables. They influence the building of Go programs GOARCH, GOOS, and GOROOT are recorded at compile time and made available by constants or functions in this package, but they do not influence the execution of the run-time system. + +# Security + +On Unix platforms, Go's runtime system behaves slightly differently when a +binary is setuid/setgid or executed with setuid/setgid-like properties, in order +to prevent dangerous behaviors. On Linux this is determined by checking for the +AT_SECURE flag in the auxiliary vector, on the BSDs and Solaris/Illumos it is +determined by checking the issetugid syscall, and on AIX it is determined by +checking if the uid/gid match the effective uid/gid. + +When the runtime determines the binary is setuid/setgid-like, it does three main +things: + - The standard input/output file descriptors (0, 1, 2) are checked to be open. + If any of them are closed, they are opened pointing at /dev/null. + - The value of the GOTRACEBACK environment variable is set to 'none'. + - When a signal is received that terminates the program, or the program + encounters an unrecoverable panic that would otherwise override the value + of GOTRACEBACK, the goroutine stack, registers, and other memory related + information are omitted. */ package runtime diff --git a/src/runtime/internal/syscall/defs_linux_386.go b/src/runtime/internal/syscall/defs_linux_386.go new file mode 100644 index 00000000000..0f550223bde --- /dev/null +++ b/src/runtime/internal/syscall/defs_linux_386.go @@ -0,0 +1,9 @@ +// Copyright 2022 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 syscall + +const ( + SYS_FCNTL = 55 +) diff --git a/src/runtime/internal/syscall/defs_linux_amd64.go b/src/runtime/internal/syscall/defs_linux_amd64.go new file mode 100644 index 00000000000..54a5c8aa74d --- /dev/null +++ b/src/runtime/internal/syscall/defs_linux_amd64.go @@ -0,0 +1,9 @@ +// Copyright 2022 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 syscall + +const ( + SYS_FCNTL = 72 +) diff --git a/src/runtime/internal/syscall/defs_linux_arm.go b/src/runtime/internal/syscall/defs_linux_arm.go new file mode 100644 index 00000000000..0f550223bde --- /dev/null +++ b/src/runtime/internal/syscall/defs_linux_arm.go @@ -0,0 +1,9 @@ +// Copyright 2022 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 syscall + +const ( + SYS_FCNTL = 55 +) diff --git a/src/runtime/internal/syscall/defs_linux_arm64.go b/src/runtime/internal/syscall/defs_linux_arm64.go new file mode 100644 index 00000000000..534b28f2930 --- /dev/null +++ b/src/runtime/internal/syscall/defs_linux_arm64.go @@ -0,0 +1,9 @@ +// Copyright 2022 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 syscall + +const ( + SYS_FCNTL = 25 +) diff --git a/src/runtime/internal/syscall/defs_linux_mips64x.go b/src/runtime/internal/syscall/defs_linux_mips64x.go new file mode 100644 index 00000000000..d01043e372a --- /dev/null +++ b/src/runtime/internal/syscall/defs_linux_mips64x.go @@ -0,0 +1,11 @@ +// Copyright 2022 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. + +//go:build linux && (mips64 || mips64le) + +package syscall + +const ( + SYS_FCNTL = 5070 +) diff --git a/src/runtime/internal/syscall/defs_linux_mipsx.go b/src/runtime/internal/syscall/defs_linux_mipsx.go new file mode 100644 index 00000000000..2bd70eca42a --- /dev/null +++ b/src/runtime/internal/syscall/defs_linux_mipsx.go @@ -0,0 +1,11 @@ +// Copyright 2022 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. + +//go:build linux && (mips || mipsle) + +package syscall + +const ( + SYS_FCNTL = 4055 +) diff --git a/src/runtime/internal/syscall/defs_linux_ppc64x.go b/src/runtime/internal/syscall/defs_linux_ppc64x.go new file mode 100644 index 00000000000..c6d236cb3b9 --- /dev/null +++ b/src/runtime/internal/syscall/defs_linux_ppc64x.go @@ -0,0 +1,11 @@ +// Copyright 2022 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. + +//go:build linux && (ppc64 || ppc64le) + +package syscall + +const ( + SYS_FCNTL = 55 +) diff --git a/src/runtime/internal/syscall/defs_linux_riscv64.go b/src/runtime/internal/syscall/defs_linux_riscv64.go new file mode 100644 index 00000000000..534b28f2930 --- /dev/null +++ b/src/runtime/internal/syscall/defs_linux_riscv64.go @@ -0,0 +1,9 @@ +// Copyright 2022 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 syscall + +const ( + SYS_FCNTL = 25 +) diff --git a/src/runtime/internal/syscall/defs_linux_s390x.go b/src/runtime/internal/syscall/defs_linux_s390x.go new file mode 100644 index 00000000000..0f550223bde --- /dev/null +++ b/src/runtime/internal/syscall/defs_linux_s390x.go @@ -0,0 +1,9 @@ +// Copyright 2022 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 syscall + +const ( + SYS_FCNTL = 55 +) diff --git a/src/runtime/os2_aix.go b/src/runtime/os2_aix.go index 4d77f0de6dd..08de0cc54f3 100644 --- a/src/runtime/os2_aix.go +++ b/src/runtime/os2_aix.go @@ -55,6 +55,10 @@ var ( //go:cgo_import_dynamic libc_sysconf sysconf "libc.a/shr_64.o" //go:cgo_import_dynamic libc_usleep usleep "libc.a/shr_64.o" //go:cgo_import_dynamic libc_write write "libc.a/shr_64.o" +//go:cgo_import_dynamic libc_getuid getuid "libc.a/shr_64.o" +//go:cgo_import_dynamic libc_geteuid geteuid "libc.a/shr_64.o" +//go:cgo_import_dynamic libc_getgid getgid "libc.a/shr_64.o" +//go:cgo_import_dynamic libc_getegid getegid "libc.a/shr_64.o" //go:cgo_import_dynamic libpthread___pth_init __pth_init "libpthread.a/shr_xpg5_64.o" //go:cgo_import_dynamic libpthread_attr_destroy pthread_attr_destroy "libpthread.a/shr_xpg5_64.o" @@ -95,6 +99,10 @@ var ( //go:linkname libc_sysconf libc_sysconf //go:linkname libc_usleep libc_usleep //go:linkname libc_write libc_write +//go:linkname libc_getuid libc_getuid +//go:linkname libc_geteuid libc_geteuid +//go:linkname libc_getgid libc_getgid +//go:linkname libc_getegid libc_getegid //go:linkname libpthread___pth_init libpthread___pth_init //go:linkname libpthread_attr_destroy libpthread_attr_destroy @@ -137,6 +145,10 @@ var ( libc_sysconf, libc_usleep, libc_write, + libc_getuid, + libc_geteuid, + libc_getgid, + libc_getegid, //libpthread libpthread___pth_init, libpthread_attr_destroy, diff --git a/src/runtime/os_aix.go b/src/runtime/os_aix.go index 41352b3a5a1..13a84e933a2 100644 --- a/src/runtime/os_aix.go +++ b/src/runtime/os_aix.go @@ -383,3 +383,43 @@ const sigPerThreadSyscall = 1 << 31 func runPerThreadSyscall() { throw("runPerThreadSyscall only valid on linux") } + +//go:nosplit +func getuid() int32 { + r, errno := syscall0(&libc_getuid) + if errno != 0 { + print("getuid failed ", errno) + throw("getuid") + } + return int32(r) +} + +//go:nosplit +func geteuid() int32 { + r, errno := syscall0(&libc_geteuid) + if errno != 0 { + print("geteuid failed ", errno) + throw("geteuid") + } + return int32(r) +} + +//go:nosplit +func getgid() int32 { + r, errno := syscall0(&libc_getgid) + if errno != 0 { + print("getgid failed ", errno) + throw("getgid") + } + return int32(r) +} + +//go:nosplit +func getegid() int32 { + r, errno := syscall0(&libc_getegid) + if errno != 0 { + print("getegid failed ", errno) + throw("getegid") + } + return int32(r) +} diff --git a/src/runtime/os_dragonfly.go b/src/runtime/os_dragonfly.go index 152d94cf43d..5d431144073 100644 --- a/src/runtime/os_dragonfly.go +++ b/src/runtime/os_dragonfly.go @@ -67,6 +67,8 @@ func pipe2(flags int32) (r, w int32, errno int32) func closeonexec(fd int32) func setNonblock(fd int32) +func issetugid() int32 + // From DragonFly's const ( _CTL_HW = 6 diff --git a/src/runtime/os_freebsd.go b/src/runtime/os_freebsd.go index d908a80cd16..efd1ae7066c 100644 --- a/src/runtime/os_freebsd.go +++ b/src/runtime/os_freebsd.go @@ -52,6 +52,8 @@ func pipe2(flags int32) (r, w int32, errno int32) func closeonexec(fd int32) func setNonblock(fd int32) +func issetugid() int32 + // From FreeBSD's const ( _CTL_HW = 6 diff --git a/src/runtime/os_linux.go b/src/runtime/os_linux.go index eb8aa076e9f..2c50e58e2ab 100644 --- a/src/runtime/os_linux.go +++ b/src/runtime/os_linux.go @@ -52,9 +52,12 @@ const ( ) // Atomically, +// // if(*addr == val) sleep +// // Might be woken up spuriously; that's allowed. // Don't sleep longer than ns; ns < 0 means forever. +// //go:nosplit func futexsleep(addr *uint32, val uint32, ns int64) { // Some Linux kernels have a bug where futex of @@ -73,6 +76,7 @@ func futexsleep(addr *uint32, val uint32, ns int64) { } // If any procs are sleeping on addr, wake up at most cnt. +// //go:nosplit func futexwakeup(addr *uint32, cnt uint32) { ret := futex(unsafe.Pointer(addr), _FUTEX_WAKE_PRIVATE, cnt, nil, nil, 0) @@ -157,6 +161,7 @@ const ( func clone(flags int32, stk, mp, gp, fn unsafe.Pointer) int32 // May run with m.p==nil, so write barriers are not allowed. +// //go:nowritebarrier func newosproc(mp *m) { stk := unsafe.Pointer(mp.g0.stack.hi) @@ -184,6 +189,7 @@ func newosproc(mp *m) { } // Version of newosproc that doesn't require a valid G. +// //go:nosplit func newosproc0(stacksize uintptr, fn unsafe.Pointer) { stack := sysAlloc(stacksize, &memstats.stacks_sys) @@ -205,6 +211,7 @@ const ( _AT_NULL = 0 // End of vector _AT_PAGESZ = 6 // System physical page size _AT_HWCAP = 16 // hardware capability bit vector + _AT_SECURE = 23 // secure mode boolean _AT_RANDOM = 25 // introduced in 2.6.29 _AT_HWCAP2 = 26 // hardware capability bit vector 2 ) @@ -274,6 +281,9 @@ func sysargs(argc int32, argv **byte) { // the ELF AT_RANDOM auxiliary vector. var startupRandomData []byte +// secureMode holds the value of AT_SECURE passed in the auxiliary vector. +var secureMode bool + func sysauxv(auxv []uintptr) int { var i int for ; auxv[i] != _AT_NULL; i += 2 { @@ -286,6 +296,9 @@ func sysauxv(auxv []uintptr) int { case _AT_PAGESZ: physPageSize = val + + case _AT_SECURE: + secureMode = val == 1 } archauxv(tag, val) @@ -365,6 +378,7 @@ func goenvs() { // Called to do synchronous initialization of Go code built with // -buildmode=c-archive or -buildmode=c-shared. // None of the Go runtime is initialized. +// //go:nosplit //go:nowritebarrierrec func libpreinit() { @@ -392,6 +406,7 @@ func minit() { } // Called from dropm to undo the effect of an minit. +// //go:nosplit func unminit() { unminitSignals() @@ -450,6 +465,12 @@ func pipe() (r, w int32, errno int32) func pipe2(flags int32) (r, w int32, errno int32) func setNonblock(fd int32) +//go:nosplit +func fcntl(fd, cmd, arg int32) (ret int32, errno int32) { + r, _, err := syscall.Syscall6(syscall.SYS_FCNTL, uintptr(fd), uintptr(cmd), uintptr(arg), 0, 0, 0) + return int32(r), int32(err) +} + const ( _si_max_size = 128 _sigev_max_size = 64 @@ -499,6 +520,7 @@ func getsig(i uint32) uintptr { } // setSignaltstackSP sets the ss_sp field of a stackt. +// //go:nosplit func setSignalstackSP(s *stackt, sp uintptr) { *(*uintptr)(unsafe.Pointer(&s.ss_sp)) = sp @@ -509,6 +531,7 @@ func (c *sigctxt) fixsigcode(sig uint32) { } // sysSigaction calls the rt_sigaction system call. +// //go:nosplit func sysSigaction(sig uint32, new, old *sigactiont) { if rt_sigaction(uintptr(sig), new, old, unsafe.Sizeof(sigactiont{}.sa_mask)) != 0 { @@ -533,6 +556,7 @@ func sysSigaction(sig uint32, new, old *sigactiont) { } // rt_sigaction is implemented in assembly. +// //go:noescape func rt_sigaction(sig uintptr, new, old *sigactiont, size uintptr) int32 @@ -855,6 +879,7 @@ func syscall_runtime_doAllThreadsSyscall(trap, a1, a2, a3, a4, a5, a6 uintptr) ( // // This function throws if the system call returns with anything other than the // expected values. +// //go:nosplit func runPerThreadSyscall() { gp := getg() diff --git a/src/runtime/os_netbsd.go b/src/runtime/os_netbsd.go index c4e69fb189c..d3982e8df44 100644 --- a/src/runtime/os_netbsd.go +++ b/src/runtime/os_netbsd.go @@ -83,6 +83,8 @@ func pipe2(flags int32) (r, w int32, errno int32) func closeonexec(fd int32) func setNonblock(fd int32) +func issetugid() int32 + const ( _ESRCH = 3 _ETIMEDOUT = 60 diff --git a/src/runtime/os_openbsd_syscall2.go b/src/runtime/os_openbsd_syscall2.go index 810d5995088..fc50c7f911c 100644 --- a/src/runtime/os_openbsd_syscall2.go +++ b/src/runtime/os_openbsd_syscall2.go @@ -99,3 +99,5 @@ func closeonexec(fd int32) func setNonblock(fd int32) func walltime() (sec int64, nsec int32) + +func issetugid() int32 diff --git a/src/runtime/os_solaris.go b/src/runtime/os_solaris.go index 8ac1b08f690..d56d45ef466 100644 --- a/src/runtime/os_solaris.go +++ b/src/runtime/os_solaris.go @@ -267,3 +267,7 @@ func sysvicall6(fn *libcFunc, a1, a2, a3, a4, a5, a6 uintptr) uintptr { } return libcall.r1 } + +func issetugid() int32 { + return int32(sysvicall0(&libc_issetugid)) +} diff --git a/src/runtime/panic.go b/src/runtime/panic.go index 6600410cb6c..387cc42bd36 100644 --- a/src/runtime/panic.go +++ b/src/runtime/panic.go @@ -1042,6 +1042,10 @@ func fatalthrow() { // Switch to the system stack to avoid any stack growth, which // may make things worse if the runtime is in a bad state. systemstack(func() { + if isSecureMode() { + exit(2) + } + startpanic_m() if dopanic_m(gp, pc, sp) { diff --git a/src/runtime/proc.go b/src/runtime/proc.go index cae15bc8e2c..26b83c536fb 100644 --- a/src/runtime/proc.go +++ b/src/runtime/proc.go @@ -708,6 +708,7 @@ func schedinit() { goargs() goenvs() + secure() parsedebugvars() gcinit() diff --git a/src/runtime/security_aix.go b/src/runtime/security_aix.go new file mode 100644 index 00000000000..c11b9c3f016 --- /dev/null +++ b/src/runtime/security_aix.go @@ -0,0 +1,17 @@ +// 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 + +// secureMode is only ever mutated in schedinit, so we don't need to worry about +// synchronization primitives. +var secureMode bool + +func initSecureMode() { + secureMode = !(getuid() == geteuid() && getgid() == getegid()) +} + +func isSecureMode() bool { + return secureMode +} diff --git a/src/runtime/security_issetugid.go b/src/runtime/security_issetugid.go new file mode 100644 index 00000000000..5048632c3a6 --- /dev/null +++ b/src/runtime/security_issetugid.go @@ -0,0 +1,19 @@ +// 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. + +//go:build darwin || dragonfly || freebsd || illumos || netbsd || openbsd || solaris + +package runtime + +// secureMode is only ever mutated in schedinit, so we don't need to worry about +// synchronization primitives. +var secureMode bool + +func initSecureMode() { + secureMode = issetugid() == 1 +} + +func isSecureMode() bool { + return secureMode +} diff --git a/src/runtime/security_linux.go b/src/runtime/security_linux.go new file mode 100644 index 00000000000..181f3a184e5 --- /dev/null +++ b/src/runtime/security_linux.go @@ -0,0 +1,15 @@ +// 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 + +import _ "unsafe" + +func initSecureMode() { + // We have already initialized the secureMode bool in sysauxv. +} + +func isSecureMode() bool { + return secureMode +} diff --git a/src/runtime/security_nonunix.go b/src/runtime/security_nonunix.go new file mode 100644 index 00000000000..85fbd3ee286 --- /dev/null +++ b/src/runtime/security_nonunix.go @@ -0,0 +1,13 @@ +// 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. + +//go:build !aix && !android && !darwin && !dragonfly && !freebsd && !hurd && !illumos && !ios && !linux && !netbsd && !openbsd && !solaris + +package runtime + +func isSecureMode() bool { + return false +} + +func secure() {} diff --git a/src/runtime/security_test.go b/src/runtime/security_test.go new file mode 100644 index 00000000000..75e217bdfd3 --- /dev/null +++ b/src/runtime/security_test.go @@ -0,0 +1,143 @@ +// 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. + +//go:build aix || android || darwin || dragonfly || freebsd || hurd || illumos || ios || linux || netbsd || openbsd || solaris + +package runtime_test + +import ( + "bytes" + "context" + "fmt" + "internal/testenv" + "io" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "testing" + "time" +) + +func privesc(command string, args ...string) error { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + var cmd *exec.Cmd + if runtime.GOOS == "darwin" { + cmd = exec.CommandContext(ctx, "sudo", append([]string{"-n", command}, args...)...) + } else { + cmd = exec.CommandContext(ctx, "su", highPrivUser, "-c", fmt.Sprintf("%s %s", command, strings.Join(args, " "))) + } + _, err := cmd.CombinedOutput() + return err +} + +const highPrivUser = "root" + +func setSetuid(t *testing.T, user, bin string) { + t.Helper() + // We escalate privileges here even if we are root, because for some reason on some builders + // (at least freebsd-amd64-13_0) the default PATH doesn't include /usr/sbin, which is where + // chown lives, but using 'su root -c' gives us the correct PATH. + + // buildTestProg uses os.MkdirTemp which creates directories with 0700, which prevents + // setuid binaries from executing because of the missing g+rx, so we need to set the parent + // directory to better permissions before anything else. We created this directory, so we + // shouldn't need to do any privilege trickery. + if err := privesc("chmod", "0777", filepath.Dir(bin)); err != nil { + t.Skipf("unable to set permissions on %q, likely no passwordless sudo/su: %s", filepath.Dir(bin), err) + } + + if err := privesc("chown", user, bin); err != nil { + t.Skipf("unable to set permissions on test binary, likely no passwordless sudo/su: %s", err) + } + if err := privesc("chmod", "u+s", bin); err != nil { + t.Skipf("unable to set permissions on test binary, likely no passwordless sudo/su: %s", err) + } +} + +func TestSUID(t *testing.T) { + // This test is relatively simple, we build a test program which opens a + // file passed via the TEST_OUTPUT envvar, prints the value of the + // GOTRACEBACK envvar to stdout, and prints "hello" to stderr. We then chown + // the program to "nobody" and set u+s on it. We execute the program, only + // passing it two files, for stdin and stdout, and passing + // GOTRACEBACK=system in the env. + // + // We expect that the program will trigger the SUID protections, resetting + // the value of GOTRACEBACK, and opening the missing stderr descriptor, such + // that the program prints "GOTRACEBACK=none" to stdout, and nothing gets + // written to the file pointed at by TEST_OUTPUT. + + if *flagQuick { + t.Skip("-quick") + } + + testenv.MustHaveGoBuild(t) + + helloBin, err := buildTestProg(t, "testsuid") + if err != nil { + t.Fatal(err) + } + + f, err := os.CreateTemp(t.TempDir(), "suid-output") + if err != nil { + t.Fatal(err) + } + tempfilePath := f.Name() + f.Close() + + lowPrivUser := "nobody" + setSetuid(t, lowPrivUser, helloBin) + + b := bytes.NewBuffer(nil) + pr, pw, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + + proc, err := os.StartProcess(helloBin, []string{helloBin}, &os.ProcAttr{ + Env: []string{"GOTRACEBACK=system", "TEST_OUTPUT=" + tempfilePath}, + Files: []*os.File{os.Stdin, pw}, + }) + if err != nil { + if os.IsPermission(err) { + t.Skip("don't have execute permission on setuid binary, possibly directory permission issue?") + } + t.Fatal(err) + } + done := make(chan bool, 1) + go func() { + io.Copy(b, pr) + pr.Close() + done <- true + }() + ps, err := proc.Wait() + if err != nil { + t.Fatal(err) + } + pw.Close() + <-done + output := b.String() + + if ps.ExitCode() == 99 { + t.Skip("binary wasn't setuid (uid == euid), unable to effectively test") + } + + expected := "GOTRACEBACK=none\n" + if output != expected { + t.Errorf("unexpected output, got: %q, want %q", output, expected) + } + + fc, err := os.ReadFile(tempfilePath) + if err != nil { + t.Fatal(err) + } + if string(fc) != "" { + t.Errorf("unexpected file content, got: %q", string(fc)) + } + + // TODO: check the registers aren't leaked? +} diff --git a/src/runtime/security_unix.go b/src/runtime/security_unix.go new file mode 100644 index 00000000000..ade5ed90300 --- /dev/null +++ b/src/runtime/security_unix.go @@ -0,0 +1,72 @@ +// 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. + +//go:build aix || android || darwin || dragonfly || freebsd || hurd || illumos || ios || linux || netbsd || openbsd || solaris + +package runtime + +func secure() { + initSecureMode() + + if !isSecureMode() { + return + } + + // When secure mode is enabled, we do two things: + // 1. ensure the file descriptors 0, 1, and 2 are open, and if not open them, + // pointing at /dev/null (or fail) + // 2. enforce specific environment variable values (currently we only force + // GOTRACEBACK=none) + // + // Other packages may also disable specific functionality when secure mode + // is enabled (determined by using linkname to call isSecureMode). + // + // NOTE: we may eventually want to enforce (1) regardless of whether secure + // mode is enabled or not. + + secureFDs() + secureEnv() +} + +func secureEnv() { + var hasTraceback bool + for i := 0; i < len(envs); i++ { + if hasPrefix(envs[i], "GOTRACEBACK=") { + hasTraceback = true + envs[i] = "GOTRACEBACK=none" + } + } + if !hasTraceback { + envs = append(envs, "GOTRACEBACK=none") + } +} + +func secureFDs() { + const ( + // F_GETFD and EBADF are standard across all unixes, define + // them here rather than in each of the OS specific files + F_GETFD = 0x01 + EBADF = 0x09 + ) + + devNull := []byte("/dev/null\x00") + for i := 0; i < 3; i++ { + ret, errno := fcntl(int32(i), F_GETFD, 0) + if ret >= 0 { + continue + } + if errno != EBADF { + print("runtime: unexpected error while checking standard file descriptor ", i, ", errno=", errno, "\n") + throw("cannot secure fds") + } + + if ret := open(&devNull[0], 2 /* O_RDWR */, 0); ret < 0 { + print("runtime: standard file descriptor ", i, " closed, unable to open /dev/null, errno=", errno, "\n") + throw("cannot secure fds") + } else if ret != int32(i) { + print("runtime: opened unexpected file descriptor ", ret, " when attempting to open ", i, "\n") + throw("cannot secure fds") + } + } +} diff --git a/src/runtime/signal_unix.go b/src/runtime/signal_unix.go index 5cb51d10ba9..64bea67b910 100644 --- a/src/runtime/signal_unix.go +++ b/src/runtime/signal_unix.go @@ -707,6 +707,10 @@ func sighandler(sig uint32, info *siginfo, ctxt unsafe.Pointer, gp *g) { print("Signal ", sig, "\n") } + if isSecureMode() { + exit(2) + } + print("PC=", hex(c.sigpc()), " m=", _g_.m.id, " sigcode=", c.sigcode(), "\n") if _g_.m.incgo && gp == _g_.m.g0 && _g_.m.curg != nil { print("signal arrived during cgo execution\n") diff --git a/src/runtime/sys_darwin.go b/src/runtime/sys_darwin.go index c90cc78968c..67b894f5890 100644 --- a/src/runtime/sys_darwin.go +++ b/src/runtime/sys_darwin.go @@ -171,6 +171,7 @@ func pthread_kill_trampoline() // mmap is used to do low-level memory allocation via mmap. Don't allow stack // splits, since this function (used by sysAlloc) is called in a lot of low-level // parts of the runtime and callers often assume it won't acquire any locks. +// //go:nosplit func mmap(addr unsafe.Pointer, n uintptr, prot, flags, fd int32, off uint32) (unsafe.Pointer, int) { args := struct { @@ -233,10 +234,10 @@ func closefd(fd int32) int32 { } func close_trampoline() +// This is exported via linkname to assembly in runtime/cgo. +// //go:nosplit //go:cgo_unsafe_args -// -// This is exported via linkname to assembly in runtime/cgo. //go:linkname exit func exit(code int32) { libcCall(unsafe.Pointer(abi.FuncPCABI0(exit_trampoline)), unsafe.Pointer(&code)) @@ -381,8 +382,12 @@ func sysctlbyname_trampoline() //go:nosplit //go:cgo_unsafe_args -func fcntl(fd, cmd, arg int32) int32 { - return libcCall(unsafe.Pointer(abi.FuncPCABI0(fcntl_trampoline)), unsafe.Pointer(&fd)) +func fcntl(fd, cmd, arg int32) (int32, int32) { + res := libcCall(unsafe.Pointer(abi.FuncPCABI0(fcntl_trampoline)), unsafe.Pointer(&fd)) + if res < 0 { + return 0, int32(-res) + } + return res, 0 } func fcntl_trampoline() @@ -484,10 +489,15 @@ func closeonexec(fd int32) { //go:nosplit func setNonblock(fd int32) { - flags := fcntl(fd, _F_GETFL, 0) + flags, _ := fcntl(fd, _F_GETFL, 0) fcntl(fd, _F_SETFL, flags|_O_NONBLOCK) } +func issetugid() int32 { + return libcCall(unsafe.Pointer(abi.FuncPCABI0(issetugid_trampoline)), nil) +} +func issetugid_trampoline() + // Tell the linker that the libc_* functions are to be found // in a system library, with the libc_ prefix missing. @@ -535,3 +545,5 @@ func setNonblock(fd int32) { //go:cgo_import_dynamic libc_pthread_cond_wait pthread_cond_wait "/usr/lib/libSystem.B.dylib" //go:cgo_import_dynamic libc_pthread_cond_timedwait_relative_np pthread_cond_timedwait_relative_np "/usr/lib/libSystem.B.dylib" //go:cgo_import_dynamic libc_pthread_cond_signal pthread_cond_signal "/usr/lib/libSystem.B.dylib" + +//go:cgo_import_dynamic libc_issetugid issetugid "/usr/lib/libSystem.B.dylib" diff --git a/src/runtime/sys_darwin_amd64.s b/src/runtime/sys_darwin_amd64.s index db4715d2b77..7d71cb6cb1e 100644 --- a/src/runtime/sys_darwin_amd64.s +++ b/src/runtime/sys_darwin_amd64.s @@ -857,3 +857,7 @@ TEXT runtime·syscall_x509(SB),NOSPLIT,$0 MOVQ BP, SP POPQ BP RET + +TEXT runtime·issetugid_trampoline(SB),NOSPLIT,$0 + CALL libc_issetugid(SB) + RET diff --git a/src/runtime/sys_darwin_arm64.s b/src/runtime/sys_darwin_arm64.s index e57ac53e108..2ef37eda8c3 100644 --- a/src/runtime/sys_darwin_arm64.s +++ b/src/runtime/sys_darwin_arm64.s @@ -756,3 +756,7 @@ TEXT runtime·syscall_x509(SB),NOSPLIT,$0 ADD $16, RSP MOVD R0, 56(R2) // save r1 RET + +TEXT runtime·issetugid_trampoline(SB),NOSPLIT,$0 + BL libc_issetugid(SB) + RET diff --git a/src/runtime/sys_dragonfly_amd64.s b/src/runtime/sys_dragonfly_amd64.s index 3af0928828a..a705612cb92 100644 --- a/src/runtime/sys_dragonfly_amd64.s +++ b/src/runtime/sys_dragonfly_amd64.s @@ -417,3 +417,13 @@ TEXT runtime·setNonblock(SB),NOSPLIT,$0-4 MOVL $92, AX // fcntl SYSCALL RET + +// func issetugid() int32 +TEXT runtime·issetugid(SB),NOSPLIT,$0 + MOVQ $0, DI + MOVQ $0, SI + MOVQ $0, DX + MOVL $253, AX + SYSCALL + MOVL AX, ret+0(FP) + RET diff --git a/src/runtime/sys_freebsd_386.s b/src/runtime/sys_freebsd_386.s index d4c4cc7fdbb..fc22abfdda4 100644 --- a/src/runtime/sys_freebsd_386.s +++ b/src/runtime/sys_freebsd_386.s @@ -470,3 +470,10 @@ TEXT runtime·cpuset_getaffinity(SB), NOSPLIT, $0-28 RET GLOBL runtime·tlsoffset(SB),NOPTR,$4 + +// func issetugid() int32 +TEXT runtime·issetugid(SB),NOSPLIT,$0 + MOVL $253, AX + INT $0x80 + MOVL AX, ret+0(FP) + RET diff --git a/src/runtime/sys_freebsd_amd64.s b/src/runtime/sys_freebsd_amd64.s index 57ae0399a5f..5d092a9b798 100644 --- a/src/runtime/sys_freebsd_amd64.s +++ b/src/runtime/sys_freebsd_amd64.s @@ -519,3 +519,13 @@ TEXT runtime·cpuset_getaffinity(SB), NOSPLIT, $0-44 NEGQ AX MOVL AX, ret+40(FP) RET + +// func issetugid() int32 +TEXT runtime·issetugid(SB),NOSPLIT,$0 + MOVQ $0, DI + MOVQ $0, SI + MOVQ $0, DX + MOVL $253, AX + SYSCALL + MOVL AX, ret+0(FP) + RET diff --git a/src/runtime/sys_freebsd_arm.s b/src/runtime/sys_freebsd_arm.s index 8546eba8058..55fb0a9a28c 100644 --- a/src/runtime/sys_freebsd_arm.s +++ b/src/runtime/sys_freebsd_arm.s @@ -28,6 +28,7 @@ #define SYS_fcntl (SYS_BASE + 92) #define SYS___sysctl (SYS_BASE + 202) #define SYS_nanosleep (SYS_BASE + 240) +#define SYS_issetugid (SYS_BASE + 253) #define SYS_clock_gettime (SYS_BASE + 232) #define SYS_sched_yield (SYS_BASE + 331) #define SYS_sigprocmask (SYS_BASE + 340) @@ -473,3 +474,10 @@ TEXT runtime·getCntxct(SB),NOSPLIT|NOFRAME,$0-8 MOVW R0, ret+4(FP) RET + +// func issetugid() int32 +TEXT runtime·issetugid(SB),NOSPLIT,$0 + MOVW $SYS_issetugid, R7 + SWI $0 + MOVW R0, ret+0(FP) + RET diff --git a/src/runtime/sys_freebsd_arm64.s b/src/runtime/sys_freebsd_arm64.s index 7dab3028de4..bb32de8b01e 100644 --- a/src/runtime/sys_freebsd_arm64.s +++ b/src/runtime/sys_freebsd_arm64.s @@ -33,6 +33,7 @@ #define SYS_fcntl 92 #define SYS___sysctl 202 #define SYS_nanosleep 240 +#define SYS_issetugid 253 #define SYS_clock_gettime 232 #define SYS_sched_yield 331 #define SYS_sigprocmask 340 @@ -521,3 +522,10 @@ TEXT runtime·getCntxct(SB),NOSPLIT,$0 MOVW R0, ret+8(FP) RET + +// func issetugid() int32 +TEXT runtime·issetugid(SB),NOSPLIT|NOFRAME,$0 + MOVD $SYS_issetugid, R8 + SVC + MOVW R0, ret+0(FP) + RET diff --git a/src/runtime/sys_netbsd_386.s b/src/runtime/sys_netbsd_386.s index 6c5386bcf12..1c54bbb856a 100644 --- a/src/runtime/sys_netbsd_386.s +++ b/src/runtime/sys_netbsd_386.s @@ -29,6 +29,7 @@ #define SYS___sysctl 202 #define SYS___sigaltstack14 281 #define SYS___sigprocmask14 293 +#define SYS_issetugid 305 #define SYS_getcontext 307 #define SYS_setcontext 308 #define SYS__lwp_create 309 @@ -501,3 +502,10 @@ TEXT runtime·setNonblock(SB),NOSPLIT,$16-4 MOVL $92, AX // fcntl INT $0x80 RET + +// func issetugid() int32 +TEXT runtime·issetugid(SB),NOSPLIT,$0 + MOVL $SYS_issetugid, AX + INT $0x80 + MOVL AX, ret+0(FP) + RET diff --git a/src/runtime/sys_netbsd_amd64.s b/src/runtime/sys_netbsd_amd64.s index c1cd95df149..3a237e3c2df 100644 --- a/src/runtime/sys_netbsd_amd64.s +++ b/src/runtime/sys_netbsd_amd64.s @@ -30,6 +30,7 @@ #define SYS___sysctl 202 #define SYS___sigaltstack14 281 #define SYS___sigprocmask14 293 +#define SYS_issetugid 305 #define SYS_getcontext 307 #define SYS_setcontext 308 #define SYS__lwp_create 309 @@ -464,3 +465,13 @@ TEXT runtime·setNonblock(SB),NOSPLIT,$0-4 MOVL $92, AX // fcntl SYSCALL RET + +// func issetugid() int32 +TEXT runtime·issetugid(SB),NOSPLIT,$0 + MOVQ $0, DI + MOVQ $0, SI + MOVQ $0, DX + MOVL $SYS_issetugid, AX + SYSCALL + MOVL AX, ret+0(FP) + RET diff --git a/src/runtime/sys_netbsd_arm.s b/src/runtime/sys_netbsd_arm.s index 2422b0282e9..0fb2da59c73 100644 --- a/src/runtime/sys_netbsd_arm.s +++ b/src/runtime/sys_netbsd_arm.s @@ -30,6 +30,7 @@ #define SYS___sysctl SWI_OS_NETBSD | 202 #define SYS___sigaltstack14 SWI_OS_NETBSD | 281 #define SYS___sigprocmask14 SWI_OS_NETBSD | 293 +#define SYS_issetugid SWI_OS_NETBSD | 305 #define SYS_getcontext SWI_OS_NETBSD | 307 #define SYS_setcontext SWI_OS_NETBSD | 308 #define SYS__lwp_create SWI_OS_NETBSD | 309 @@ -443,3 +444,9 @@ TEXT runtime·read_tls_fallback(SB),NOSPLIT|NOFRAME,$0 SWI $SYS__lwp_getprivate MOVM.IAW (R13), [R1, R2, R3, R12] RET + +// func issetugid() int32 +TEXT runtime·issetugid(SB),NOSPLIT,$0 + SWI $SYS_issetugid + MOVW R0, ret+0(FP) + RET diff --git a/src/runtime/sys_netbsd_arm64.s b/src/runtime/sys_netbsd_arm64.s index 6d2c31631d0..0bb86229f06 100644 --- a/src/runtime/sys_netbsd_arm64.s +++ b/src/runtime/sys_netbsd_arm64.s @@ -32,6 +32,7 @@ #define SYS___sysctl 202 #define SYS___sigaltstack14 281 #define SYS___sigprocmask14 293 +#define SYS_issetugid 305 #define SYS_getcontext 307 #define SYS_setcontext 308 #define SYS__lwp_create 309 @@ -479,3 +480,9 @@ TEXT runtime·setNonblock(SB),NOSPLIT|NOFRAME,$0-4 MOVD $F_SETFL, R1 // arg 2 - cmd SVC $SYS_fcntl RET + +// func issetugid() int32 +TEXT runtime·issetugid(SB),NOSPLIT|NOFRAME,$0 + SVC $SYS_issetugid + MOVW R0, ret+0(FP) + RET diff --git a/src/runtime/sys_openbsd2.go b/src/runtime/sys_openbsd2.go index cba35cfee3e..deb7e8404ff 100644 --- a/src/runtime/sys_openbsd2.go +++ b/src/runtime/sys_openbsd2.go @@ -265,6 +265,14 @@ func setNonblock(fd int32) { fcntl(fd, _F_SETFL, flags|_O_NONBLOCK) } +//go:nosplit +//go:cgo_unsafe_args +func issetugid() (ret int32) { + libcCall(unsafe.Pointer(abi.FuncPCABI0(issetugid_trampoline)), unsafe.Pointer(&ret)) + return +} +func issetugid_trampoline() + // Tell the linker that the libc_* functions are to be found // in a system library, with the libc_ prefix missing. @@ -297,4 +305,6 @@ func setNonblock(fd int32) { //go:cgo_import_dynamic libc_sigaction sigaction "libc.so" //go:cgo_import_dynamic libc_sigaltstack sigaltstack "libc.so" +//go:cgo_import_dynamic libc_issetugid issetugid "libc.so" + //go:cgo_import_dynamic _ _ "libc.so" diff --git a/src/runtime/sys_openbsd_386.s b/src/runtime/sys_openbsd_386.s index 890b96b673d..3113e7ed294 100644 --- a/src/runtime/sys_openbsd_386.s +++ b/src/runtime/sys_openbsd_386.s @@ -969,3 +969,12 @@ ok: MOVL BP, SP POPL BP RET + +TEXT runtime·issetugid_trampoline(SB),NOSPLIT,$0 + PUSHL BP + CALL libc_issetugid(SB) + NOP SP // tell vet SP changed - stop checking offsets + MOVL 8(SP), DX // pointer to return value + MOVL AX, 0(DX) + POPL BP + RET diff --git a/src/runtime/sys_openbsd_amd64.s b/src/runtime/sys_openbsd_amd64.s index fc6d5dc387d..19025b2906b 100644 --- a/src/runtime/sys_openbsd_amd64.s +++ b/src/runtime/sys_openbsd_amd64.s @@ -766,3 +766,9 @@ ok: MOVQ BP, SP POPQ BP RET + +TEXT runtime·issetugid_trampoline(SB),NOSPLIT,$0 + MOVQ DI, BX // BX is caller-save + CALL libc_issetugid(SB) + MOVL AX, 0(BX) // return value + RET diff --git a/src/runtime/sys_openbsd_arm.s b/src/runtime/sys_openbsd_arm.s index a9cb1fbafe1..dd2712b531c 100644 --- a/src/runtime/sys_openbsd_arm.s +++ b/src/runtime/sys_openbsd_arm.s @@ -806,3 +806,12 @@ ok: MOVW $0, R0 // no error (it's ignored anyway) MOVW R9, R13 RET + +TEXT runtime·issetugid_trampoline(SB),NOSPLIT,$0 + MOVW R13, R9 + MOVW R0, R8 + BIC $0x7, R13 // align for ELF ABI + BL libc_issetugid(SB) + MOVW R0, 0(R8) + MOVW R9, R13 + RET diff --git a/src/runtime/sys_openbsd_arm64.s b/src/runtime/sys_openbsd_arm64.s index 3fa7e1ede25..0dfcc82bf90 100644 --- a/src/runtime/sys_openbsd_arm64.s +++ b/src/runtime/sys_openbsd_arm64.s @@ -700,3 +700,9 @@ TEXT runtime·syscall10X(SB),NOSPLIT,$0 ok: RET + +TEXT runtime·issetugid_trampoline(SB),NOSPLIT,$0 + MOVD R0, R19 // pointer to args + CALL libc_issetugid(SB) + MOVW R0, 0(R19) // return value + RET diff --git a/src/runtime/sys_openbsd_mips64.s b/src/runtime/sys_openbsd_mips64.s index bc392e4c54d..b32ae800803 100644 --- a/src/runtime/sys_openbsd_mips64.s +++ b/src/runtime/sys_openbsd_mips64.s @@ -398,3 +398,10 @@ TEXT runtime·setNonblock(SB),NOSPLIT|NOFRAME,$0-4 MOVV $92, R2 // sys_fcntl SYSCALL RET + +// func issetugid() int32 +TEXT runtime·issetugid(SB),NOSPLIT,$0 + MOVV $253, R2 // sys_issetugid + SYSCALL + MOVW R2, ret+0(FP) + RET diff --git a/src/runtime/syscall2_solaris.go b/src/runtime/syscall2_solaris.go index 33104892021..0b5ebfda029 100644 --- a/src/runtime/syscall2_solaris.go +++ b/src/runtime/syscall2_solaris.go @@ -22,6 +22,7 @@ import _ "unsafe" // for go:linkname //go:cgo_import_dynamic libc_setpgid setpgid "libc.so" //go:cgo_import_dynamic libc_syscall syscall "libc.so" //go:cgo_import_dynamic libc_wait4 wait4 "libc.so" +//go:cgo_import_dynamic libc_issetugid issetugid "libc.so" //go:linkname libc_chdir libc_chdir //go:linkname libc_chroot libc_chroot @@ -39,3 +40,4 @@ import _ "unsafe" // for go:linkname //go:linkname libc_setpgid libc_setpgid //go:linkname libc_syscall libc_syscall //go:linkname libc_wait4 libc_wait4 +//go:linkname libc_issetugid libc_issetugid diff --git a/src/runtime/syscall_solaris.go b/src/runtime/syscall_solaris.go index e270e271c0a..7fd4e4fe159 100644 --- a/src/runtime/syscall_solaris.go +++ b/src/runtime/syscall_solaris.go @@ -22,6 +22,7 @@ var ( libc_setuid, libc_setpgid, libc_syscall, + libc_issetugid, libc_wait4 libcFunc ) diff --git a/src/runtime/testdata/testsuid/main.go b/src/runtime/testdata/testsuid/main.go new file mode 100644 index 00000000000..1949d2d6662 --- /dev/null +++ b/src/runtime/testdata/testsuid/main.go @@ -0,0 +1,25 @@ +// 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 main + +import ( + "fmt" + "log" + "os" +) + +func main() { + if os.Geteuid() == os.Getuid() { + os.Exit(99) + } + + fmt.Fprintf(os.Stdout, "GOTRACEBACK=%s\n", os.Getenv("GOTRACEBACK")) + f, err := os.OpenFile(os.Getenv("TEST_OUTPUT"), os.O_CREATE|os.O_RDWR, 0600) + if err != nil { + log.Fatalf("os.Open failed: %s", err) + } + defer f.Close() + fmt.Fprintf(os.Stderr, "hello\n") +}