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/platform"
    15  	"internal/syscall/unix"
    16  	"internal/testenv"
    17  	"io"
    18  	"os"
    19  	"os/exec"
    20  	"os/user"
    21  	"path"
    22  	"path/filepath"
    23  	"runtime"
    24  	"strconv"
    25  	"strings"
    26  	"syscall"
    27  	"testing"
    28  	"time"
    29  	"unsafe"
    30  )
    31  
    32  // whoamiNEWUSER returns a command that runs "whoami" with CLONE_NEWUSER,
    33  // mapping uid and gid 0 to the actual uid and gid of the test.
    34  func whoamiNEWUSER(t *testing.T, uid, gid int, setgroups bool) *exec.Cmd {
    35  	t.Helper()
    36  	testenv.MustHaveExecPath(t, "whoami")
    37  	cmd := testenv.Command(t, "whoami")
    38  	cmd.SysProcAttr = &syscall.SysProcAttr{
    39  		Cloneflags: syscall.CLONE_NEWUSER,
    40  		UidMappings: []syscall.SysProcIDMap{
    41  			{ContainerID: 0, HostID: uid, Size: 1},
    42  		},
    43  		GidMappings: []syscall.SysProcIDMap{
    44  			{ContainerID: 0, HostID: gid, Size: 1},
    45  		},
    46  		GidMappingsEnableSetgroups: setgroups,
    47  	}
    48  	return cmd
    49  }
    50  
    51  func TestCloneNEWUSERAndRemap(t *testing.T) {
    52  	for _, setgroups := range []bool{false, true} {
    53  		setgroups := setgroups
    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 os.Getenv("GO_WANT_HELPER_PROCESS") == "1" {
   338  		defer os.Exit(0)
   339  		if err := syscall.Chroot(os.TempDir()); err != nil {
   340  			fmt.Fprintln(os.Stderr, err)
   341  			os.Exit(2)
   342  		}
   343  	}
   344  
   345  	if os.Getuid() == 0 {
   346  		t.Skip("test exercises unprivileged user namespace, fails with privileges")
   347  	}
   348  
   349  	exe := testenv.Executable(t)
   350  	cmd := testenv.Command(t, exe, "-test.run=^TestUnshareUidGidMapping$")
   351  	cmd.Env = append(cmd.Environ(), "GO_WANT_HELPER_PROCESS=1")
   352  	cmd.SysProcAttr = &syscall.SysProcAttr{
   353  		Unshareflags:               syscall.CLONE_NEWNS | syscall.CLONE_NEWUSER,
   354  		GidMappingsEnableSetgroups: false,
   355  		UidMappings: []syscall.SysProcIDMap{
   356  			{
   357  				ContainerID: 0,
   358  				HostID:      syscall.Getuid(),
   359  				Size:        1,
   360  			},
   361  		},
   362  		GidMappings: []syscall.SysProcIDMap{
   363  			{
   364  				ContainerID: 0,
   365  				HostID:      syscall.Getgid(),
   366  				Size:        1,
   367  			},
   368  		},
   369  	}
   370  	out, err := cmd.CombinedOutput()
   371  	if err != nil {
   372  		if testenv.SyscallIsNotSupported(err) {
   373  			t.Skipf("skipping: could not start process with CLONE_NEWNS and CLONE_NEWUSER: %v", err)
   374  		}
   375  		t.Fatalf("Cmd failed with err %v, output: %s", err, out)
   376  	}
   377  }
   378  
   379  func prepareCgroupFD(t *testing.T) (int, string) {
   380  	t.Helper()
   381  
   382  	const O_PATH = 0x200000 // Same for all architectures, but for some reason not defined in syscall for 386||amd64.
   383  
   384  	// Requires cgroup v2.
   385  	const prefix = "/sys/fs/cgroup"
   386  	selfCg, err := os.ReadFile("/proc/self/cgroup")
   387  	if err != nil {
   388  		if os.IsNotExist(err) || os.IsPermission(err) {
   389  			t.Skip(err)
   390  		}
   391  		t.Fatal(err)
   392  	}
   393  
   394  	// Expect a single line like this:
   395  	// 0::/user.slice/user-1000.slice/user@1000.service/app.slice/vte-spawn-891992a2-efbb-4f28-aedb-b24f9e706770.scope
   396  	// Otherwise it's either cgroup v1 or a hybrid hierarchy.
   397  	if bytes.Count(selfCg, []byte("\n")) > 1 {
   398  		t.Skip("cgroup v2 not available")
   399  	}
   400  	cg := bytes.TrimPrefix(selfCg, []byte("0::"))
   401  	if len(cg) == len(selfCg) { // No prefix found.
   402  		t.Skipf("cgroup v2 not available (/proc/self/cgroup contents: %q)", selfCg)
   403  	}
   404  
   405  	// Need an ability to create a sub-cgroup.
   406  	subCgroup, err := os.MkdirTemp(prefix+string(bytes.TrimSpace(cg)), "subcg-")
   407  	if err != nil {
   408  		// ErrPermission or EROFS (#57262) when running in an unprivileged container.
   409  		// ErrNotExist when cgroupfs is not mounted in chroot/schroot.
   410  		if os.IsNotExist(err) || testenv.SyscallIsNotSupported(err) {
   411  			t.Skipf("skipping: %v", err)
   412  		}
   413  		t.Fatal(err)
   414  	}
   415  	t.Cleanup(func() { syscall.Rmdir(subCgroup) })
   416  
   417  	cgroupFD, err := syscall.Open(subCgroup, O_PATH, 0)
   418  	if err != nil {
   419  		t.Fatal(&os.PathError{Op: "open", Path: subCgroup, Err: err})
   420  	}
   421  	t.Cleanup(func() { syscall.Close(cgroupFD) })
   422  
   423  	return cgroupFD, "/" + path.Base(subCgroup)
   424  }
   425  
   426  func TestUseCgroupFD(t *testing.T) {
   427  	if os.Getenv("GO_WANT_HELPER_PROCESS") == "1" {
   428  		// Read and print own cgroup path.
   429  		selfCg, err := os.ReadFile("/proc/self/cgroup")
   430  		if err != nil {
   431  			fmt.Fprintln(os.Stderr, err)
   432  			os.Exit(2)
   433  		}
   434  		fmt.Print(string(selfCg))
   435  		os.Exit(0)
   436  	}
   437  
   438  	exe := testenv.Executable(t)
   439  	fd, suffix := prepareCgroupFD(t)
   440  
   441  	cmd := testenv.Command(t, exe, "-test.run=^TestUseCgroupFD$")
   442  	cmd.Env = append(cmd.Environ(), "GO_WANT_HELPER_PROCESS=1")
   443  	cmd.SysProcAttr = &syscall.SysProcAttr{
   444  		UseCgroupFD: true,
   445  		CgroupFD:    fd,
   446  	}
   447  	out, err := cmd.CombinedOutput()
   448  	if err != nil {
   449  		if testenv.SyscallIsNotSupported(err) && !errors.Is(err, syscall.EINVAL) {
   450  			// Can be one of:
   451  			// - clone3 not supported (old kernel);
   452  			// - clone3 not allowed (by e.g. seccomp);
   453  			// - lack of CAP_SYS_ADMIN.
   454  			t.Skipf("clone3 with CLONE_INTO_CGROUP not available: %v", err)
   455  		}
   456  		t.Fatalf("Cmd failed with err %v, output: %s", err, out)
   457  	}
   458  	// NB: this wouldn't work with cgroupns.
   459  	if !bytes.HasSuffix(bytes.TrimSpace(out), []byte(suffix)) {
   460  		t.Fatalf("got: %q, want: a line that ends with %q", out, suffix)
   461  	}
   462  }
   463  
   464  func TestCloneTimeNamespace(t *testing.T) {
   465  	if os.Getenv("GO_WANT_HELPER_PROCESS") == "1" {
   466  		timens, err := os.Readlink("/proc/self/ns/time")
   467  		if err != nil {
   468  			fmt.Fprintln(os.Stderr, err)
   469  			os.Exit(2)
   470  		}
   471  		fmt.Print(string(timens))
   472  		os.Exit(0)
   473  	}
   474  
   475  	exe := testenv.Executable(t)
   476  	cmd := testenv.Command(t, exe, "-test.run=^TestCloneTimeNamespace$")
   477  	cmd.Env = append(cmd.Environ(), "GO_WANT_HELPER_PROCESS=1")
   478  	cmd.SysProcAttr = &syscall.SysProcAttr{
   479  		Cloneflags: syscall.CLONE_NEWTIME,
   480  	}
   481  	out, err := cmd.CombinedOutput()
   482  	if err != nil {
   483  		if testenv.SyscallIsNotSupported(err) {
   484  			// CLONE_NEWTIME does not appear to be supported.
   485  			t.Skipf("skipping, CLONE_NEWTIME not supported: %v", err)
   486  		}
   487  		t.Fatalf("Cmd failed with err %v, output: %s", err, out)
   488  	}
   489  
   490  	// Inode number of the time namespaces should be different.
   491  	// Based on https://man7.org/linux/man-pages/man7/time_namespaces.7.html#EXAMPLES
   492  	timens, err := os.Readlink("/proc/self/ns/time")
   493  	if err != nil {
   494  		t.Fatal(err)
   495  	}
   496  
   497  	parentTimeNS := timens
   498  	childTimeNS := string(out)
   499  	if childTimeNS == parentTimeNS {
   500  		t.Fatalf("expected child time namespace to be different from parent time namespace: %s", parentTimeNS)
   501  	}
   502  }
   503  
   504  func testPidFD(t *testing.T, userns bool) error {
   505  	if os.Getenv("GO_WANT_HELPER_PROCESS") == "1" {
   506  		// Child: wait for a signal.
   507  		time.Sleep(time.Hour)
   508  	}
   509  
   510  	exe := testenv.Executable(t)
   511  	var pidfd int
   512  	cmd := testenv.Command(t, exe, "-test.run=^TestPidFD$")
   513  	cmd.Env = append(cmd.Environ(), "GO_WANT_HELPER_PROCESS=1")
   514  	cmd.SysProcAttr = &syscall.SysProcAttr{
   515  		PidFD: &pidfd,
   516  	}
   517  	if userns {
   518  		cmd.SysProcAttr.Cloneflags = syscall.CLONE_NEWUSER
   519  	}
   520  	if err := cmd.Start(); err != nil {
   521  		return err
   522  	}
   523  	defer func() {
   524  		cmd.Process.Kill()
   525  		cmd.Wait()
   526  	}()
   527  	t.Log("got pidfd:", pidfd)
   528  	// If pidfd is not supported by the kernel, -1 is returned.
   529  	if pidfd == -1 {
   530  		t.Skip("pidfd not supported")
   531  	}
   532  	defer syscall.Close(pidfd)
   533  
   534  	// Use pidfd to send a signal to the child.
   535  	sig := syscall.SIGINT
   536  	if err := unix.PidFDSendSignal(uintptr(pidfd), sig); err != nil {
   537  		if err != syscall.EINVAL && testenv.SyscallIsNotSupported(err) {
   538  			t.Skip("pidfd_send_signal syscall not supported:", err)
   539  		}
   540  		t.Fatal("pidfd_send_signal syscall failed:", err)
   541  	}
   542  	// Check if the child received our signal.
   543  	err := cmd.Wait()
   544  	if cmd.ProcessState == nil || cmd.ProcessState.Sys().(syscall.WaitStatus).Signal() != sig {
   545  		t.Fatal("unexpected child error:", err)
   546  	}
   547  	return nil
   548  }
   549  
   550  func TestPidFD(t *testing.T) {
   551  	if err := testPidFD(t, false); err != nil {
   552  		t.Fatal("can't start a process:", err)
   553  	}
   554  }
   555  
   556  func TestPidFDWithUserNS(t *testing.T) {
   557  	if err := testPidFD(t, true); err != nil {
   558  		if testenv.SyscallIsNotSupported(err) {
   559  			t.Skip("userns not supported:", err)
   560  		}
   561  		t.Fatal("can't start a process:", err)
   562  	}
   563  }
   564  
   565  func TestPidFDClone3(t *testing.T) {
   566  	*syscall.ForceClone3 = true
   567  	defer func() { *syscall.ForceClone3 = false }()
   568  
   569  	if err := testPidFD(t, false); err != nil {
   570  		if testenv.SyscallIsNotSupported(err) {
   571  			t.Skip("clone3 not supported:", err)
   572  		}
   573  		t.Fatal("can't start a process:", err)
   574  	}
   575  }
   576  
   577  type capHeader struct {
   578  	version uint32
   579  	pid     int32
   580  }
   581  
   582  type capData struct {
   583  	effective   uint32
   584  	permitted   uint32
   585  	inheritable uint32
   586  }
   587  
   588  const CAP_SYS_TIME = 25
   589  const CAP_SYSLOG = 34
   590  
   591  type caps struct {
   592  	hdr  capHeader
   593  	data [2]capData
   594  }
   595  
   596  func getCaps() (caps, error) {
   597  	var c caps
   598  
   599  	// Get capability version
   600  	if _, _, errno := syscall.Syscall(syscall.SYS_CAPGET, uintptr(unsafe.Pointer(&c.hdr)), uintptr(unsafe.Pointer(nil)), 0); errno != 0 {
   601  		return c, fmt.Errorf("SYS_CAPGET: %v", errno)
   602  	}
   603  
   604  	// Get current capabilities
   605  	if _, _, errno := syscall.Syscall(syscall.SYS_CAPGET, uintptr(unsafe.Pointer(&c.hdr)), uintptr(unsafe.Pointer(&c.data[0])), 0); errno != 0 {
   606  		return c, fmt.Errorf("SYS_CAPGET: %v", errno)
   607  	}
   608  
   609  	return c, nil
   610  }
   611  
   612  func TestAmbientCaps(t *testing.T) {
   613  	testAmbientCaps(t, false)
   614  }
   615  
   616  func TestAmbientCapsUserns(t *testing.T) {
   617  	b, err := os.ReadFile("/proc/sys/kernel/apparmor_restrict_unprivileged_userns")
   618  	if err == nil && strings.TrimSpace(string(b)) == "1" {
   619  		t.Skip("AppArmor restriction for unprivileged user namespaces is enabled")
   620  	}
   621  	testAmbientCaps(t, true)
   622  }
   623  
   624  func testAmbientCaps(t *testing.T, userns bool) {
   625  	if os.Getenv("GO_WANT_HELPER_PROCESS") == "1" {
   626  		caps, err := getCaps()
   627  		if err != nil {
   628  			fmt.Fprintln(os.Stderr, err)
   629  			os.Exit(2)
   630  		}
   631  		if caps.data[0].effective&(1<<uint(CAP_SYS_TIME)) == 0 {
   632  			fmt.Fprintln(os.Stderr, "CAP_SYS_TIME unexpectedly not in the effective capability mask")
   633  			os.Exit(2)
   634  		}
   635  		if caps.data[1].effective&(1<<uint(CAP_SYSLOG&31)) == 0 {
   636  			fmt.Fprintln(os.Stderr, "CAP_SYSLOG unexpectedly not in the effective capability mask")
   637  			os.Exit(2)
   638  		}
   639  		os.Exit(0)
   640  	}
   641  
   642  	// skip on android, due to lack of lookup support
   643  	if runtime.GOOS == "android" {
   644  		t.Skip("skipping test on android; see Issue 27327")
   645  	}
   646  
   647  	u, err := user.Lookup("nobody")
   648  	if err != nil {
   649  		t.Fatal(err)
   650  	}
   651  	uid, err := strconv.ParseInt(u.Uid, 0, 32)
   652  	if err != nil {
   653  		t.Fatal(err)
   654  	}
   655  	gid, err := strconv.ParseInt(u.Gid, 0, 32)
   656  	if err != nil {
   657  		t.Fatal(err)
   658  	}
   659  
   660  	// Copy the test binary to a temporary location which is readable by nobody.
   661  	f, err := os.CreateTemp("", "gotest")
   662  	if err != nil {
   663  		t.Fatal(err)
   664  	}
   665  	t.Cleanup(func() {
   666  		f.Close()
   667  		os.Remove(f.Name())
   668  	})
   669  
   670  	exe := testenv.Executable(t)
   671  	e, err := os.Open(exe)
   672  	if err != nil {
   673  		t.Fatal(err)
   674  	}
   675  	defer e.Close()
   676  	if _, err := io.Copy(f, e); err != nil {
   677  		t.Fatal(err)
   678  	}
   679  	if err := f.Chmod(0755); err != nil {
   680  		t.Fatal(err)
   681  	}
   682  	if err := f.Close(); err != nil {
   683  		t.Fatal(err)
   684  	}
   685  
   686  	cmd := testenv.Command(t, f.Name(), "-test.run=^"+t.Name()+"$")
   687  	cmd.Env = append(cmd.Environ(), "GO_WANT_HELPER_PROCESS=1")
   688  	cmd.Stdout = os.Stdout
   689  	cmd.Stderr = os.Stderr
   690  	cmd.SysProcAttr = &syscall.SysProcAttr{
   691  		Credential: &syscall.Credential{
   692  			Uid: uint32(uid),
   693  			Gid: uint32(gid),
   694  		},
   695  		AmbientCaps: []uintptr{CAP_SYS_TIME, CAP_SYSLOG},
   696  	}
   697  	if userns {
   698  		cmd.SysProcAttr.Cloneflags = syscall.CLONE_NEWUSER
   699  		const nobody = 65534
   700  		uid := os.Getuid()
   701  		gid := os.Getgid()
   702  		cmd.SysProcAttr.UidMappings = []syscall.SysProcIDMap{{
   703  			ContainerID: int(nobody),
   704  			HostID:      uid,
   705  			Size:        int(1),
   706  		}}
   707  		cmd.SysProcAttr.GidMappings = []syscall.SysProcIDMap{{
   708  			ContainerID: int(nobody),
   709  			HostID:      gid,
   710  			Size:        int(1),
   711  		}}
   712  
   713  		// Set credentials to run as user and group nobody.
   714  		cmd.SysProcAttr.Credential = &syscall.Credential{
   715  			Uid: nobody,
   716  			Gid: nobody,
   717  		}
   718  	}
   719  	if err := cmd.Run(); err != nil {
   720  		if testenv.SyscallIsNotSupported(err) {
   721  			t.Skipf("skipping: %v: %v", cmd, err)
   722  		}
   723  		t.Fatal(err.Error())
   724  	}
   725  }
   726  

View as plain text