From 5da979b4033759d952d64bbc727d1fc45bb785f0 Mon Sep 17 00:00:00 2001
From: Michael Pratt <mpratt@google.com>
Date: Tue, 8 Feb 2022 16:45:14 -0500
Subject: [PATCH] runtime: implement SUID/SGID protections

Offering: Cloud Core Network
CVE: CVE-2023-29403
Reference: https://go-review.googlesource.com/c/go/+/501228

On Unix platforms, the runtime previously did nothing special when a
program was run with either the SUID or SGID bits set. This can be
dangerous in certain cases, such as when dumping memory state, or
assuming the status of standard i/o file descriptors.

Taking cues from glibc, this change implements a set of protections when
a binary is run with SUID or SGID bits set (or is SUID/SGID-like). On
Linux, whether to enable these protections is determined by whether the
AT_SECURE flag is passed in the auxiliary vector. On platforms which
have the issetugid syscall (the BSDs, darwin, and Solaris/Illumos), that
is used. On the remaining platforms (currently only AIX) we check
!(getuid() == geteuid() && getgid == getegid()).

Currently when we determine a binary is "tainted" (using the glibc
terminology), we implement two specific protections:
  1. we check if the file descriptors 0, 1, and 2 are open, and if they
     are not, we open them, pointing at /dev/null (or fail).
  2. we force GOTRACKBACK=none, and generally prevent dumping of
     trackbacks and registers when a program panics/aborts.

In the future we may add additional protections.

This change requires implementing issetugid on the platforms which
support it, and implementing getuid, geteuid, getgid, and getegid on
AIX.

Thanks to Vincent Dehors from Synacktiv for reporting this issue.

Note: When the runtime determines the binary is setuid/setgid-like and a signal is received that terminates the program, set inFatal to true. The buildtag unix was only introduced in go 1.19, so you need to change //go:build unix to the following:

//go:build aix || android || darwin || dragonfly || freebsd || hurd || illumos || ios || linux || netbsd || openbsd || solaris

Edited-by: tangxi t00586138

Updates #60272
Fixes #60517
Fixes CVE-2023-29403

Change-Id: I057fa7153d29cf26515e7f49fed86e4f8bedd0f0
Reviewed-on: https://team-review.git.corp.google.com/c/golang/go-private/+/1878434
Reviewed-by: Damien Neil <dneil@google.com>
Reviewed-by: Ian Lance Taylor <iant@google.com>
Run-TryBot: Roland Shoemaker <bracewell@google.com>
Reviewed-by: Russ Cox <rsc@google.com>
(cherry picked from commit 87065663ea6d89cd54f65a515d8f2ed0ef285c19)
Reviewed-on: https://team-review.git.corp.google.com/c/golang/go-private/+/1902231
Reviewed-on: https://team-review.git.corp.google.com/c/golang/go-private/+/1904340
Reviewed-by: Michael Knyszek <mknyszek@google.com>
Reviewed-on: https://go-review.googlesource.com/c/go/+/501228
Auto-Submit: Michael Knyszek <mknyszek@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Run-TryBot: David Chase <drchase@google.com>
Signed-off-by: Tang Xi tangxi6@huawei.com
---
 src/cmd/compile/internal/gc/racewalk.go       |   3 +-
 src/go/build/deps_test.go                     |   1 +
 src/runtime/export_darwin_test.go             |   7 -
 src/runtime/export_unix_test.go               |   1 +
 src/runtime/extern.go                         |  19 +++
 src/runtime/internal/syscall/asm_linux_386.s  |  34 ++++
 .../internal/syscall/asm_linux_amd64.s        |  33 ++++
 src/runtime/internal/syscall/asm_linux_arm.s  |  32 ++++
 .../internal/syscall/asm_linux_arm64.s        |  29 ++++
 .../internal/syscall/defs_linux_386.go        |   7 +
 .../internal/syscall/defs_linux_amd64.go      |   7 +
 .../internal/syscall/defs_linux_arm.go        |   7 +
 .../internal/syscall/defs_linux_arm64.go      |   7 +
 src/runtime/internal/syscall/syscall_linux.go |  12 ++
 src/runtime/nbpipe_fcntl_libc_test.go         |  18 ---
 src/runtime/nbpipe_fcntl_unix_test.go         |  17 --
 src/runtime/nbpipe_test.go                    |  26 ++--
 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_solaris.go                     |   4 +
 src/runtime/panic.go                          |   3 +
 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                  | 145 ++++++++++++++++++
 src/runtime/security_unix.go                  |  72 +++++++++
 src/runtime/signal_unix.go                    |   4 +
 src/runtime/sys_darwin.go                     |  27 +++-
 src/runtime/sys_darwin_amd64.s                |   7 +
 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_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/syscall2_solaris.go               |   2 +
 src/runtime/syscall_solaris.go                |   1 +
 src/runtime/testdata/testsuid/main.go         |  25 +++
 52 files changed, 749 insertions(+), 59 deletions(-)
 create mode 100644 src/runtime/internal/syscall/asm_linux_386.s
 create mode 100644 src/runtime/internal/syscall/asm_linux_amd64.s
 create mode 100644 src/runtime/internal/syscall/asm_linux_arm.s
 create mode 100644 src/runtime/internal/syscall/asm_linux_arm64.s
 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/syscall_linux.go
 delete mode 100644 src/runtime/nbpipe_fcntl_libc_test.go
 delete mode 100644 src/runtime/nbpipe_fcntl_unix_test.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/cmd/compile/internal/gc/racewalk.go b/src/cmd/compile/internal/gc/racewalk.go
