]> Cypherpunks repositories - gostls13.git/commitdiff
syscall: avoid making assumptions about syscall permissions
authorBryan C. Mills <bcmills@google.com>
Thu, 8 Dec 2022 20:23:32 +0000 (15:23 -0500)
committerGopher Robot <gobot@golang.org>
Fri, 27 Jan 2023 17:37:09 +0000 (17:37 +0000)
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>

src/syscall/exec_linux_test.go

index a6900f9033829e1df62bebabf0fda82a8a4ab118..e0aa3fe996d3b352af62ca51076f33f99d9b00a8 100644 (file)
@@ -13,6 +13,7 @@ import (
        "fmt"
        "internal/testenv"
        "io"
+       "io/fs"
        "os"
        "os/exec"
        "os/user"
@@ -26,102 +27,45 @@ import (
        "unsafe"
 )
 
-func isDocker() bool {
-       _, err := os.Stat("/.dockerenv")
-       return err == nil
-}
-
-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")
+// 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
+       }
+
+       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
                }
-               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")
+       if errors.Is(err, fs.ErrPermission) {
+               return true
        }
-}
 
-// 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
-}
+       // TODO(#41198): Also return true if errors.Is(err, errors.ErrUnsupported).
 
-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")
-               }
-       }
-
-       // 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")
-               }
-       }
+       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 @@ func whoamiCmd(t *testing.T, uid, gid int, setgroups bool) *exec.Cmd {
        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 TestCloneNEWUSERAndRemapRootDisableSetgroups(t *testing.T) {
-       if os.Getuid() != 0 {
-               t.Skip("skipping root only test")
-       }
-       testNEWUSERRemap(t, 0, 0, false)
-}
-
-func TestCloneNEWUSERAndRemapRootEnableSetgroups(t *testing.T) {
-       if os.Getuid() != 0 {
-               t.Skip("skipping root only test")
-       }
-       testNEWUSERRemap(t, 0, 0, true)
-}
-
-func TestCloneNEWUSERAndRemapNoRootDisableSetgroups(t *testing.T) {
-       if os.Getuid() == 0 {
-               t.Skip("skipping unprivileged user only test")
-       }
-       testNEWUSERRemap(t, os.Getuid(), os.Getgid(), false)
-}
-
-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")
+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()
+
+                       cmd := whoamiNEWUSER(t, uid, gid, setgroups)
+                       out, err := cmd.CombinedOutput()
+                       t.Logf("%v: %v", cmd, err)
+
+                       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
+                       }
+
+                       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)
+                       }
+
+                       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 @@ func TestUnshare(t *testing.T) {
                }
                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 @@ func TestUnshare(t *testing.T) {
        }
        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 TestUnshare(t *testing.T) {
 }
 
 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 @@ func TestGroupCleanup(t *testing.T) {
        }
        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 TestGroupCleanup(t *testing.T) {
 }
 
 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 @@ func TestGroupCleanupUserNamespace(t *testing.T) {
        }
        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 @@ func TestGroupCleanupUserNamespace(t *testing.T) {
        }
 }
 
-// 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 @@ func TestUnshareUidGidMapping(t *testing.T) {
        }
        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 @@ func prepareCgroupFD(t *testing.T) (int, string) {
                        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 @@ func prepareCgroupFD(t *testing.T) (int, string) {
        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 prepareCgroupFD(t *testing.T) (int, string) {
 }
 
 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 @@ func getCaps() (caps, error) {
        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 @@ func testAmbientCaps(t *testing.T, userns bool) {
        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 @@ func testAmbientCaps(t *testing.T, userns bool) {
                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 @@ func testAmbientCaps(t *testing.T, userns bool) {
                }
        }
        if err := cmd.Run(); err != nil {
+               if isNotSupported(err) {
+                       t.Skipf("skipping: %v: %v", cmd, err)
+               }
                t.Fatal(err.Error())
        }
 }