syscall: avoid making assumptions about syscall permissions

We currently check for at least three different permission bits before
running tests that require root permissions: we look for UID 0, lack
of an LXC container, and lack of a Docker container, and probe a
number of distro-specific files in /proc and /sys.

The sheer number of these checks suggests that we have probably missed
at least one. Per Alan J. Perlis, “If you have a procedure with ten
parameters, you probably missed some.” (And, indeed, we definitely
have: a Debian patch¹ adds one more environment check!)

CL 58170 added some of these container checks, but “decided to go this
way instead of just skipping os.IsPermission errors because many of
those tests were specifically written to check false positive
permission errors.” However, we can't in general distinguish between a
false-positive error and a real one caused by a container: if one is
making a change to the syscall package, they should run the tests with
-v and check for unexpected skips.

Notably:

- TestUnshare already skips itself if the command fails with an error
  ending in the string "operation not permitted", which could be caused
  by a variety of possible bugs.

- The Unshare tests added in CL 38471 will fail with a permission
  error if CLONE_NEWNS is not supported, but it seems to me that if
  CLONE_NEWNS is supported — sufficient to start the process! — then
  Unmount must also be supported, and the test can at least check that
  the two are consistent.

- The AmbientCaps tests should fail to start the subprocess with
  EINVAL or similar (not produce bogus output) if the kernel does not
  support ambient caps for any reason, which we can then detect.
  (If the subprocess fails in the way the test is concerned about, it
  will exit with status 2, not fail to start in the first place.)

By executing the system calls and checking for permission errors,
this change exposed an existing bug for AmbientCaps (filed as #57208),
which was detected by the linux-arm-aws builder.

For #57208.
Updates #21379.
Updates #14693.

¹https://sources.debian.org/patches/golang-1.19/1.19.3-1/0006-skip-userns-test-in-schroot-as-well.patch/

Change-Id: I9b167661fa1bb823168c8b50d8bbbf9643e49f76
Reviewed-on: https://go-review.googlesource.com/c/go/+/456375
Reviewed-by: Ian Lance Taylor <iant@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Russ Cox <rsc@golang.org>
Auto-Submit: Bryan Mills <bcmills@google.com>
Reviewed-by: Alexander Morozov <lk4d4math@gmail.com>
Run-TryBot: Bryan Mills <bcmills@google.com>
diff --git a/src/syscall/exec_linux_test.go b/src/syscall/exec_linux_test.go
index a6900f9..e0aa3fe 100644
--- a/src/syscall/exec_linux_test.go
+++ b/src/syscall/exec_linux_test.go
@@ -13,6 +13,7 @@
 	"fmt"
 	"internal/testenv"
 	"io"
+	"io/fs"
 	"os"
 	"os/exec"
 	"os/user"
@@ -26,102 +27,45 @@
 	"unsafe"
 )
 
