go/src/syscall/exec_linux_test.go
Dmitri Shuralyov 0dfb22ed70 all: use ^TestName$ regular pattern for invoking a single test
Use ^ and $ in the -run flag regular expression value when the intention
is to invoke a single named test. This removes the reliance on there not
being another similarly named test to achieve the intended result.

In particular, package syscall has tests named TestUnshareMountNameSpace
and TestUnshareMountNameSpaceChroot that both trigger themselves setting
GO_WANT_HELPER_PROCESS=1 to run alternate code in a helper process. As a
consequence of overlap in their test names, the former was inadvertently
triggering one too many helpers.

Spotted while reviewing CL 525196. Apply the same change in other places
to make it easier for code readers to see that said tests aren't running
extraneous tests. The unlikely cases of -run=TestSomething intentionally
being used to run all tests that have the TestSomething substring in the
name can be better written as -run=^.*TestSomething.*$ or with a comment
so it is clear it wasn't an oversight.

Change-Id: Iba208aba3998acdbf8c6708e5d23ab88938bfc1e
Reviewed-on: https://go-review.googlesource.com/c/go/+/524948
Reviewed-by: Tobias Klauser <tobias.klauser@gmail.com>
Auto-Submit: Dmitri Shuralyov <dmitshur@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Reviewed-by: Ian Lance Taylor <iant@google.com>
Reviewed-by: Kirill Kolyshkin <kolyshkin@gmail.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
2023-09-05 23:35:29 +00:00

674 lines
19 KiB
Go

// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build linux
package syscall_test
import (
"bytes"
"errors"
"flag"
"fmt"
"internal/platform"
"internal/testenv"
"io"
"os"
"os/exec"
"os/user"
"path"
"path/filepath"
"runtime"
"strconv"
"strings"
"syscall"
"testing"
"unsafe"
)
// 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{
{ContainerID: 0, HostID: uid, Size: 1},
},
GidMappings: []syscall.SysProcIDMap{
{ContainerID: 0, HostID: gid, Size: 1},
},
GidMappingsEnableSetgroups: setgroups,
}
return cmd
}
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 testenv.SyscallIsNotSupported(err) {
t.Skipf("skipping: CLONE_NEWUSER appears to be unsupported")
}
t.Fatalf("got non-permission error") // Already logged above.
}
return
}
if err != nil {
if testenv.SyscallIsNotSupported(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 := whoamiNEWUSER(t, os.Getuid(), os.Getgid(), false)
cmd.SysProcAttr.Credential = &syscall.Credential{}
if err := cmd.Run(); err != nil {
if testenv.SyscallIsNotSupported(err) {
t.Skipf("skipping: %v: %v", cmd, err)
}
t.Fatal(err)
}
}
func TestUnshare(t *testing.T) {
path := "/proc/net/dev"
if _, err := os.Stat(path); err != nil {
if os.IsNotExist(err) {
t.Skip("kernel doesn't support proc filesystem")
}
if os.IsPermission(err) {
t.Skip("unable to test proc filesystem due to permissions")
}
t.Fatal(err)
}
b, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
orig := strings.TrimSpace(string(b))
if strings.Contains(orig, "lo:") && strings.Count(orig, ":") == 1 {
// This test expects there to be at least 1 more network interface
// in addition to the local network interface, so that it can tell
// that unshare worked.
t.Skip("not enough network interfaces to test unshare with")
}
cmd := testenv.Command(t, "cat", path)
cmd.SysProcAttr = &syscall.SysProcAttr{
Unshareflags: syscall.CLONE_NEWNET,
}
out, err := cmd.CombinedOutput()
if err != nil {
if testenv.SyscallIsNotSupported(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)
}
// Check there is only the local network interface.
sout := strings.TrimSpace(string(out))
if !strings.Contains(sout, "lo:") {
t.Fatalf("Expected lo network interface to exist, got %s", sout)
}
origLines := strings.Split(orig, "\n")
lines := strings.Split(sout, "\n")
if len(lines) >= len(origLines) {
t.Logf("%s before unshare:\n%s", path, orig)
t.Logf("%s after unshare:\n%s", path, sout)
t.Fatalf("Got %d lines of output, want < %d", len(lines), len(origLines))
}
}
func TestGroupCleanup(t *testing.T) {
testenv.MustHaveExecPath(t, "id")
cmd := testenv.Command(t, "id")
cmd.SysProcAttr = &syscall.SysProcAttr{
Credential: &syscall.Credential{
Uid: 0,
Gid: 0,
},
}
out, err := cmd.CombinedOutput()
if err != nil {
if testenv.SyscallIsNotSupported(err) {
t.Skipf("skipping: %v: %v", cmd, err)
}
t.Fatalf("Cmd failed with err %v, output: %s", err, out)
}
strOut := strings.TrimSpace(string(out))
t.Logf("id: %s", strOut)
expected := "uid=0(root) gid=0(root)"
// Just check prefix because some distros reportedly output a
// context parameter; see https://golang.org/issue/16224.
// Alpine does not output groups; see https://golang.org/issue/19938.
if !strings.HasPrefix(strOut, expected) {
t.Errorf("expected prefix: %q", expected)
}
}
func TestGroupCleanupUserNamespace(t *testing.T) {
testenv.MustHaveExecPath(t, "id")
cmd := testenv.Command(t, "id")
uid, gid := os.Getuid(), os.Getgid()
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUSER,
Credential: &syscall.Credential{
Uid: uint32(uid),
Gid: uint32(gid),
},
UidMappings: []syscall.SysProcIDMap{
{ContainerID: 0, HostID: uid, Size: 1},
},
GidMappings: []syscall.SysProcIDMap{
{ContainerID: 0, HostID: gid, Size: 1},
},
}
out, err := cmd.CombinedOutput()
if err != nil {
if testenv.SyscallIsNotSupported(err) {
t.Skipf("skipping: %v: %v", cmd, err)
}
t.Fatalf("Cmd failed with err %v, output: %s", err, out)
}
strOut := strings.TrimSpace(string(out))
t.Logf("id: %s", strOut)
// As in TestGroupCleanup, just check prefix.
// The actual groups and contexts seem to vary from one distro to the next.
expected := "uid=0(root) gid=0(root) groups=0(root)"
if !strings.HasPrefix(strOut, expected) {
t.Errorf("expected prefix: %q", expected)
}
}
// Test for https://go.dev/issue/19661: unshare fails because systemd
// has forced / to be shared
func TestUnshareMountNameSpace(t *testing.T) {
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)
}
exe, err := os.Executable()
if err != nil {
t.Fatal(err)
}
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 testenv.SyscallIsNotSupported(err) {
t.Skipf("skipping: could not start process with CLONE_NEWNS: %v", err)
}
if testing.Short() && testenv.Builder() != "" && os.Getenv("USER") == "swarming" {
// The Go build system's swarming user is known not to support
// starting a process with CLONE_NEWNS.
// Unfortunately, it doesn't get recognized as such due the current
// implementation of a no-network check using 'unshare -n -r'.
// Since this test does need start this process, we need to skip it.
t.Skipf("skipping: could not start process with CLONE_NEWNS: %v", 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.
if err := os.Remove(d); err != nil {
t.Errorf("rmdir failed on %v: %v", d, err)
}
}
// Test for Issue 20103: unshare fails when chroot is used
func TestUnshareMountNameSpaceChroot(t *testing.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 := t.TempDir()
// Since we are doing a chroot, we need the binary there,
// and it must be statically linked.
testenv.MustHaveGoBuild(t)
if platform.MustLinkExternal(runtime.GOOS, runtime.GOARCH, false) {
t.Skipf("skipping: can't build static binary because %s/%s requires external linking", runtime.GOOS, runtime.GOARCH)
}
x := filepath.Join(d, "syscall.test")
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("%v: %v\n%s", cmd, err, o)
}
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 testenv.SyscallIsNotSupported(err) {
t.Skipf("skipping: could not start process with CLONE_NEWNS and Chroot %q: %v", d, err)
}
if testing.Short() && testenv.Builder() != "" && os.Getenv("USER") == "swarming" {
// The Go build system's swarming user is known not to support
// starting a process with CLONE_NEWNS and Chroot.
// Unfortunately, it doesn't get recognized as such due the current
// implementation of a no-network check using 'unshare -n -r'.
// Since this test does need start this process, we need to skip it.
t.Skipf("skipping: could not start process with CLONE_NEWNS and Chroot %q: %v", d, 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.
if err := os.Remove(x); err != nil {
t.Errorf("rm failed on %v: %v", x, err)
}
if err := os.Remove(d); err != nil {
t.Errorf("rmdir failed on %v: %v", d, err)
}
}
// 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")
}
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,
UidMappings: []syscall.SysProcIDMap{
{
ContainerID: 0,
HostID: syscall.Getuid(),
Size: 1,
},
},
GidMappings: []syscall.SysProcIDMap{
{
ContainerID: 0,
HostID: syscall.Getgid(),
Size: 1,
},
},
}
out, err := cmd.CombinedOutput()
if err != nil {
if testenv.SyscallIsNotSupported(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)
}
}
func prepareCgroupFD(t *testing.T) (int, string) {
t.Helper()
const O_PATH = 0x200000 // Same for all architectures, but for some reason not defined in syscall for 386||amd64.
// Requires cgroup v2.
const prefix = "/sys/fs/cgroup"
selfCg, err := os.ReadFile("/proc/self/cgroup")
if err != nil {
if os.IsNotExist(err) || os.IsPermission(err) {
t.Skip(err)
}
t.Fatal(err)
}
// Expect a single line like this:
// 0::/user.slice/user-1000.slice/user@1000.service/app.slice/vte-spawn-891992a2-efbb-4f28-aedb-b24f9e706770.scope
// Otherwise it's either cgroup v1 or a hybrid hierarchy.
if bytes.Count(selfCg, []byte("\n")) > 1 {
t.Skip("cgroup v2 not available")
}
cg := bytes.TrimPrefix(selfCg, []byte("0::"))
if len(cg) == len(selfCg) { // No prefix found.
t.Skipf("cgroup v2 not available (/proc/self/cgroup contents: %q)", selfCg)
}
// Need an ability to create a sub-cgroup.
subCgroup, err := os.MkdirTemp(prefix+string(bytes.TrimSpace(cg)), "subcg-")
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) || testenv.SyscallIsNotSupported(err) {
t.Skipf("skipping: %v", err)
}
t.Fatal(err)
}
t.Cleanup(func() { syscall.Rmdir(subCgroup) })
cgroupFD, err := syscall.Open(subCgroup, O_PATH, 0)
if err != nil {
t.Fatal(&os.PathError{Op: "open", Path: subCgroup, Err: err})
}
t.Cleanup(func() { syscall.Close(cgroupFD) })
return cgroupFD, "/" + path.Base(subCgroup)
}
func TestUseCgroupFD(t *testing.T) {
testenv.MustHaveExec(t)
if os.Getenv("GO_WANT_HELPER_PROCESS") == "1" {
// Read and print own cgroup path.
selfCg, err := os.ReadFile("/proc/self/cgroup")
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
fmt.Print(string(selfCg))
os.Exit(0)
}
exe, err := os.Executable()
if err != nil {
t.Fatal(err)
}
fd, suffix := prepareCgroupFD(t)
cmd := testenv.Command(t, exe, "-test.run=^TestUseCgroupFD$")
cmd.Env = append(cmd.Environ(), "GO_WANT_HELPER_PROCESS=1")
cmd.SysProcAttr = &syscall.SysProcAttr{
UseCgroupFD: true,
CgroupFD: fd,
}
out, err := cmd.CombinedOutput()
if err != nil {
if testenv.SyscallIsNotSupported(err) && !errors.Is(err, syscall.EINVAL) {
// Can be one of:
// - clone3 not supported (old kernel);
// - clone3 not allowed (by e.g. seccomp);
// - lack of CAP_SYS_ADMIN.
t.Skipf("clone3 with CLONE_INTO_CGROUP not available: %v", err)
}
t.Fatalf("Cmd failed with err %v, output: %s", err, out)
}
// NB: this wouldn't work with cgroupns.
if !bytes.HasSuffix(bytes.TrimSpace(out), []byte(suffix)) {
t.Fatalf("got: %q, want: a line that ends with %q", out, suffix)
}
}
func TestCloneTimeNamespace(t *testing.T) {
testenv.MustHaveExec(t)
if os.Getenv("GO_WANT_HELPER_PROCESS") == "1" {
timens, err := os.Readlink("/proc/self/ns/time")
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
fmt.Print(string(timens))
os.Exit(0)
}
exe, err := os.Executable()
if err != nil {
t.Fatal(err)
}
cmd := testenv.Command(t, exe, "-test.run=^TestCloneTimeNamespace$")
cmd.Env = append(cmd.Environ(), "GO_WANT_HELPER_PROCESS=1")
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWTIME,
}
out, err := cmd.CombinedOutput()
if err != nil {
if testenv.SyscallIsNotSupported(err) {
// CLONE_NEWTIME does not appear to be supported.
t.Skipf("skipping, CLONE_NEWTIME not supported: %v", err)
}
t.Fatalf("Cmd failed with err %v, output: %s", err, out)
}
// Inode number of the time namespaces should be different.
// Based on https://man7.org/linux/man-pages/man7/time_namespaces.7.html#EXAMPLES
timens, err := os.Readlink("/proc/self/ns/time")
if err != nil {
t.Fatal(err)
}
parentTimeNS := string(timens)
childTimeNS := string(out)
if childTimeNS == parentTimeNS {
t.Fatalf("expected child time namespace to be different from parent time namespace: %s", parentTimeNS)
}
}
type capHeader struct {
version uint32
pid int32
}
type capData struct {
effective uint32
permitted uint32
inheritable uint32
}
const CAP_SYS_TIME = 25
const CAP_SYSLOG = 34
type caps struct {
hdr capHeader
data [2]capData
}
func getCaps() (caps, error) {
var c caps
// Get capability version
if _, _, errno := syscall.Syscall(syscall.SYS_CAPGET, uintptr(unsafe.Pointer(&c.hdr)), uintptr(unsafe.Pointer(nil)), 0); errno != 0 {
return c, fmt.Errorf("SYS_CAPGET: %v", errno)
}
// Get current capabilities
if _, _, errno := syscall.Syscall(syscall.SYS_CAPGET, uintptr(unsafe.Pointer(&c.hdr)), uintptr(unsafe.Pointer(&c.data[0])), 0); errno != 0 {
return c, fmt.Errorf("SYS_CAPGET: %v", errno)
}
return c, nil
}
func TestAmbientCaps(t *testing.T) {
testAmbientCaps(t, false)
}
func TestAmbientCapsUserns(t *testing.T) {
testAmbientCaps(t, true)
}
func testAmbientCaps(t *testing.T, userns bool) {
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" {
t.Skip("skipping test on android; see Issue 27327")
}
u, err := user.Lookup("nobody")
if err != nil {
t.Fatal(err)
}
uid, err := strconv.ParseInt(u.Uid, 0, 32)
if err != nil {
t.Fatal(err)
}
gid, err := strconv.ParseInt(u.Gid, 0, 32)
if err != nil {
t.Fatal(err)
}
// Copy the test binary to a temporary location which is readable by nobody.
f, err := os.CreateTemp("", "gotest")
if err != nil {
t.Fatal(err)
}
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)
}
defer e.Close()
if _, err := io.Copy(f, e); err != nil {
t.Fatal(err)
}
if err := f.Chmod(0755); err != nil {
t.Fatal(err)
}
if err := f.Close(); err != nil {
t.Fatal(err)
}
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{
Credential: &syscall.Credential{
Uid: uint32(uid),
Gid: uint32(gid),
},
AmbientCaps: []uintptr{CAP_SYS_TIME, CAP_SYSLOG},
}
if userns {
cmd.SysProcAttr.Cloneflags = syscall.CLONE_NEWUSER
const nobody = 65534
uid := os.Getuid()
gid := os.Getgid()
cmd.SysProcAttr.UidMappings = []syscall.SysProcIDMap{{
ContainerID: int(nobody),
HostID: int(uid),
Size: int(1),
}}
cmd.SysProcAttr.GidMappings = []syscall.SysProcIDMap{{
ContainerID: int(nobody),
HostID: int(gid),
Size: int(1),
}}
// Set credentials to run as user and group nobody.
cmd.SysProcAttr.Credential = &syscall.Credential{
Uid: nobody,
Gid: nobody,
}
}
if err := cmd.Run(); err != nil {
if testenv.SyscallIsNotSupported(err) {
t.Skipf("skipping: %v: %v", cmd, err)
}
t.Fatal(err.Error())
}
}