index 6f25137..710c97a 100644
--- a/src/cmd/compile/internal/gc/racewalk.go
+++ b/src/cmd/compile/internal/gc/racewalk.go
@@ -34,8 +34,9 @@ import (
 // at best instrumentation would cause infinite recursion.
 var omit_pkgs = []string{
 	"runtime/internal/atomic",
-	"runtime/internal/sys",
 	"runtime/internal/math",
+	"runtime/internal/sys",
+	"runtime/internal/syscall",
 	"runtime",
 	"runtime/race",
 	"runtime/msan",
diff --git a/src/go/build/deps_test.go b/src/go/build/deps_test.go
index 875aceb..c134417 100644
--- a/src/go/build/deps_test.go
+++ b/src/go/build/deps_test.go
@@ -81,6 +81,7 @@ var depsRules = `
 	< internal/bytealg
 	< internal/unsafeheader
 	< runtime/internal/sys
+	< runtime/internal/syscall
 	< runtime/internal/atomic
 	< runtime/internal/math
 	< runtime
diff --git a/src/runtime/export_darwin_test.go b/src/runtime/export_darwin_test.go
index e9b6eb3..034c52d 100644
--- a/src/runtime/export_darwin_test.go
+++ b/src/runtime/export_darwin_test.go
@@ -4,10 +4,3 @@
 
 package runtime
 
-func Fcntl(fd, cmd, arg uintptr) (uintptr, uintptr) {
-	r := fcntl(int32(fd), int32(cmd), int32(arg))
-	if r < 0 {
-		return ^uintptr(0), uintptr(-r)
-	}
-	return uintptr(r), 0
-}
diff --git a/src/runtime/export_unix_test.go b/src/runtime/export_unix_test.go
index 621488e..b1cf15f 100644
--- a/src/runtime/export_unix_test.go
+++ b/src/runtime/export_unix_test.go
@@ -12,6 +12,7 @@ var NonblockingPipe = nonblockingPipe
 var Pipe = pipe
 var SetNonblock = setNonblock
 var Closeonexec = closeonexec
+var Fcntl = fcntl
 
 func sigismember(mask *sigset, i int) bool {
 	clear := *mask
diff --git a/src/runtime/extern.go b/src/runtime/extern.go
index 7316503..056465c 100644
--- a/src/runtime/extern.go
+++ b/src/runtime/extern.go
@@ -172,6 +172,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/asm_linux_386.s b/src/runtime/internal/syscall/asm_linux_386.s
new file mode 100644
index 0000000..15aae4d
--- /dev/null
+++ b/src/runtime/internal/syscall/asm_linux_386.s
@@ -0,0 +1,34 @@
+// 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.
+
+#include "textflag.h"
+
+// See ../sys_linux_386.s for the reason why we always use int 0x80
+// instead of the glibc-specific "CALL 0x10(GS)".
+#define INVOKE_SYSCALL	INT	$0x80
+
+// func Syscall6(num, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2, errno uintptr)
+//
+// Syscall # in AX, args in BX CX DX SI DI BP, return in AX
+TEXT ·Syscall6(SB),NOSPLIT,$0-40
+	MOVL	num+0(FP), AX	// syscall entry
+	MOVL	a1+4(FP), BX
+	MOVL	a2+8(FP), CX
+	MOVL	a3+12(FP), DX
+	MOVL	a4+16(FP), SI
+	MOVL	a5+20(FP), DI
+	MOVL	a6+24(FP), BP
+	INVOKE_SYSCALL
+	CMPL	AX, $0xfffff001
+	JLS	ok
+	MOVL	$-1, r1+28(FP)
+	MOVL	$0, r2+32(FP)
+	NEGL	AX
+	MOVL	AX, errno+36(FP)
+	RET
+ok:
+	MOVL	AX, r1+28(FP)
+	MOVL	DX, r2+32(FP)
+	MOVL	$0, errno+36(FP)
+	RET
diff --git a/src/runtime/internal/syscall/asm_linux_amd64.s b/src/runtime/internal/syscall/asm_linux_amd64.s
new file mode 100644
index 0000000..961d9bd
--- /dev/null
+++ b/src/runtime/internal/syscall/asm_linux_amd64.s
@@ -0,0 +1,33 @@
+// 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.
+
+#include "textflag.h"
+
+// func Syscall6(num, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2, errno uintptr)
+//
+// Syscall # in AX, args in DI SI DX R10 R8 R9, return in AX DX.
+//
+// Note that this differs from "standard" ABI convention, which would pass 4th
+// arg in CX, not R10.
+TEXT ·Syscall6(SB),NOSPLIT,$0-80
+	MOVQ	num+0(FP), AX	// syscall entry
+	MOVQ	a1+8(FP), DI
+	MOVQ	a2+16(FP), SI
+	MOVQ	a3+24(FP), DX
+	MOVQ	a4+32(FP), R10
+	MOVQ	a5+40(FP), R8
+	MOVQ	a6+48(FP), R9
+	SYSCALL
+	CMPQ	AX, $0xfffffffffffff001
+	JLS	ok
+	MOVQ	$-1, r1+56(FP)
+	MOVQ	$0, r2+64(FP)
+	NEGQ	AX
+	MOVQ	AX, errno+72(FP)
+	RET
+ok:
+	MOVQ	AX, r1+56(FP)
+	MOVQ	DX, r2+64(FP)
+	MOVQ	$0, errno+72(FP)
+	RET
diff --git a/src/runtime/internal/syscall/asm_linux_arm.s b/src/runtime/internal/syscall/asm_linux_arm.s
new file mode 100644
index 0000000..dbf1826
--- /dev/null
+++ b/src/runtime/internal/syscall/asm_linux_arm.s
@@ -0,0 +1,32 @@
+// 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.
+
+#include "textflag.h"
+
+// func Syscall6(num, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2, errno uintptr)
+TEXT ·Syscall6(SB),NOSPLIT,$0-40
+	MOVW	num+0(FP), R7	// syscall entry
+	MOVW	a1+4(FP), R0
+	MOVW	a2+8(FP), R1
+	MOVW	a3+12(FP), R2
+	MOVW	a4+16(FP), R3
+	MOVW	a5+20(FP), R4
+	MOVW	a6+24(FP), R5
+	SWI	$0
+	MOVW	$0xfffff001, R6
+	CMP	R6, R0
+	BLS	ok
+	MOVW	$-1, R1
+	MOVW	R1, r1+28(FP)
+	MOVW	$0, R2
+	MOVW	R2, r2+32(FP)
+	RSB	$0, R0, R0
+	MOVW	R0, errno+36(FP)
+	RET
+ok:
+	MOVW	R0, r1+28(FP)
+	MOVW	R1, r2+32(FP)
+	MOVW	$0, R0
+	MOVW	R0, errno+36(FP)
+	RET
diff --git a/src/runtime/internal/syscall/asm_linux_arm64.s b/src/runtime/internal/syscall/asm_linux_arm64.s
new file mode 100644
index 0000000..83e862f
--- /dev/null
+++ b/src/runtime/internal/syscall/asm_linux_arm64.s
@@ -0,0 +1,29 @@
+// 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.
+
+#include "textflag.h"
+
+// func Syscall6(num, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2, errno uintptr)
+TEXT ·Syscall6(SB),NOSPLIT,$0-80
+	MOVD	num+0(FP), R8	// syscall entry
+	MOVD	a1+8(FP), R0
+	MOVD	a2+16(FP), R1
+	MOVD	a3+24(FP), R2
+	MOVD	a4+32(FP), R3
+	MOVD	a5+40(FP), R4
+	MOVD	a6+48(FP), R5
+	SVC
+	CMN	$4095, R0
+	BCC	ok
+	MOVD	$-1, R4
+	MOVD	R4, r1+56(FP)
+	MOVD	ZR, r2+64(FP)
+	NEG	R0, R0
+	MOVD	R0, errno+72(FP)
+	RET
+ok:
+	MOVD	R0, r1+56(FP)
+	MOVD	R1, r2+64(FP)
+	MOVD	ZR, errno+72(FP)
+	RET
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 0000000..31d704e
--- /dev/null
+++ b/src/runtime/internal/syscall/defs_linux_386.go
@@ -0,0 +1,7 @@
+// 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 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 0000000..2368eb0
--- /dev/null
+++ b/src/runtime/internal/syscall/defs_linux_amd64.go
@@ -0,0 +1,7 @@
+// 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 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 0000000..31d704e
--- /dev/null
+++ b/src/runtime/internal/syscall/defs_linux_arm.go
@@ -0,0 +1,7 @@
+// 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 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 0000000..6292c90
--- /dev/null
+++ b/src/runtime/internal/syscall/defs_linux_arm64.go
@@ -0,0 +1,7 @@
+// 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 syscall
+
+const SYS_FCNTL = 25
diff --git a/src/runtime/internal/syscall/syscall_linux.go b/src/runtime/internal/syscall/syscall_linux.go
new file mode 100644
index 0000000..06d5f21
--- /dev/null
+++ b/src/runtime/internal/syscall/syscall_linux.go
@@ -0,0 +1,12 @@
+// 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 provides the syscall primitives required for the runtime.
+package syscall
+
+// TODO(https://go.dev/issue/51087): This package is incomplete and currently
+// only contains very minimal support for Linux.
+
+// Syscall6 calls system call number 'num' with arguments a1-6.
+func Syscall6(num, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2, errno uintptr)
diff --git a/src/runtime/nbpipe_fcntl_libc_test.go b/src/runtime/nbpipe_fcntl_libc_test.go
deleted file mode 100644
index b38c583..0000000
--- a/src/runtime/nbpipe_fcntl_libc_test.go
+++ /dev/null
@@ -1,18 +0,0 @@
-// Copyright 2019 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.
-
-// +build aix darwin solaris
-
-package runtime_test
-
-import (
-	"runtime"
-	"syscall"
-)
-
-// Call fcntl libc function rather than calling syscall.
-func fcntl(fd uintptr, cmd int, arg uintptr) (uintptr, syscall.Errno) {
-	res, errno := runtime.Fcntl(fd, uintptr(cmd), arg)
-	return res, syscall.Errno(errno)
-}
diff --git a/src/runtime/nbpipe_fcntl_unix_test.go b/src/runtime/nbpipe_fcntl_unix_test.go
deleted file mode 100644
index 75acdb6..0000000
--- a/src/runtime/nbpipe_fcntl_unix_test.go
+++ /dev/null
@@ -1,17 +0,0 @@
-// Copyright 2019 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.
-
-// +build dragonfly freebsd linux netbsd openbsd
-
-package runtime_test
-
-import (
-	"internal/syscall/unix"
-	"syscall"
-)
-
-func fcntl(fd uintptr, cmd int, arg uintptr) (uintptr, syscall.Errno) {
-	res, _, err := syscall.Syscall(unix.FcntlSyscall, fd, uintptr(cmd), arg)
-	return res, err
-}
diff --git a/src/runtime/nbpipe_test.go b/src/runtime/nbpipe_test.go
index d739f57..9496f83 100644
--- a/src/runtime/nbpipe_test.go
+++ b/src/runtime/nbpipe_test.go
@@ -14,23 +14,29 @@ import (
 )
 
 func TestNonblockingPipe(t *testing.T) {
-	t.Parallel()
-
 	// NonblockingPipe is the test name for nonblockingPipe.
 	r, w, errno := runtime.NonblockingPipe()
 	if errno != 0 {
 		t.Fatal(syscall.Errno(errno))
 	}
-	defer func() {
-		runtime.Close(r)
-		runtime.Close(w)
-	}()
+	defer runtime.Close(w)
 
 	checkIsPipe(t, r, w)
 	checkNonblocking(t, r, "reader")
 	checkCloseonexec(t, r, "reader")
 	checkNonblocking(t, w, "writer")
 	checkCloseonexec(t, w, "writer")
+
+	// Test that fcntl returns an error as expected.
+	if runtime.Close(r) != 0 {
+		t.Fatalf("Close(%d) failed", r)
+	}
+	val, errno := runtime.Fcntl(r, syscall.F_GETFD, 0)
+	if val != -1 {
+		t.Errorf("Fcntl succeeded unexpectedly")
+	} else if syscall.Errno(errno) != syscall.EBADF {
+		t.Errorf("Fcntl failed with error %v, expected %v", syscall.Errno(errno), syscall.EBADF)
+	}
 }
 
 func checkIsPipe(t *testing.T, r, w int32) {
@@ -49,8 +55,8 @@ func checkIsPipe(t *testing.T, r, w int32) {
 
 func checkNonblocking(t *testing.T, fd int32, name string) {
 	t.Helper()
-	flags, errno := fcntl(uintptr(fd), syscall.F_GETFL, 0)
-	if errno != 0 {
+	flags, errno := runtime.Fcntl(fd, syscall.F_GETFL, 0)
+	if flags == -1 {
 		t.Errorf("fcntl(%s, F_GETFL) failed: %v", name, syscall.Errno(errno))
 	} else if flags&syscall.O_NONBLOCK == 0 {
 		t.Errorf("O_NONBLOCK not set in %s flags %#x", name, flags)
@@ -59,8 +65,8 @@ func checkNonblocking(t *testing.T, fd int32, name string) {
 
 func checkCloseonexec(t *testing.T, fd int32, name string) {
 	t.Helper()
-	flags, errno := fcntl(uintptr(fd), syscall.F_GETFD, 0)
-	if errno != 0 {
+	flags, errno := runtime.Fcntl(fd, syscall.F_GETFD, 0)
+	if flags == -1 {
 		t.Errorf("fcntl(%s, F_GETFD) failed: %v", name, syscall.Errno(errno))
 	} else if flags&syscall.FD_CLOEXEC == 0 {
 		t.Errorf("FD_CLOEXEC not set in %s flags %#x", name, flags)
diff --git a/src/runtime/os2_aix.go b/src/runtime/os2_aix.go
index 31ac6dd..8b609aa 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 9a6b8ae..7fb7aaf 100644
--- a/src/runtime/os_aix.go
+++ b/src/runtime/os_aix.go
@@ -375,3 +375,43 @@ func setNonblock(fd int32) {
 	flags := fcntl(fd, _F_GETFL, 0)
 	fcntl(fd, _F_SETFL, flags|_O_NONBLOCK)
 }
+
+//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 6578fcb..df0ff38 100644
--- a/src/runtime/os_dragonfly.go
+++ b/src/runtime/os_dragonfly.go
@@ -62,6 +62,8 @@ func pipe() (r, w int32, errno int32)
 
 const stackSystem = 0
 
+func issetugid() int32
+
 // From DragonFly's <sys/sysctl.h>
 const (
 	_CTL_HW      = 6
diff --git a/src/runtime/os_freebsd.go b/src/runtime/os_freebsd.go
index 730973a..35242c3 100644
--- a/src/runtime/os_freebsd.go
+++ b/src/runtime/os_freebsd.go
@@ -46,6 +46,8 @@ func pipe2(flags int32) (r, w int32, errno int32)
 func closeonexec(fd int32)
 func setNonblock(fd int32)
 
+func issetugid() int32
+
 // From FreeBSD's <sys/sysctl.h>
 const (
 	_CTL_HW      = 6
diff --git a/src/runtime/os_linux.go b/src/runtime/os_linux.go
index 7b95ff2..e83b6df 100644
--- a/src/runtime/os_linux.go
+++ b/src/runtime/os_linux.go
@@ -7,6 +7,7 @@ package runtime
 import (
 	"runtime/internal/atomic"
 	"runtime/internal/sys"
+	"runtime/internal/syscall"
 	"unsafe"
 )
 
@@ -31,9 +32,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
@@ -52,6 +56,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)
@@ -136,6 +141,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)
@@ -163,6 +169,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)
@@ -184,6 +191,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
 )
@@ -249,6 +257,9 @@ func sysargs(argc int32, argv **byte) {
 	sysauxv(buf[:])
 }
 
+// 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 {
@@ -261,6 +272,9 @@ func sysauxv(auxv []uintptr) int {
 
 		case _AT_PAGESZ:
 			physPageSize = val
+
+		case _AT_SECURE:
+			secureMode = val == 1
 		}
 
 		archauxv(tag, val)
@@ -322,6 +336,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() {
@@ -358,6 +373,7 @@ func minit() {
 }
 
 // Called from dropm to undo the effect of an minit.
+//
 //go:nosplit
 func unminit() {
 	unminitSignals()
@@ -397,6 +413,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)
+}
+
 //go:nosplit
 //go:nowritebarrierrec
 func setsig(i uint32, fn uintptr) {
@@ -441,6 +463,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
@@ -451,6 +474,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 {
@@ -475,6 +499,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
 
diff --git a/src/runtime/os_netbsd.go b/src/runtime/os_netbsd.go
index 97106c7..748e324 100644
--- a/src/runtime/os_netbsd.go
+++ b/src/runtime/os_netbsd.go
@@ -77,6 +77,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_solaris.go b/src/runtime/os_solaris.go
index 89129e5..a9c081d 100644
--- a/src/runtime/os_solaris.go
+++ b/src/runtime/os_solaris.go
@@ -264,3 +264,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 615249f..e6d787d 100644
--- a/src/runtime/panic.go
+++ b/src/runtime/panic.go
@@ -1166,6 +1166,9 @@ 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 e1aafff..c0b961f 100644
--- a/src/runtime/proc.go
+++ b/src/runtime/proc.go
@@ -634,6 +634,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 0000000..c11b9c3
--- /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 0000000..ee1aa67
--- /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.
+
+// +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 0000000..181f3a1
--- /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 0000000..19d16ea
--- /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.
+
+// +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 0000000..5653e4f
--- /dev/null
+++ b/src/runtime/security_test.go
@@ -0,0 +1,145 @@
+// 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
+// +build aix android darwin dragonfly freebsd hurd illumos ios linux netbsd openbsd solaris
+
+package runtime_test
+
+import (
+	"bytes"
+	"context"
+	"fmt"
+	"internal/testenv"
+	"io"
+	"io/ioutil"
+	"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.Create(filepath.Join(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 := ioutil.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 0000000..724471c
--- /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.
+
+// +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 003c7b0..c13ede9 100644
--- a/src/runtime/signal_unix.go
+++ b/src/runtime/signal_unix.go
@@ -633,6 +633,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.lockedg != 0 && _g_.m.ncgo > 0 && gp == _g_.m.g0 {
 		print("signal arrived during cgo execution\n")
diff --git a/src/runtime/sys_darwin.go b/src/runtime/sys_darwin.go
index 0647443..6c3527d 100644
--- a/src/runtime/sys_darwin.go
+++ b/src/runtime/sys_darwin.go
@@ -10,6 +10,7 @@ import "unsafe"
 // fn is the raw pc value of the entry point of the desired function.
 // Switches to the system stack, if not already there.
 // Preserves the calling point as the location where a profiler traceback will begin.
+//
 //go:nosplit
 func libcCall(fn, arg unsafe.Pointer) int32 {
 	// Leave caller's PC/SP/G around for traceback.
@@ -247,10 +248,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(funcPC(exit_trampoline)), unsafe.Pointer(&code))
@@ -360,8 +361,13 @@ func sysctl_trampoline()
 
 //go:nosplit
 //go:cgo_unsafe_args
-func fcntl(fd, cmd, arg int32) int32 {
-	return libcCall(unsafe.Pointer(funcPC(fcntl_trampoline)), unsafe.Pointer(&fd))
+func fcntl(fd, cmd, arg int32) (ret int32, errno int32) {
+	args := struct {
+		fd, cmd, arg int32
+		ret, errno   int32
+	}{fd, cmd, arg, 0, 0}
+	libcCall(unsafe.Pointer(funcPC(fcntl_trampoline)), unsafe.Pointer(&args))
+	return args.ret, args.errno
 }
 func fcntl_trampoline()
 
@@ -440,10 +446,17 @@ func closeonexec(fd int32) {
 
 //go:nosplit
 func setNonblock(fd int32) {
-	flags := fcntl(fd, _F_GETFL, 0)
-	fcntl(fd, _F_SETFL, flags|_O_NONBLOCK)
+	flags, _ := fcntl(fd, _F_GETFL, 0)
+	if flags != -1 {
+		fcntl(fd, _F_SETFL, flags|_O_NONBLOCK)
+	}
 }
 
+func issetugid() int32 {
+	return libcCall(unsafe.Pointer(funcPC(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.
 
@@ -495,3 +508,5 @@ func setNonblock(fd int32) {
 //go:cgo_import_dynamic _ _ "/usr/lib/libSystem.B.dylib"
 //go:cgo_import_dynamic _ _ "/System/Library/Frameworks/Security.framework/Versions/A/Security"
 //go:cgo_import_dynamic _ _ "/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation"
+
+//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 825852d..85d0e40 100644
--- a/src/runtime/sys_darwin_amd64.s
+++ b/src/runtime/sys_darwin_amd64.s
@@ -851,3 +851,10 @@ TEXT runtime·syscallNoErr(SB),NOSPLIT,$0
 	MOVQ	BP, SP
 	POPQ	BP
 	RET
+
+TEXT runtime·issetugid_trampoline(SB),NOSPLIT,$0
+	PUSHQ	BP
+	MOVQ	SP, BP
+	CALL	libc_issetugid(SB)
+	POPQ	BP
+	RET
diff --git a/src/runtime/sys_darwin_arm64.s b/src/runtime/sys_darwin_arm64.s
index 585d4f2..fa3f9d8 100644
--- a/src/runtime/sys_darwin_arm64.s
+++ b/src/runtime/sys_darwin_arm64.s
@@ -693,3 +693,7 @@ TEXT runtime·syscall6X(SB),NOSPLIT,$0
 	MOVD	R0, 72(R2)	// save err
 ok:
 	RET
+
+TEXT runtime·issetugid_trampoline(SB),NOSPLIT,$0
+	BL	libc_issetugid(SB)
+	RET
\ No newline at end of file
diff --git a/src/runtime/sys_dragonfly_amd64.s b/src/runtime/sys_dragonfly_amd64.s
index 580633a..ea04d0e 100644
--- a/src/runtime/sys_dragonfly_amd64.s
+++ b/src/runtime/sys_dragonfly_amd64.s
@@ -405,3 +405,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 c346e71..f725d2b 100644
--- a/src/runtime/sys_freebsd_386.s
+++ b/src/runtime/sys_freebsd_386.s
@@ -464,3 +464,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 010b2ec..0eb0a03 100644
--- a/src/runtime/sys_freebsd_amd64.s
+++ b/src/runtime/sys_freebsd_amd64.s
@@ -502,3 +502,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 1e12f9c..c31b77c 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)
@@ -470,3 +471,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 2330f2f..af87220 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
@@ -536,3 +537,10 @@ TEXT runtime·getpfr0(SB),NOSPLIT,$0
 	MRS	ID_AA64PFR0_EL1, R0
 	MOVD	R0, ret+0(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 d0c470c..59f43cf 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
@@ -497,3 +498,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 dc9bd12..9e78014 100644
--- a/src/runtime/sys_netbsd_amd64.s
+++ b/src/runtime/sys_netbsd_amd64.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
@@ -466,3 +467,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 678dea5..91fb22f 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
@@ -439,3 +440,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 e70be0f..b263662 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
@@ -474,3 +475,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_openbsd_386.s b/src/runtime/sys_openbsd_386.s
index 24fbfd6..72637d1 100644
--- a/src/runtime/sys_openbsd_386.s
+++ b/src/runtime/sys_openbsd_386.s
@@ -459,3 +459,12 @@ TEXT runtime·setNonblock(SB),NOSPLIT,$16-4
 	RET
 
 GLOBL runtime·tlsoffset(SB),NOPTR,$4
+
+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 37d70ab..9cb49a6 100644
--- a/src/runtime/sys_openbsd_amd64.s
+++ b/src/runtime/sys_openbsd_amd64.s
@@ -414,3 +414,9 @@ TEXT runtime·setNonblock(SB),NOSPLIT,$0-4
 	MOVL	$92, AX // fcntl
 	SYSCALL
 	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 9e18ce0..5467cf8 100644
--- a/src/runtime/sys_openbsd_arm.s
+++ b/src/runtime/sys_openbsd_arm.s
@@ -433,3 +433,12 @@ TEXT runtime·read_tls_fallback(SB),NOSPLIT|NOFRAME,$0
 	INVOKE_SYSCALL
 	MOVM.IAW (R13), [R1, R2, R3, R12]
 	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 621b1b1..00b7864 100644
--- a/src/runtime/sys_openbsd_arm64.s
+++ b/src/runtime/sys_openbsd_arm64.s
@@ -446,3 +446,9 @@ TEXT runtime·setNonblock(SB),NOSPLIT|NOFRAME,$0-4
 	MOVD	$92, R8			// sys_fcntl
 	INVOKE_SYSCALL
 	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/syscall2_solaris.go b/src/runtime/syscall2_solaris.go
index e098e80..426481b 100644
--- a/src/runtime/syscall2_solaris.go
+++ b/src/runtime/syscall2_solaris.go
@@ -23,6 +23,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
@@ -41,3 +42,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 0945169..aff1504 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 0000000..1949d2d
--- /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")
+}
-- 
2.33.0