-func isDocker() bool {
-	_, err := os.Stat("/.dockerenv")
-	return err == nil
-}
+// isNotSupported reports whether err may indicate that a system call is
+// not supported by the current platform or execution environment.
+func isNotSupported(err error) bool {
+	if err == nil {
+		return false
+	}
 
-func isLXC() bool {
-	return os.Getenv("container") == "lxc"
-}
-
-func skipInContainer(t *testing.T) {
-	// TODO: the callers of this func are using this func to skip
-	// tests when running as some sort of "fake root" that's uid 0
-	// but lacks certain Linux capabilities. Most of the Go builds
-	// run in privileged containers, though, where root is much
-	// closer (if not identical) to the real root. We should test
-	// for what we need exactly (which capabilities are active?),
-	// instead of just assuming "docker == bad". Then we'd get more test
-	// coverage on a bunch of builders too.
-	if isDocker() {
-		t.Skip("skip this test in Docker container")
-	}
-	if isLXC() {
-		t.Skip("skip this test in LXC container")
-	}
-}
-
-func skipNoUserNamespaces(t *testing.T) {
-	if _, err := os.Stat("/proc/self/ns/user"); err != nil {
-		if os.IsNotExist(err) {
-			t.Skip("kernel doesn't support user namespaces")
-		}
-		if os.IsPermission(err) {
-			t.Skip("unable to test user namespaces due to permissions")
-		}
-		t.Fatalf("Failed to stat /proc/self/ns/user: %v", err)
-	}
-}
-
-func skipUnprivilegedUserClone(t *testing.T) {
-	// Skip the test if the sysctl that prevents unprivileged user
-	// from creating user namespaces is enabled.
-	data, errRead := os.ReadFile("/proc/sys/kernel/unprivileged_userns_clone")
-	if os.IsNotExist(errRead) {
-		// This file is only available in some Debian/Ubuntu kernels.
-		return
-	}
-	if errRead != nil || len(data) < 1 || data[0] == '0' {
-		t.Skip("kernel prohibits user namespace in unprivileged process")
-	}
-}
-
-// Check if we are in a chroot by checking if the inode of / is
-// different from 2 (there is no better test available to non-root on
-// linux).
-func isChrooted(t *testing.T) bool {
-	root, err := os.Stat("/")
-	if err != nil {
-		t.Fatalf("cannot stat /: %v", err)
-	}
-	return root.Sys().(*syscall.Stat_t).Ino != 2
-}
-
-func checkUserNS(t *testing.T) {
-	skipInContainer(t)
-	skipNoUserNamespaces(t)
-	if isChrooted(t) {
-		// create_user_ns in the kernel (see
-		// https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/tree/kernel/user_namespace.c)
-		// forbids the creation of user namespaces when chrooted.
-		t.Skip("cannot create user namespaces when chrooted")
-	}
-	// On some systems, there is a sysctl setting.
-	if os.Getuid() != 0 {
-		skipUnprivilegedUserClone(t)
-	}
-	// On Centos 7 make sure they set the kernel parameter user_namespace=1
-	// See issue 16283 and 20796.
-	if _, err := os.Stat("/sys/module/user_namespace/parameters/enable"); err == nil {
-		buf, _ := os.ReadFile("/sys/module/user_namespace/parameters/enabled")
-		if !strings.HasPrefix(string(buf), "Y") {
-			t.Skip("kernel doesn't support user namespaces")
+	var errno syscall.Errno
+	if errors.As(err, &errno) {
+		switch errno {
+		case syscall.ENOSYS, syscall.ENOTSUP:
+			// Explicitly not supported.
+			return true
+		case syscall.EPERM, syscall.EROFS:
+			// User lacks permission: either the call requires root permission and the
+			// user is not root, or the call is denied by a container security policy.
+			return true
+		case syscall.EINVAL:
+			// Some containers return EINVAL instead of EPERM if a system call is
+			// denied by security policy.
+			return true
 		}
 	}
 
-	// On Centos 7.5+, user namespaces are disabled if user.max_user_namespaces = 0
-	if _, err := os.Stat("/proc/sys/user/max_user_namespaces"); err == nil {
-		buf, errRead := os.ReadFile("/proc/sys/user/max_user_namespaces")
-		if errRead == nil && buf[0] == '0' {
-			t.Skip("kernel doesn't support user namespaces")
-		}
+	if errors.Is(err, fs.ErrPermission) {
+		return true
 	}
+
+	// TODO(#41198): Also return true if errors.Is(err, errors.ErrUnsupported).
+
+	return false
 }
 
-func whoamiCmd(t *testing.T, uid, gid int, setgroups bool) *exec.Cmd {
-	checkUserNS(t)
-	cmd := exec.Command("whoami")
+// whoamiNEWUSER returns a command that runs "whoami" with CLONE_NEWUSER,
+// mapping uid and gid 0 to the actual uid and gid of the test.
+func whoamiNEWUSER(t *testing.T, uid, gid int, setgroups bool) *exec.Cmd {
+	t.Helper()
+	testenv.MustHaveExecPath(t, "whoami")
+	cmd := testenv.Command(t, "whoami")
 	cmd.SysProcAttr = &syscall.SysProcAttr{
 		Cloneflags: syscall.CLONE_NEWUSER,
 		UidMappings: []syscall.SysProcIDMap{
@@ -135,70 +79,60 @@
 	return cmd
 }
 
-func testNEWUSERRemap(t *testing.T, uid, gid int, setgroups bool) {
-	cmd := whoamiCmd(t, uid, gid, setgroups)
-	out, err := cmd.CombinedOutput()
-	if err != nil {
-		t.Fatalf("Cmd failed with err %v, output: %s", err, out)
-	}
-	sout := strings.TrimSpace(string(out))
-	want := "root"
-	if sout != want {
-		t.Fatalf("whoami = %q; want %q", out, want)
-	}
-}
+func TestCloneNEWUSERAndRemap(t *testing.T) {
+	for _, setgroups := range []bool{false, true} {
+		setgroups := setgroups
+		t.Run(fmt.Sprintf("setgroups=%v", setgroups), func(t *testing.T) {
+			uid := os.Getuid()
+			gid := os.Getgid()
 
-func TestCloneNEWUSERAndRemapRootDisableSetgroups(t *testing.T) {
-	if os.Getuid() != 0 {
-		t.Skip("skipping root only test")
-	}
-	testNEWUSERRemap(t, 0, 0, false)
-}
+			cmd := whoamiNEWUSER(t, uid, gid, setgroups)
+			out, err := cmd.CombinedOutput()
+			t.Logf("%v: %v", cmd, err)
 
-func TestCloneNEWUSERAndRemapRootEnableSetgroups(t *testing.T) {
-	if os.Getuid() != 0 {
-		t.Skip("skipping root only test")
-	}
-	testNEWUSERRemap(t, 0, 0, true)
-}
+			if uid != 0 && setgroups {
+				t.Logf("as non-root, expected permission error due to unprivileged gid_map")
+				if !os.IsPermission(err) {
+					if err == nil {
+						t.Skipf("unexpected success: probably old kernel without security fix?")
+					}
+					if isNotSupported(err) {
+						t.Skipf("skipping: CLONE_NEWUSER appears to be unsupported")
+					}
+					t.Fatalf("got non-permission error") // Already logged above.
+				}
+				return
+			}
 
-func TestCloneNEWUSERAndRemapNoRootDisableSetgroups(t *testing.T) {
-	if os.Getuid() == 0 {
-		t.Skip("skipping unprivileged user only test")
-	}
-	testNEWUSERRemap(t, os.Getuid(), os.Getgid(), false)
-}
+			if err != nil {
+				if isNotSupported(err) {
+					// May be inside a container that disallows CLONE_NEWUSER.
+					t.Skipf("skipping: CLONE_NEWUSER appears to be unsupported")
+				}
+				t.Fatalf("unexpected command failure; output:\n%s", out)
+			}
 
-func TestCloneNEWUSERAndRemapNoRootSetgroupsEnableSetgroups(t *testing.T) {
-	if os.Getuid() == 0 {
-		t.Skip("skipping unprivileged user only test")
-	}
-	cmd := whoamiCmd(t, os.Getuid(), os.Getgid(), true)
-	err := cmd.Run()
-	if err == nil {
-		t.Skip("probably old kernel without security fix")
-	}
-	if !os.IsPermission(err) {
-		t.Fatalf("Unprivileged gid_map rewriting with GidMappingsEnableSetgroups must fail")
+			sout := strings.TrimSpace(string(out))
+			want := "root"
+			if sout != want {
+				t.Fatalf("whoami = %q; want %q", out, want)
+			}
+		})
 	}
 }
 
 func TestEmptyCredGroupsDisableSetgroups(t *testing.T) {
-	cmd := whoamiCmd(t, os.Getuid(), os.Getgid(), false)
+	cmd := whoamiNEWUSER(t, os.Getuid(), os.Getgid(), false)
 	cmd.SysProcAttr.Credential = &syscall.Credential{}
 	if err := cmd.Run(); err != nil {
+		if isNotSupported(err) {
+			t.Skipf("skipping: %v: %v", cmd, err)
+		}
 		t.Fatal(err)
 	}
 }
 
 func TestUnshare(t *testing.T) {
-	skipInContainer(t)
-	// Make sure we are running as root so we have permissions to use unshare
-	// and create a network namespace.
-	if os.Getuid() != 0 {
-		t.Skip("kernel prohibits unshare in unprivileged process, unless using user namespace")
-	}
-
 	path := "/proc/net/dev"
 	if _, err := os.Stat(path); err != nil {
 		if os.IsNotExist(err) {
@@ -209,12 +143,6 @@
 		}
 		t.Fatal(err)
 	}
-	if _, err := os.Stat("/proc/self/ns/net"); err != nil {
-		if os.IsNotExist(err) {
-			t.Skip("kernel doesn't support net namespace")
-		}
-		t.Fatal(err)
-	}
 
 	orig, err := os.ReadFile(path)
 	if err != nil {
@@ -222,17 +150,15 @@
 	}
 	origLines := strings.Split(strings.TrimSpace(string(orig)), "\n")
 
-	cmd := exec.Command("cat", path)
+	cmd := testenv.Command(t, "cat", path)
 	cmd.SysProcAttr = &syscall.SysProcAttr{
 		Unshareflags: syscall.CLONE_NEWNET,
 	}
 	out, err := cmd.CombinedOutput()
 	if err != nil {
-		if strings.Contains(err.Error(), "operation not permitted") {
-			// Issue 17206: despite all the checks above,
-			// this still reportedly fails for some users.
-			// (older kernels?). Just skip.
-			t.Skip("skipping due to permission error")
+		if isNotSupported(err) {
+			// CLONE_NEWNET does not appear to be supported.
+			t.Skipf("skipping due to permission error: %v", err)
 		}
 		t.Fatalf("Cmd failed with err %v, output: %s", err, out)
 	}
@@ -250,10 +176,8 @@
 }
 
 func TestGroupCleanup(t *testing.T) {
-	if os.Getuid() != 0 {
-		t.Skip("we need root for credential")
-	}
-	cmd := exec.Command("id")
+	testenv.MustHaveExecPath(t, "id")
+	cmd := testenv.Command(t, "id")
 	cmd.SysProcAttr = &syscall.SysProcAttr{
 		Credential: &syscall.Credential{
 			Uid: 0,
@@ -262,6 +186,9 @@
 	}
 	out, err := cmd.CombinedOutput()
 	if err != nil {
+		if isNotSupported(err) {
+			t.Skipf("skipping: %v: %v", cmd, err)
+		}
 		t.Fatalf("Cmd failed with err %v, output: %s", err, out)
 	}
 	strOut := strings.TrimSpace(string(out))
@@ -277,11 +204,8 @@
 }
 
 func TestGroupCleanupUserNamespace(t *testing.T) {
-	if os.Getuid() != 0 {
-		t.Skip("we need root for credential")
-	}
-	checkUserNS(t)
-	cmd := exec.Command("id")
+	testenv.MustHaveExecPath(t, "id")
+	cmd := testenv.Command(t, "id")
 	uid, gid := os.Getuid(), os.Getgid()
 	cmd.SysProcAttr = &syscall.SysProcAttr{
 		Cloneflags: syscall.CLONE_NEWUSER,
@@ -298,6 +222,9 @@
 	}
 	out, err := cmd.CombinedOutput()
 	if err != nil {
+		if isNotSupported(err) {
+			t.Skipf("skipping: %v: %v", cmd, err)
+		}
 		t.Fatalf("Cmd failed with err %v, output: %s", err, out)
 	}
 	strOut := strings.TrimSpace(string(out))
@@ -311,135 +238,131 @@
 	}
 }
 
-// TestUnshareHelperProcess isn't a real test. It's used as a helper process
-// for TestUnshareMountNameSpace.
-func TestUnshareMountNameSpaceHelper(*testing.T) {
-	if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
-		return
-	}
-	defer os.Exit(0)
-	if err := syscall.Mount("none", flag.Args()[0], "proc", 0, ""); err != nil {
-		fmt.Fprintf(os.Stderr, "unshare: mount %v failed: %v", os.Args, err)
-		os.Exit(2)
-	}
-}
-
-// Test for Issue 38471: unshare fails because systemd has forced / to be shared
+// Test for https://go.dev/issue/19661: unshare fails because systemd
+// has forced / to be shared
 func TestUnshareMountNameSpace(t *testing.T) {
-	skipInContainer(t)
-	// Make sure we are running as root so we have permissions to use unshare
-	// and create a network namespace.
-	if os.Getuid() != 0 {
-		t.Skip("kernel prohibits unshare in unprivileged process, unless using user namespace")
+	testenv.MustHaveExec(t)
+
+	if os.Getenv("GO_WANT_HELPER_PROCESS") == "1" {
+		dir := flag.Args()[0]
+		err := syscall.Mount("none", dir, "proc", 0, "")
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "unshare: mount %v failed: %#v", dir, err)
+			os.Exit(2)
+		}
+		os.Exit(0)
 	}
 
-	d, err := os.MkdirTemp("", "unshare")
+	exe, err := os.Executable()
 	if err != nil {
-		t.Fatalf("tempdir: %v", err)
+		t.Fatal(err)
 	}
 
-	cmd := exec.Command(os.Args[0], "-test.run=TestUnshareMountNameSpaceHelper", d)
-	cmd.Env = append(os.Environ(), "GO_WANT_HELPER_PROCESS=1")
+	d := t.TempDir()
+	t.Cleanup(func() {
+		// If the subprocess fails to unshare the parent directory, force-unmount it
+		// so that the test can clean it up.
+		if _, err := os.Stat(d); err == nil {
+			syscall.Unmount(d, syscall.MNT_FORCE)
+		}
+	})
+	cmd := testenv.Command(t, exe, "-test.run=TestUnshareMountNameSpace", d)
+	cmd.Env = append(cmd.Environ(), "GO_WANT_HELPER_PROCESS=1")
 	cmd.SysProcAttr = &syscall.SysProcAttr{Unshareflags: syscall.CLONE_NEWNS}
 
 	o, err := cmd.CombinedOutput()
 	if err != nil {
-		if strings.Contains(err.Error(), ": permission denied") {
-			t.Skipf("Skipping test (golang.org/issue/19698); unshare failed due to permissions: %s, %v", o, err)
+		if isNotSupported(err) {
+			t.Skipf("skipping: could not start process with CLONE_NEWNS: %v", err)
 		}
-		t.Fatalf("unshare failed: %s, %v", o, err)
+		t.Fatalf("unshare failed: %v\n%s", err, o)
 	}
 
 	// How do we tell if the namespace was really unshared? It turns out
 	// to be simple: just try to remove the directory. If it's still mounted
-	// on the rm will fail with EBUSY. Then we have some cleanup to do:
-	// we must unmount it, then try to remove it again.
-
+	// on the rm will fail with EBUSY.
 	if err := os.Remove(d); err != nil {
 		t.Errorf("rmdir failed on %v: %v", d, err)
-		if err := syscall.Unmount(d, syscall.MNT_FORCE); err != nil {
-			t.Errorf("Can't unmount %v: %v", d, err)
-		}
-		if err := os.Remove(d); err != nil {
-			t.Errorf("rmdir after unmount failed on %v: %v", d, err)
-		}
 	}
 }
 
 // Test for Issue 20103: unshare fails when chroot is used
 func TestUnshareMountNameSpaceChroot(t *testing.T) {
-	skipInContainer(t)
-	// Make sure we are running as root so we have permissions to use unshare
-	// and create a network namespace.
-	if os.Getuid() != 0 {
-		t.Skip("kernel prohibits unshare in unprivileged process, unless using user namespace")
+	if os.Getenv("GO_WANT_HELPER_PROCESS") == "1" {
+		dir := flag.Args()[0]
+		err := syscall.Mount("none", dir, "proc", 0, "")
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "unshare: mount %v failed: %#v", dir, err)
+			os.Exit(2)
+		}
+		os.Exit(0)
 	}
 
-	d, err := os.MkdirTemp("", "unshare")
-	if err != nil {
-		t.Fatalf("tempdir: %v", err)
-	}
+	d := t.TempDir()
 
 	// Since we are doing a chroot, we need the binary there,
 	// and it must be statically linked.
+	testenv.MustHaveGoBuild(t)
 	x := filepath.Join(d, "syscall.test")
-	cmd := exec.Command(testenv.GoToolPath(t), "test", "-c", "-o", x, "syscall")
-	cmd.Env = append(os.Environ(), "CGO_ENABLED=0")
+	t.Cleanup(func() {
+		// If the subprocess fails to unshare the parent directory, force-unmount it
+		// so that the test can clean it up.
+		if _, err := os.Stat(d); err == nil {
+			syscall.Unmount(d, syscall.MNT_FORCE)
+		}
+	})
+
+	cmd := testenv.Command(t, testenv.GoToolPath(t), "test", "-c", "-o", x, "syscall")
+	cmd.Env = append(cmd.Environ(), "CGO_ENABLED=0")
 	if o, err := cmd.CombinedOutput(); err != nil {
 		t.Fatalf("Build of syscall in chroot failed, output %v, err %v", o, err)
 	}
 
-	cmd = exec.Command("/syscall.test", "-test.run=TestUnshareMountNameSpaceHelper", "/")
-	cmd.Env = append(os.Environ(), "GO_WANT_HELPER_PROCESS=1")
+	cmd = testenv.Command(t, "/syscall.test", "-test.run=TestUnshareMountNameSpaceChroot", "/")
+	cmd.Env = append(cmd.Environ(), "GO_WANT_HELPER_PROCESS=1")
 	cmd.SysProcAttr = &syscall.SysProcAttr{Chroot: d, Unshareflags: syscall.CLONE_NEWNS}
 
 	o, err := cmd.CombinedOutput()
 	if err != nil {
-		if strings.Contains(err.Error(), ": permission denied") {
-			t.Skipf("Skipping test (golang.org/issue/19698); unshare failed due to permissions: %s, %v", o, err)
+		if isNotSupported(err) {
+			t.Skipf("skipping: could not start process with CLONE_NEWNS and Chroot %q: %v", d, err)
 		}
-		t.Fatalf("unshare failed: %s, %v", o, err)
+		t.Fatalf("unshare failed: %v\n%s", err, o)
 	}
 
 	// How do we tell if the namespace was really unshared? It turns out
 	// to be simple: just try to remove the executable. If it's still mounted
-	// on, the rm will fail. Then we have some cleanup to do:
-	// we must force unmount it, then try to remove it again.
-
+	// on, the rm will fail.
 	if err := os.Remove(x); err != nil {
 		t.Errorf("rm failed on %v: %v", x, err)
-		if err := syscall.Unmount(d, syscall.MNT_FORCE); err != nil {
-			t.Fatalf("Can't unmount %v: %v", d, err)
-		}
-		if err := os.Remove(x); err != nil {
-			t.Fatalf("rm failed on %v: %v", x, err)
-		}
 	}
-
 	if err := os.Remove(d); err != nil {
 		t.Errorf("rmdir failed on %v: %v", d, err)
 	}
 }
 
-func TestUnshareUidGidMappingHelper(*testing.T) {
-	if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
-		return
-	}
-	defer os.Exit(0)
-	if err := syscall.Chroot(os.TempDir()); err != nil {
-		fmt.Fprintln(os.Stderr, err)
-		os.Exit(2)
-	}
-}
-
 // Test for Issue 29789: unshare fails when uid/gid mapping is specified
 func TestUnshareUidGidMapping(t *testing.T) {
+	if os.Getenv("GO_WANT_HELPER_PROCESS") == "1" {
+		defer os.Exit(0)
+		if err := syscall.Chroot(os.TempDir()); err != nil {
+			fmt.Fprintln(os.Stderr, err)
+			os.Exit(2)
+		}
+	}
+
 	if os.Getuid() == 0 {
 		t.Skip("test exercises unprivileged user namespace, fails with privileges")
 	}
-	checkUserNS(t)
-	cmd := exec.Command(os.Args[0], "-test.run=TestUnshareUidGidMappingHelper")
-	cmd.Env = append(os.Environ(), "GO_WANT_HELPER_PROCESS=1")
+
+	testenv.MustHaveExec(t)
+	exe, err := os.Executable()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	cmd := testenv.Command(t, exe, "-test.run=TestUnshareUidGidMapping")
+	cmd.Env = append(cmd.Environ(), "GO_WANT_HELPER_PROCESS=1")
 	cmd.SysProcAttr = &syscall.SysProcAttr{
 		Unshareflags:               syscall.CLONE_NEWNS | syscall.CLONE_NEWUSER,
 		GidMappingsEnableSetgroups: false,
@@ -460,6 +383,9 @@
 	}
 	out, err := cmd.CombinedOutput()
 	if err != nil {
+		if isNotSupported(err) {
+			t.Skipf("skipping: could not start process with CLONE_NEWNS and CLONE_NEWUSER: %v", err)
+		}
 		t.Fatalf("Cmd failed with err %v, output: %s", err, out)
 	}
 }
@@ -497,8 +423,7 @@
 			CgroupFD:    -1,
 		},
 	})
-	// // EPERM can be returned if clone3 is not enabled by seccomp.
-	if err == syscall.ENOSYS || err == syscall.EPERM {
+	if isNotSupported(err) {
 		t.Skipf("clone3 with CLONE_INTO_CGROUP not available: %v", err)
 	}
 
@@ -507,8 +432,8 @@
 	if err != nil {
 		// ErrPermission or EROFS (#57262) when running in an unprivileged container.
 		// ErrNotExist when cgroupfs is not mounted in chroot/schroot.
-		if os.IsNotExist(err) || os.IsPermission(err) || errors.Is(err, syscall.EROFS) {
-			t.Skip(err)
+		if os.IsNotExist(err) || isNotSupported(err) {
+			t.Skipf("skipping: %v", err)
 		}
 		t.Fatal(err)
 	}
@@ -524,10 +449,16 @@
 }
 
 func TestUseCgroupFD(t *testing.T) {
+	testenv.MustHaveExec(t)
+	exe, err := os.Executable()
+	if err != nil {
+		t.Fatal(err)
+	}
+
 	fd, suffix := prepareCgroupFD(t)
 
-	cmd := exec.Command(os.Args[0], "-test.run=TestUseCgroupFDHelper")
-	cmd.Env = append(os.Environ(), "GO_WANT_HELPER_PROCESS=1")
+	cmd := testenv.Command(t, exe, "-test.run=TestUseCgroupFDHelper")
+	cmd.Env = append(cmd.Environ(), "GO_WANT_HELPER_PROCESS=1")
 	cmd.SysProcAttr = &syscall.SysProcAttr{
 		UseCgroupFD: true,
 		CgroupFD:    fd,
@@ -591,68 +522,31 @@
 	return c, nil
 }
 
-func mustSupportAmbientCaps(t *testing.T) {
-	var uname syscall.Utsname
-	if err := syscall.Uname(&uname); err != nil {
-		t.Fatalf("Uname: %v", err)
-	}
-	var buf [65]byte
-	for i, b := range uname.Release {
-		buf[i] = byte(b)
-	}
-	ver := string(buf[:])
-	ver, _, _ = strings.Cut(ver, "\x00")
-	if strings.HasPrefix(ver, "2.") ||
-		strings.HasPrefix(ver, "3.") ||
-		strings.HasPrefix(ver, "4.1.") ||
-		strings.HasPrefix(ver, "4.2.") {
-		t.Skipf("kernel version %q predates required 4.3; skipping test", ver)
-	}
-}
-
-// TestAmbientCapsHelper isn't a real test. It's used as a helper process for
-// TestAmbientCaps.
-func TestAmbientCapsHelper(*testing.T) {
-	if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
-		return
-	}
-	defer os.Exit(0)
-
-	caps, err := getCaps()
-	if err != nil {
-		fmt.Fprintln(os.Stderr, err)
-		os.Exit(2)
-	}
-	if caps.data[0].effective&(1<<uint(CAP_SYS_TIME)) == 0 {
-		fmt.Fprintln(os.Stderr, "CAP_SYS_TIME unexpectedly not in the effective capability mask")
-		os.Exit(2)
-	}
-	if caps.data[1].effective&(1<<uint(CAP_SYSLOG&31)) == 0 {
-		fmt.Fprintln(os.Stderr, "CAP_SYSLOG unexpectedly not in the effective capability mask")
-		os.Exit(2)
-	}
-}
-
 func TestAmbientCaps(t *testing.T) {
-	// Make sure we are running as root so we have permissions to use unshare
-	// and create a network namespace.
-	if os.Getuid() != 0 {
-		t.Skip("kernel prohibits unshare in unprivileged process, unless using user namespace")
-	}
-
 	testAmbientCaps(t, false)
 }
 
 func TestAmbientCapsUserns(t *testing.T) {
-	checkUserNS(t)
 	testAmbientCaps(t, true)
 }
 
 func testAmbientCaps(t *testing.T, userns bool) {
-	skipInContainer(t)
-	mustSupportAmbientCaps(t)
-
-	skipUnprivilegedUserClone(t)
+	if os.Getenv("GO_WANT_HELPER_PROCESS") == "1" {
+		caps, err := getCaps()
+		if err != nil {
+			fmt.Fprintln(os.Stderr, err)
+			os.Exit(2)
+		}
+		if caps.data[0].effective&(1<<uint(CAP_SYS_TIME)) == 0 {
+			fmt.Fprintln(os.Stderr, "CAP_SYS_TIME unexpectedly not in the effective capability mask")
+			os.Exit(2)
+		}
+		if caps.data[1].effective&(1<<uint(CAP_SYSLOG&31)) == 0 {
+			fmt.Fprintln(os.Stderr, "CAP_SYSLOG unexpectedly not in the effective capability mask")
+			os.Exit(2)
+		}
+		os.Exit(0)
+	}
 
 	// skip on android, due to lack of lookup support
 	if runtime.GOOS == "android" {
@@ -677,9 +571,18 @@
 	if err != nil {
 		t.Fatal(err)
 	}
-	defer os.Remove(f.Name())
-	defer f.Close()
-	e, err := os.Open(os.Args[0])
+	t.Cleanup(func() {
+		f.Close()
+		os.Remove(f.Name())
+	})
+
+	testenv.MustHaveExec(t)
+	exe, err := os.Executable()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	e, err := os.Open(exe)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -694,8 +597,8 @@
 		t.Fatal(err)
 	}
 
-	cmd := exec.Command(f.Name(), "-test.run=TestAmbientCapsHelper")
-	cmd.Env = append(os.Environ(), "GO_WANT_HELPER_PROCESS=1")
+	cmd := testenv.Command(t, f.Name(), "-test.run="+t.Name())
+	cmd.Env = append(cmd.Environ(), "GO_WANT_HELPER_PROCESS=1")
 	cmd.Stdout = os.Stdout
 	cmd.Stderr = os.Stderr
 	cmd.SysProcAttr = &syscall.SysProcAttr{
@@ -728,6 +631,9 @@
 		}
 	}
 	if err := cmd.Run(); err != nil {
+		if isNotSupported(err) {
+			t.Skipf("skipping: %v: %v", cmd, err)
+		}
 		t.Fatal(err.Error())
 	}
 }