Source file src/syscall/exec_linux_test.go

     1  // Copyright 2015 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  //go:build linux
     6  
     7  package syscall_test
     8  
     9  import (
    10  	"bytes"
    11  	"errors"
    12  	"flag"
    13  	"fmt"
    14  	"internal/asan"
    15  	"internal/platform"
    16  	"internal/syscall/unix"
    17  	"internal/testenv"
    18  	"io"
    19  	"os"
    20  	"os/exec"
    21  	"os/user"
    22  	"path"
    23  	"path/filepath"
    24  	"runtime"
    25  	"strconv"
    26  	"strings"
    27  	"syscall"
    28  	"testing"
    29  	"time"
    30  	"unsafe"
    31  )
    32  
    33  // whoamiNEWUSER returns a command that runs "whoami" with CLONE_NEWUSER,
    34  // mapping uid and gid 0 to the actual uid and gid of the test.
    35  func whoamiNEWUSER(t *testing.T, uid, gid int, setgroups bool) *exec.Cmd {
    36  	t.Helper()
    37  	testenv.MustHaveExecPath(t, "whoami")
    38  	cmd := testenv.Command(t, "whoami")
    39  	cmd.SysProcAttr = &syscall.SysProcAttr{
    40  		Cloneflags: syscall.CLONE_NEWUSER,
    41  		UidMappings: []syscall.SysProcIDMap{
    42  			{ContainerID: 0, HostID: uid, Size: 1},
    43  		},
    44  		GidMappings: []syscall.SysProcIDMap{
    45  			{ContainerID: 0, HostID: gid, Size: 1},
    46  		},
    47  		GidMappingsEnableSetgroups: setgroups,
    48  	}
    49  	return cmd
    50  }
    51  
    52  func TestCloneNEWUSERAndRemap(t *testing.T) {
    53  	for _, setgroups := range []bool{false, true} {
    54  		t.Run(fmt.Sprintf("setgroups=%v", setgroups), func(t *testing.T) {
    55  			uid := os.Getuid()
    56  			gid := os.Getgid()
    57  
    58  			cmd := whoamiNEWUSER(t, uid, gid, setgroups)
    59  			out, err := cmd.CombinedOutput()
    60  			t.Logf("%v: %v", cmd, err)
    61  
    62  			if uid != 0 && setgroups {
    63  				t.Logf("as non-root, expected permission error due to unprivileged gid_map")
    64  				if !os.IsPermission(err) {
    65  					if err == nil {
    66  						t.Skipf("unexpected success: probably old kernel without security fix?")
    67  					}
    68  					if testenv.SyscallIsNotSupported(err) {
    69  						t.Skipf("skipping: CLONE_NEWUSER appears to be unsupported")
    70  					}
    71  					t.Fatalf("got non-permission error") // Already logged above.
    72  				}
    73  				return
    74  			}
    75  
    76  			if err != nil {
    77  				if testenv.SyscallIsNotSupported(err) {
    78  					// May be inside a container that disallows CLONE_NEWUSER.
    79  					t.Skipf("skipping: CLONE_NEWUSER appears to be unsupported")
    80  				}
    81  				t.Fatalf("unexpected command failure; output:\n%s", out)
    82  			}
    83  
    84  			sout := strings.TrimSpace(string(out))
    85  			want := "root"
    86  			if sout != want {
    87  				t.Fatalf("whoami = %q; want %q", out, want)
    88  			}
    89  		})
    90  	}
    91  }
    92  
    93  func TestEmptyCredGroupsDisableSetgroups(t *testing.T) {
    94  	cmd := whoamiNEWUSER(t, os.Getuid(), os.Getgid(), false)
    95  	cmd.SysProcAttr.Credential = &syscall.Credential{}
    96  	if err := cmd.Run(); err != nil {
    97  		if testenv.SyscallIsNotSupported(err) {
    98  			t.Skipf("skipping: %v: %v", cmd, err)
    99  		}
   100  		t.Fatal(err)
   101  	}
   102  }
   103  
   104  func TestUnshare(t *testing.T) {
   105  	path := "/proc/net/dev"
   106  	if _, err := os.Stat(path); err != nil {
   107  		if os.IsNotExist(err) {
   108  			t.Skip("kernel doesn't support proc filesystem")
   109  		}
   110  		if os.IsPermission(err) {
   111  			t.Skip("unable to test proc filesystem due to permissions")
   112  		}
   113  		t.Fatal(err)
   114  	}
   115  
   116  	b, err := os.ReadFile(path)
   117  	if err != nil {
   118  		t.Fatal(err)
   119  	}
   120  	orig := strings.TrimSpace(string(b))
   121  	if strings.Contains(orig, "lo:") && strings.Count(orig, ":") == 1 {
   122  		// This test expects there to be at least 1 more network interface
   123  		// in addition to the local network interface, so that it can tell
   124  		// that unshare worked.
   125  		t.Skip("not enough network interfaces to test unshare with")
   126  	}
   127  
   128  	cmd := testenv.Command(t, "cat", path)
   129  	cmd.SysProcAttr = &syscall.SysProcAttr{
   130  		Unshareflags: syscall.CLONE_NEWNET,
   131  	}
   132  	out, err := cmd.CombinedOutput()
   133  	if err != nil {
   134  		if testenv.SyscallIsNotSupported(err) {
   135  			// CLONE_NEWNET does not appear to be supported.
   136  			t.Skipf("skipping due to permission error: %v", err)
   137  		}
   138  		t.Fatalf("Cmd failed with err %v, output: %s", err, out)
   139  	}
   140  
   141  	// Check there is only the local network interface.
   142  	sout := strings.TrimSpace(string(out))
   143  	if !strings.Contains(sout, "lo:") {
   144  		t.Fatalf("Expected lo network interface to exist, got %s", sout)
   145  	}
   146  
   147  	origLines := strings.Split(orig, "\n")
   148  	lines := strings.Split(sout, "\n")
   149  	if len(lines) >= len(origLines) {
   150  		t.Logf("%s before unshare:\n%s", path, orig)
   151  		t.Logf("%s after unshare:\n%s", path, sout)
   152  		t.Fatalf("Got %d lines of output, want < %d", len(lines), len(origLines))
   153  	}
   154  }
   155  
   156  func TestGroupCleanup(t *testing.T) {
   157  	testenv.MustHaveExecPath(t, "id")
   158  	cmd := testenv.Command(t, "id")
   159  	cmd.SysProcAttr = &syscall.SysProcAttr{
   160  		Credential: &syscall.Credential{
   161  			Uid: 0,
   162  			Gid: 0,
   163  		},
   164  	}
   165  	out, err := cmd.CombinedOutput()
   166  	if err != nil {
   167  		if testenv.SyscallIsNotSupported(err) {
   168  			t.Skipf("skipping: %v: %v", cmd, err)
   169  		}
   170  		t.Fatalf("Cmd failed with err %v, output: %s", err, out)
   171  	}
   172  	strOut := strings.TrimSpace(string(out))
   173  	t.Logf("id: %s", strOut)
   174  
   175  	expected := "uid=0(root) gid=0(root)"
   176  	// Just check prefix because some distros reportedly output a
   177  	// context parameter; see https://golang.org/issue/16224.
   178  	// Alpine does not output groups; see https://golang.org/issue/19938.
   179  	if !strings.HasPrefix(strOut, expected) {
   180  		t.Errorf("expected prefix: %q", expected)
   181  	}
   182  }
   183  
   184  func TestGroupCleanupUserNamespace(t *testing.T) {
   185  	testenv.MustHaveExecPath(t, "id")
   186  	cmd := testenv.Command(t, "id")
   187  	uid, gid := os.Getuid(), os.Getgid()
   188  	cmd.SysProcAttr = &syscall.SysProcAttr{
   189  		Cloneflags: syscall.CLONE_NEWUSER,
   190  		Credential: &syscall.Credential{
   191  			Uid: uint32(uid),
   192  			Gid: uint32(gid),
   193  		},
   194  		UidMappings: []syscall.SysProcIDMap{
   195  			{ContainerID: 0, HostID: uid, Size: 1},
   196  		},
   197  		GidMappings: []syscall.SysProcIDMap{
   198  			{ContainerID: 0, HostID: gid, Size: 1},
   199  		},
   200  	}
   201  	out, err := cmd.CombinedOutput()
   202  	if err != nil {
   203  		if testenv.SyscallIsNotSupported(err) {
   204  			t.Skipf("skipping: %v: %v", cmd, err)
   205  		}
   206  		t.Fatalf("Cmd failed with err %v, output: %s", err, out)
   207  	}
   208  	strOut := strings.TrimSpace(string(out))
   209  	t.Logf("id: %s", strOut)
   210  
   211  	// As in TestGroupCleanup, just check prefix.
   212  	// The actual groups and contexts seem to vary from one distro to the next.
   213  	expected := "uid=0(root) gid=0(root) groups=0(root)"
   214  	if !strings.HasPrefix(strOut, expected) {
   215  		t.Errorf("expected prefix: %q", expected)
   216  	}
   217  }
   218  
   219  // Test for https://go.dev/issue/19661: unshare fails because systemd
   220  // has forced / to be shared
   221  func TestUnshareMountNameSpace(t *testing.T) {
   222  	const mountNotSupported = "mount is not supported: " // Output prefix indicating a test skip.
   223  	if os.Getenv("GO_WANT_HELPER_PROCESS") == "1" {
   224  		dir := flag.Args()[0]
   225  		err := syscall.Mount("none", dir, "proc", 0, "")
   226  		if testenv.SyscallIsNotSupported(err) {
   227  			fmt.Print(mountNotSupported, err)
   228  		} else if err != nil {
   229  			fmt.Fprintf(os.Stderr, "unshare: mount %s: %v\n", dir, err)
   230  			os.Exit(2)
   231  		}
   232  		os.Exit(0)
   233  	}
   234  
   235  	exe := testenv.Executable(t)
   236  	d := t.TempDir()
   237  	t.Cleanup(func() {
   238  		// If the subprocess fails to unshare the parent directory, force-unmount it
   239  		// so that the test can clean it up.
   240  		if _, err := os.Stat(d); err == nil {
   241  			syscall.Unmount(d, syscall.MNT_FORCE)
   242  		}
   243  	})
   244  	cmd := testenv.Command(t, exe, "-test.run=^TestUnshareMountNameSpace$", d)
   245  	cmd.Env = append(cmd.Environ(), "GO_WANT_HELPER_PROCESS=1")
   246  	cmd.SysProcAttr = &syscall.SysProcAttr{Unshareflags: syscall.CLONE_NEWNS}
   247  
   248  	out, err := cmd.CombinedOutput()
   249  	if err != nil {
   250  		if testenv.SyscallIsNotSupported(err) {
   251  			t.Skipf("skipping: could not start process with CLONE_NEWNS: %v", err)
   252  		}
   253  		t.Fatalf("unshare failed: %v\n%s", err, out)
   254  	} else if len(out) != 0 {
   255  		if bytes.HasPrefix(out, []byte(mountNotSupported)) {
   256  			t.Skipf("skipping: helper process reported %s", out)
   257  		}
   258  		t.Fatalf("unexpected output from helper process: %s", out)
   259  	}
   260  
   261  	// How do we tell if the namespace was really unshared? It turns out
   262  	// to be simple: just try to remove the directory. If it's still mounted
   263  	// on the rm will fail with EBUSY.
   264  	if err := os.Remove(d); err != nil {
   265  		t.Errorf("rmdir failed on %v: %v", d, err)
   266  	}
   267  }
   268  
   269  // Test for Issue 20103: unshare fails when chroot is used
   270  func TestUnshareMountNameSpaceChroot(t *testing.T) {
   271  	const mountNotSupported = "mount is not supported: " // Output prefix indicating a test skip.
   272  	if os.Getenv("GO_WANT_HELPER_PROCESS") == "1" {
   273  		dir := flag.Args()[0]
   274  		err := syscall.Mount("none", dir, "proc", 0, "")
   275  		if testenv.SyscallIsNotSupported(err) {
   276  			fmt.Print(mountNotSupported, err)
   277  		} else if err != nil {
   278  			fmt.Fprintf(os.Stderr, "unshare: mount %s: %v\n", dir, err)
   279  			os.Exit(2)
   280  		}
   281  		os.Exit(0)
   282  	}
   283  
   284  	d := t.TempDir()
   285  
   286  	// Since we are doing a chroot, we need the binary there,
   287  	// and it must be statically linked.
   288  	testenv.MustHaveGoBuild(t)
   289  	if platform.MustLinkExternal(runtime.GOOS, runtime.GOARCH, false) {
   290  		t.Skipf("skipping: can't build static binary because %s/%s requires external linking", runtime.GOOS, runtime.GOARCH)
   291  	}
   292  	x := filepath.Join(d, "syscall.test")
   293  	t.Cleanup(func() {
   294  		// If the subprocess fails to unshare the parent directory, force-unmount it
   295  		// so that the test can clean it up.
   296  		if _, err := os.Stat(d); err == nil {
   297  			syscall.Unmount(d, syscall.MNT_FORCE)
   298  		}
   299  	})
   300  
   301  	cmd := testenv.Command(t, testenv.GoToolPath(t), "test", "-c", "-o", x, "syscall")
   302  	cmd.Env = append(cmd.Environ(), "CGO_ENABLED=0")
   303  	if o, err := cmd.CombinedOutput(); err != nil {
   304  		t.Fatalf("%v: %v\n%s", cmd, err, o)
   305  	}
   306  
   307  	cmd = testenv.Command(t, "/syscall.test", "-test.run=^TestUnshareMountNameSpaceChroot$", "/")
   308  	cmd.Env = append(cmd.Environ(), "GO_WANT_HELPER_PROCESS=1")
   309  	cmd.SysProcAttr = &syscall.SysProcAttr{Chroot: d, Unshareflags: syscall.CLONE_NEWNS}
   310  
   311  	out, err := cmd.CombinedOutput()
   312  	if err != nil {
   313  		if testenv.SyscallIsNotSupported(err) {
   314  			t.Skipf("skipping: could not start process with CLONE_NEWNS and Chroot %q: %v", d, err)
   315  		}
   316  		t.Fatalf("unshare failed: %v\n%s", err, out)
   317  	} else if len(out) != 0 {
   318  		if bytes.HasPrefix(out, []byte(mountNotSupported)) {
   319  			t.Skipf("skipping: helper process reported %s", out)
   320  		}
   321  		t.Fatalf("unexpected output from helper process: %s", out)
   322  	}
   323  
   324  	// How do we tell if the namespace was really unshared? It turns out
   325  	// to be simple: just try to remove the executable. If it's still mounted
   326  	// on, the rm will fail.
   327  	if err := os.Remove(x); err != nil {
   328  		t.Errorf("rm failed on %v: %v", x, err)
   329  	}
   330  	if err := os.Remove(d); err != nil {
   331  		t.Errorf("rmdir failed on %v: %v", d, err)
   332  	}
   333  }
   334  
   335  // Test for Issue 29789: unshare fails when uid/gid mapping is specified
   336  func TestUnshareUidGidMapping(t *testing.T) {
   337  	if asan.Enabled {
   338  		t.Skip("test fails with ASAN beause the ASAN leak checker fails finding memory regions")
   339  	}
   340  
   341  	if os.Getenv("GO_WANT_HELPER_PROCESS") == "1" {
   342  		defer os.Exit(0)
   343  		if err := syscall.Chroot(os.TempDir()); err != nil {
   344  			fmt.Fprintln(os.Stderr, err)
   345  			os.Exit(2)
   346  		}
   347  	}
   348  
   349  	if os.Getuid() == 0 {
   350  		t.Skip("test exercises unprivileged user namespace, fails with privileges")
   351  	}
   352  
   353  	exe := testenv.Executable(t)
   354  	cmd := testenv.Command(t, exe, "-test.run=^TestUnshareUidGidMapping$")
   355  	cmd.Env = append(cmd.Environ(), "GO_WANT_HELPER_PROCESS=1")
   356  	cmd.SysProcAttr = &syscall.SysProcAttr{
   357  		Unshareflags:               syscall.CLONE_NEWNS | syscall.CLONE_NEWUSER,
   358  		GidMappingsEnableSetgroups: false,
   359  		UidMappings: []syscall.SysProcIDMap{
   360  			{
   361  				ContainerID: 0,
   362  				HostID:      syscall.Getuid(),
   363  				Size:        1,
   364  			},
   365  		},
   366  		GidMappings: []syscall.SysProcIDMap{
   367  			{
   368  				ContainerID: 0,
   369  				HostID:      syscall.Getgid(),
   370  				Size:        1,
   371  			},
   372  		},
   373  	}
   374  	out, err := cmd.CombinedOutput()
   375  	if err != nil {
   376  		if testenv.SyscallIsNotSupported(err) {
   377  			t.Skipf("skipping: could not start process with CLONE_NEWNS and CLONE_NEWUSER: %v", err)
   378  		}
   379  		t.Fatalf("Cmd failed with err %v, output: %s", err, out)
   380  	}
   381  }
   382  
   383  func prepareCgroupFD(t *testing.T) (int, string) {
   384  	t.Helper()
   385  
   386  	const O_PATH = 0x200000 // Same for all architectures, but for some reason not defined in syscall for 386||amd64.
   387  
   388  	// Requires cgroup v2.
   389  	const prefix = "/sys/fs/cgroup"
   390  	selfCg, err := os.ReadFile("/proc/self/cgroup")
   391  	if err != nil {
   392  		if os.IsNotExist(err) || os.IsPermission(err) {
   393  			t.Skip(err)
   394  		}
   395  		t.Fatal(err)
   396  	}
   397  
   398  	// Expect a single line like this:
   399  	// 0::/user.slice/user-1000.slice/user@1000.service/app.slice/vte-spawn-891992a2-efbb-4f28-aedb-b24f9e706770.scope
   400  	// Otherwise it's either cgroup v1 or a hybrid hierarchy.
   401  	if bytes.Count(selfCg, []byte("\n")) > 1 {
   402  		t.Skip("cgroup v2 not available")
   403  	}
   404  	cg := bytes.TrimPrefix(selfCg, []byte("0::"))
   405  	if len(cg) == len(selfCg) { // No prefix found.
   406  		t.Skipf("cgroup v2 not available (/proc/self/cgroup contents: %q)", selfCg)
   407  	}
   408  
   409  	// Need an ability to create a sub-cgroup.
   410  	subCgroup, err := os.MkdirTemp(prefix+string(bytes.TrimSpace(cg)), "subcg-")
   411  	if err != nil {
   412  		// ErrPermission or EROFS (#57262) when running in an unprivileged container.
   413  		// ErrNotExist when cgroupfs is not mounted in chroot/schroot.
   414  		if os.IsNotExist(err) || testenv.SyscallIsNotSupported(err) {
   415  			t.Skipf("skipping: %v", err)
   416  		}
   417  		t.Fatal(err)
   418  	}
   419  	t.Cleanup(func() { syscall.Rmdir(subCgroup) })
   420  
   421  	cgroupFD, err := syscall.Open(subCgroup, O_PATH, 0)
   422  	if err != nil {
   423  		t.Fatal(&os.PathError{Op: "open", Path: subCgroup, Err: err})
   424  	}
   425  	t.Cleanup(func() { syscall.Close(cgroupFD) })
   426  
   427  	return cgroupFD, "/" + path.Base(subCgroup)
   428  }
   429  
   430  func TestUseCgroupFD(t *testing.T) {
   431  	if os.Getenv("GO_WANT_HELPER_PROCESS") == "1" {
   432  		// Read and print own cgroup path.
   433  		selfCg, err := os.ReadFile("/proc/self/cgroup")
   434  		if err != nil {
   435  			fmt.Fprintln(os.Stderr, err)
   436  			os.Exit(2)
   437  		}
   438  		fmt.Print(string(selfCg))
   439  		os.Exit(0)
   440  	}
   441  
   442  	exe := testenv.Executable(t)
   443  	fd, suffix := prepareCgroupFD(t)
   444  
   445  	cmd := testenv.Command(t, exe, "-test.run=^TestUseCgroupFD$")
   446  	cmd.Env = append(cmd.Environ(), "GO_WANT_HELPER_PROCESS=1")
   447  	cmd.SysProcAttr = &syscall.SysProcAttr{
   448  		UseCgroupFD: true,
   449  		CgroupFD:    fd,
   450  	}
   451  	out, err := cmd.CombinedOutput()
   452  	if err != nil {
   453  		if testenv.SyscallIsNotSupported(err) && !errors.Is(err, syscall.EINVAL) {
   454  			// Can be one of:
   455  			// - clone3 not supported (old kernel);
   456  			// - clone3 not allowed (by e.g. seccomp);
   457  			// - lack of CAP_SYS_ADMIN.
   458  			t.Skipf("clone3 with CLONE_INTO_CGROUP not available: %v", err)
   459  		}
   460  		t.Fatalf("Cmd failed with err %v, output: %s", err, out)
   461  	}
   462  	// NB: this wouldn't work with cgroupns.
   463  	if !bytes.HasSuffix(bytes.TrimSpace(out), []byte(suffix)) {
   464  		t.Fatalf("got: %q, want: a line that ends with %q", out, suffix)
   465  	}
   466  }
   467  
   468  func TestCloneTimeNamespace(t *testing.T) {
   469  	if os.Getenv("GO_WANT_HELPER_PROCESS") == "1" {
   470  		timens, err := os.Readlink("/proc/self/ns/time")
   471  		if err != nil {
   472  			fmt.Fprintln(os.Stderr, err)
   473  			os.Exit(2)
   474  		}
   475  		fmt.Print(string(timens))
   476  		os.Exit(0)
   477  	}
   478  
   479  	exe := testenv.Executable(t)
   480  	cmd := testenv.Command(t, exe, "-test.run=^TestCloneTimeNamespace$")
   481  	cmd.Env = append(cmd.Environ(), "GO_WANT_HELPER_PROCESS=1")
   482  	cmd.SysProcAttr = &syscall.SysProcAttr{
   483  		Cloneflags: syscall.CLONE_NEWTIME,
   484  	}
   485  	out, err := cmd.CombinedOutput()
   486  	if err != nil {
   487  		if testenv.SyscallIsNotSupported(err) {
   488  			// CLONE_NEWTIME does not appear to be supported.
   489  			t.Skipf("skipping, CLONE_NEWTIME not supported: %v", err)
   490  		}
   491  		t.Fatalf("Cmd failed with err %v, output: %s", err, out)
   492  	}
   493  
   494  	// Inode number of the time namespaces should be different.
   495  	// Based on https://man7.org/linux/man-pages/man7/time_namespaces.7.html#EXAMPLES
   496  	timens, err := os.Readlink("/proc/self/ns/time")
   497  	if err != nil {
   498  		t.Fatal(err)
   499  	}
   500  
   501  	parentTimeNS := timens
   502  	childTimeNS := string(out)
   503  	if childTimeNS == parentTimeNS {
   504  		t.Fatalf("expected child time namespace to be different from parent time namespace: %s", parentTimeNS)
   505  	}
   506  }
   507  
   508  func testPidFD(t *testing.T, userns bool) error {
   509  	if os.Getenv("GO_WANT_HELPER_PROCESS") == "1" {
   510  		// Child: wait for a signal.
   511  		time.Sleep(time.Hour)
   512  	}
   513  
   514  	exe := testenv.Executable(t)
   515  	var pidfd int
   516  	cmd := testenv.Command(t, exe, "-test.run=^TestPidFD$")
   517  	cmd.Env = append(cmd.Environ(), "GO_WANT_HELPER_PROCESS=1")
   518  	cmd.SysProcAttr = &syscall.SysProcAttr{
   519  		PidFD: &pidfd,
   520  	}
   521  	if userns {
   522  		cmd.SysProcAttr.Cloneflags = syscall.CLONE_NEWUSER
   523  	}
   524  	if err := cmd.Start(); err != nil {
   525  		return err
   526  	}
   527  	defer func() {
   528  		cmd.Process.Kill()
   529  		cmd.Wait()
   530  	}()
   531  	t.Log("got pidfd:", pidfd)
   532  	// If pidfd is not supported by the kernel, -1 is returned.
   533  	if pidfd == -1 {
   534  		t.Skip("pidfd not supported")
   535  	}
   536  	defer syscall.Close(pidfd)
   537  
   538  	// Use pidfd to send a signal to the child.
   539  	sig := syscall.SIGINT
   540  	if err := unix.PidFDSendSignal(uintptr(pidfd), sig); err != nil {
   541  		if err != syscall.EINVAL && testenv.SyscallIsNotSupported(err) {
   542  			t.Skip("pidfd_send_signal syscall not supported:", err)
   543  		}
   544  		t.Fatal("pidfd_send_signal syscall failed:", err)
   545  	}
   546  	// Check if the child received our signal.
   547  	err := cmd.Wait()
   548  	if cmd.ProcessState == nil || cmd.ProcessState.Sys().(syscall.WaitStatus).Signal() != sig {
   549  		t.Fatal("unexpected child error:", err)
   550  	}
   551  	return nil
   552  }
   553  
   554  func TestPidFD(t *testing.T) {
   555  	if err := testPidFD(t, false); err != nil {
   556  		t.Fatal("can't start a process:", err)
   557  	}
   558  }
   559  
   560  func TestPidFDWithUserNS(t *testing.T) {
   561  	if err := testPidFD(t, true); err != nil {
   562  		if testenv.SyscallIsNotSupported(err) {
   563  			t.Skip("userns not supported:", err)
   564  		}
   565  		t.Fatal("can't start a process:", err)
   566  	}
   567  }
   568  
   569  func TestPidFDClone3(t *testing.T) {
   570  	*syscall.ForceClone3 = true
   571  	defer func() { *syscall.ForceClone3 = false }()
   572  
   573  	if err := testPidFD(t, false); err != nil {
   574  		if testenv.SyscallIsNotSupported(err) {
   575  			t.Skip("clone3 not supported:", err)
   576  		}
   577  		t.Fatal("can't start a process:", err)
   578  	}
   579  }
   580  
   581  type capHeader struct {
   582  	version uint32
   583  	pid     int32
   584  }
   585  
   586  type capData struct {
   587  	effective   uint32
   588  	permitted   uint32
   589  	inheritable uint32
   590  }
   591  
   592  const CAP_SYS_TIME = 25
   593  const CAP_SYSLOG = 34
   594  
   595  type caps struct {
   596  	hdr  capHeader
   597  	data [2]capData
   598  }
   599  
   600  func getCaps() (caps, error) {
   601  	var c caps
   602  
   603  	// Get capability version
   604  	if _, _, errno := syscall.Syscall(syscall.SYS_CAPGET, uintptr(unsafe.Pointer(&c.hdr)), uintptr(unsafe.Pointer(nil)), 0); errno != 0 {
   605  		return c, fmt.Errorf("SYS_CAPGET: %v", errno)
   606  	}
   607  
   608  	// Get current capabilities
   609  	if _, _, errno := syscall.Syscall(syscall.SYS_CAPGET, uintptr(unsafe.Pointer(&c.hdr)), uintptr(unsafe.Pointer(&c.data[0])), 0); errno != 0 {
   610  		return c, fmt.Errorf("SYS_CAPGET: %v", errno)
   611  	}
   612  
   613  	return c, nil
   614  }
   615  
   616  func TestAmbientCaps(t *testing.T) {
   617  	testAmbientCaps(t, false)
   618  }
   619  
   620  func TestAmbientCapsUserns(t *testing.T) {
   621  	b, err := os.ReadFile("/proc/sys/kernel/apparmor_restrict_unprivileged_userns")
   622  	if err == nil && strings.TrimSpace(string(b)) == "1" {
   623  		t.Skip("AppArmor restriction for unprivileged user namespaces is enabled")
   624  	}
   625  	testAmbientCaps(t, true)
   626  }
   627  
   628  func testAmbientCaps(t *testing.T, userns bool) {
   629  	if os.Getenv("GO_WANT_HELPER_PROCESS") == "1" {
   630  		caps, err := getCaps()
   631  		if err != nil {
   632  			fmt.Fprintln(os.Stderr, err)
   633  			os.Exit(2)
   634  		}
   635  		if caps.data[0].effective&(1<<uint(CAP_SYS_TIME)) == 0 {
   636  			fmt.Fprintln(os.Stderr, "CAP_SYS_TIME unexpectedly not in the effective capability mask")
   637  			os.Exit(2)
   638  		}
   639  		if caps.data[1].effective&(1<<uint(CAP_SYSLOG&31)) == 0 {
   640  			fmt.Fprintln(os.Stderr, "CAP_SYSLOG unexpectedly not in the effective capability mask")
   641  			os.Exit(2)
   642  		}
   643  		os.Exit(0)
   644  	}
   645  
   646  	// skip on android, due to lack of lookup support
   647  	if runtime.GOOS == "android" {
   648  		t.Skip("skipping test on android; see Issue 27327")
   649  	}
   650  
   651  	u, err := user.Lookup("nobody")
   652  	if err != nil {
   653  		t.Skip("skipping: the nobody user does not exist; see Issue 71644")
   654  	}
   655  	uid, err := strconv.ParseInt(u.Uid, 0, 32)
   656  	if err != nil {
   657  		t.Fatal(err)
   658  	}
   659  	gid, err := strconv.ParseInt(u.Gid, 0, 32)
   660  	if err != nil {
   661  		t.Fatal(err)
   662  	}
   663  
   664  	// Copy the test binary to a temporary location which is readable by nobody.
   665  	f, err := os.CreateTemp("", "gotest")
   666  	if err != nil {
   667  		t.Fatal(err)
   668  	}
   669  	t.Cleanup(func() {
   670  		f.Close()
   671  		os.Remove(f.Name())
   672  	})
   673  
   674  	exe := testenv.Executable(t)
   675  	e, err := os.Open(exe)
   676  	if err != nil {
   677  		t.Fatal(err)
   678  	}
   679  	defer e.Close()
   680  	if _, err := io.Copy(f, e); err != nil {
   681  		t.Fatal(err)
   682  	}
   683  	if err := f.Chmod(0755); err != nil {
   684  		t.Fatal(err)
   685  	}
   686  	if err := f.Close(); err != nil {
   687  		t.Fatal(err)
   688  	}
   689  
   690  	cmd := testenv.Command(t, f.Name(), "-test.run=^"+t.Name()+"$")
   691  	cmd.Env = append(cmd.Environ(), "GO_WANT_HELPER_PROCESS=1")
   692  	cmd.Stdout = os.Stdout
   693  	cmd.Stderr = os.Stderr
   694  	cmd.SysProcAttr = &syscall.SysProcAttr{
   695  		Credential: &syscall.Credential{
   696  			Uid: uint32(uid),
   697  			Gid: uint32(gid),
   698  		},
   699  		AmbientCaps: []uintptr{CAP_SYS_TIME, CAP_SYSLOG},
   700  	}
   701  	if userns {
   702  		cmd.SysProcAttr.Cloneflags = syscall.CLONE_NEWUSER
   703  		const nobody = 65534
   704  		uid := os.Getuid()
   705  		gid := os.Getgid()
   706  		cmd.SysProcAttr.UidMappings = []syscall.SysProcIDMap{{
   707  			ContainerID: int(nobody),
   708  			HostID:      uid,
   709  			Size:        int(1),
   710  		}}
   711  		cmd.SysProcAttr.GidMappings = []syscall.SysProcIDMap{{
   712  			ContainerID: int(nobody),
   713  			HostID:      gid,
   714  			Size:        int(1),
   715  		}}
   716  
   717  		// Set credentials to run as user and group nobody.
   718  		cmd.SysProcAttr.Credential = &syscall.Credential{
   719  			Uid: nobody,
   720  			Gid: nobody,
   721  		}
   722  	}
   723  	if err := cmd.Run(); err != nil {
   724  		if testenv.SyscallIsNotSupported(err) {
   725  			t.Skipf("skipping: %v: %v", cmd, err)
   726  		}
   727  		t.Fatal(err.Error())
   728  	}
   729  }
   730  

View as plain text