github.com/hernad/nomad@v1.6.112/drivers/shared/executor/executor_linux_test.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package executor
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"os"
    10  	"path/filepath"
    11  	"regexp"
    12  	"strconv"
    13  	"strings"
    14  	"testing"
    15  	"time"
    16  
    17  	"github.com/hernad/nomad/ci"
    18  	"github.com/hernad/nomad/client/allocdir"
    19  	"github.com/hernad/nomad/client/lib/cgutil"
    20  	"github.com/hernad/nomad/client/taskenv"
    21  	"github.com/hernad/nomad/client/testutil"
    22  	"github.com/hernad/nomad/drivers/shared/capabilities"
    23  	"github.com/hernad/nomad/helper/testlog"
    24  	"github.com/hernad/nomad/nomad/mock"
    25  	"github.com/hernad/nomad/plugins/drivers"
    26  	tu "github.com/hernad/nomad/testutil"
    27  	"github.com/opencontainers/runc/libcontainer/cgroups"
    28  	lconfigs "github.com/opencontainers/runc/libcontainer/configs"
    29  	"github.com/opencontainers/runc/libcontainer/devices"
    30  	"github.com/shoenig/test"
    31  	"github.com/shoenig/test/must"
    32  	"github.com/stretchr/testify/require"
    33  	"golang.org/x/sys/unix"
    34  )
    35  
    36  func init() {
    37  	executorFactories["LibcontainerExecutor"] = libcontainerFactory
    38  }
    39  
    40  var libcontainerFactory = executorFactory{
    41  	new: NewExecutorWithIsolation,
    42  	configureExecCmd: func(t *testing.T, cmd *ExecCommand) {
    43  		cmd.ResourceLimits = true
    44  		setupRootfs(t, cmd.TaskDir)
    45  	},
    46  }
    47  
    48  // testExecutorContextWithChroot returns an ExecutorContext and AllocDir with
    49  // chroot. Use testExecutorContext if you don't need a chroot.
    50  //
    51  // The caller is responsible for calling AllocDir.Destroy() to cleanup.
    52  func testExecutorCommandWithChroot(t *testing.T) *testExecCmd {
    53  	chrootEnv := map[string]string{
    54  		"/etc/ld.so.cache":  "/etc/ld.so.cache",
    55  		"/etc/ld.so.conf":   "/etc/ld.so.conf",
    56  		"/etc/ld.so.conf.d": "/etc/ld.so.conf.d",
    57  		"/etc/passwd":       "/etc/passwd",
    58  		"/lib":              "/lib",
    59  		"/lib64":            "/lib64",
    60  		"/usr/lib":          "/usr/lib",
    61  		"/bin/ls":           "/bin/ls",
    62  		"/bin/cat":          "/bin/cat",
    63  		"/bin/echo":         "/bin/echo",
    64  		"/bin/bash":         "/bin/bash",
    65  		"/bin/sleep":        "/bin/sleep",
    66  		"/foobar":           "/does/not/exist",
    67  	}
    68  
    69  	alloc := mock.Alloc()
    70  	task := alloc.Job.TaskGroups[0].Tasks[0]
    71  	taskEnv := taskenv.NewBuilder(mock.Node(), alloc, task, "global").Build()
    72  
    73  	allocDir := allocdir.NewAllocDir(testlog.HCLogger(t), os.TempDir(), alloc.ID)
    74  	if err := allocDir.Build(); err != nil {
    75  		t.Fatalf("AllocDir.Build() failed: %v", err)
    76  	}
    77  	if err := allocDir.NewTaskDir(task.Name).Build(true, chrootEnv); err != nil {
    78  		allocDir.Destroy()
    79  		t.Fatalf("allocDir.NewTaskDir(%q) failed: %v", task.Name, err)
    80  	}
    81  	td := allocDir.TaskDirs[task.Name]
    82  	cmd := &ExecCommand{
    83  		Env:     taskEnv.List(),
    84  		TaskDir: td.Dir,
    85  		Resources: &drivers.Resources{
    86  			NomadResources: alloc.AllocatedResources.Tasks[task.Name],
    87  		},
    88  	}
    89  
    90  	if cgutil.UseV2 {
    91  		cmd.Resources.LinuxResources = &drivers.LinuxResources{
    92  			CpusetCgroupPath: filepath.Join(cgutil.CgroupRoot, "testing.scope", cgutil.CgroupScope(alloc.ID, task.Name)),
    93  		}
    94  	}
    95  
    96  	testCmd := &testExecCmd{
    97  		command:  cmd,
    98  		allocDir: allocDir,
    99  	}
   100  	configureTLogging(t, testCmd)
   101  	return testCmd
   102  }
   103  
   104  func TestExecutor_configureNamespaces(t *testing.T) {
   105  	ci.Parallel(t)
   106  	t.Run("host host", func(t *testing.T) {
   107  		require.Equal(t, lconfigs.Namespaces{
   108  			{Type: lconfigs.NEWNS},
   109  		}, configureNamespaces("host", "host"))
   110  	})
   111  
   112  	t.Run("host private", func(t *testing.T) {
   113  		require.Equal(t, lconfigs.Namespaces{
   114  			{Type: lconfigs.NEWNS},
   115  			{Type: lconfigs.NEWIPC},
   116  		}, configureNamespaces("host", "private"))
   117  	})
   118  
   119  	t.Run("private host", func(t *testing.T) {
   120  		require.Equal(t, lconfigs.Namespaces{
   121  			{Type: lconfigs.NEWNS},
   122  			{Type: lconfigs.NEWPID},
   123  		}, configureNamespaces("private", "host"))
   124  	})
   125  
   126  	t.Run("private private", func(t *testing.T) {
   127  		require.Equal(t, lconfigs.Namespaces{
   128  			{Type: lconfigs.NEWNS},
   129  			{Type: lconfigs.NEWPID},
   130  			{Type: lconfigs.NEWIPC},
   131  		}, configureNamespaces("private", "private"))
   132  	})
   133  }
   134  
   135  func TestExecutor_Isolation_PID_and_IPC_hostMode(t *testing.T) {
   136  	ci.Parallel(t)
   137  	r := require.New(t)
   138  	testutil.ExecCompatible(t)
   139  
   140  	testExecCmd := testExecutorCommandWithChroot(t)
   141  	execCmd, allocDir := testExecCmd.command, testExecCmd.allocDir
   142  	execCmd.Cmd = "/bin/ls"
   143  	execCmd.Args = []string{"-F", "/", "/etc/"}
   144  	defer allocDir.Destroy()
   145  
   146  	execCmd.ResourceLimits = true
   147  	execCmd.ModePID = "host" // disable PID namespace
   148  	execCmd.ModeIPC = "host" // disable IPC namespace
   149  
   150  	executor := NewExecutorWithIsolation(testlog.HCLogger(t), 0)
   151  	defer executor.Shutdown("SIGKILL", 0)
   152  
   153  	ps, err := executor.Launch(execCmd)
   154  	r.NoError(err)
   155  	r.NotZero(ps.Pid)
   156  
   157  	estate, err := executor.Wait(context.Background())
   158  	r.NoError(err)
   159  	r.Zero(estate.ExitCode)
   160  
   161  	lexec, ok := executor.(*LibcontainerExecutor)
   162  	r.True(ok)
   163  
   164  	// Check that namespaces were applied to the container config
   165  	config := lexec.container.Config()
   166  
   167  	r.Contains(config.Namespaces, lconfigs.Namespace{Type: lconfigs.NEWNS})
   168  	r.NotContains(config.Namespaces, lconfigs.Namespace{Type: lconfigs.NEWPID})
   169  	r.NotContains(config.Namespaces, lconfigs.Namespace{Type: lconfigs.NEWIPC})
   170  
   171  	// Shut down executor
   172  	r.NoError(executor.Shutdown("", 0))
   173  	executor.Wait(context.Background())
   174  }
   175  
   176  func TestExecutor_IsolationAndConstraints(t *testing.T) {
   177  	ci.Parallel(t)
   178  	testutil.ExecCompatible(t)
   179  	testutil.CgroupsCompatibleV1(t) // todo(shoenig): hard codes cgroups v1 lookup
   180  
   181  	r := require.New(t)
   182  
   183  	testExecCmd := testExecutorCommandWithChroot(t)
   184  	execCmd, allocDir := testExecCmd.command, testExecCmd.allocDir
   185  	execCmd.Cmd = "/bin/ls"
   186  	execCmd.Args = []string{"-F", "/", "/etc/"}
   187  	defer allocDir.Destroy()
   188  
   189  	execCmd.ResourceLimits = true
   190  	execCmd.ModePID = "private"
   191  	execCmd.ModeIPC = "private"
   192  
   193  	executor := NewExecutorWithIsolation(testlog.HCLogger(t), 0)
   194  	defer executor.Shutdown("SIGKILL", 0)
   195  
   196  	ps, err := executor.Launch(execCmd)
   197  	r.NoError(err)
   198  	r.NotZero(ps.Pid)
   199  
   200  	estate, err := executor.Wait(context.Background())
   201  	r.NoError(err)
   202  	r.Zero(estate.ExitCode)
   203  
   204  	lexec, ok := executor.(*LibcontainerExecutor)
   205  	r.True(ok)
   206  
   207  	// Check if the resource constraints were applied
   208  	state, err := lexec.container.State()
   209  	r.NoError(err)
   210  
   211  	memLimits := filepath.Join(state.CgroupPaths["memory"], "memory.limit_in_bytes")
   212  	data, err := os.ReadFile(memLimits)
   213  	r.NoError(err)
   214  
   215  	expectedMemLim := strconv.Itoa(int(execCmd.Resources.NomadResources.Memory.MemoryMB * 1024 * 1024))
   216  	actualMemLim := strings.TrimSpace(string(data))
   217  	r.Equal(actualMemLim, expectedMemLim)
   218  
   219  	// Check that namespaces were applied to the container config
   220  	config := lexec.container.Config()
   221  
   222  	r.Contains(config.Namespaces, lconfigs.Namespace{Type: lconfigs.NEWNS})
   223  	r.Contains(config.Namespaces, lconfigs.Namespace{Type: lconfigs.NEWPID})
   224  	r.Contains(config.Namespaces, lconfigs.Namespace{Type: lconfigs.NEWIPC})
   225  
   226  	// Shut down executor
   227  	r.NoError(executor.Shutdown("", 0))
   228  	executor.Wait(context.Background())
   229  
   230  	// Check if Nomad has actually removed the cgroups
   231  	tu.WaitForResult(func() (bool, error) {
   232  		_, err = os.Stat(memLimits)
   233  		if err == nil {
   234  			return false, fmt.Errorf("expected an error from os.Stat %s", memLimits)
   235  		}
   236  		return true, nil
   237  	}, func(err error) { t.Error(err) })
   238  
   239  	expected := `/:
   240  alloc/
   241  bin/
   242  dev/
   243  etc/
   244  lib/
   245  lib64/
   246  local/
   247  private/
   248  proc/
   249  secrets/
   250  sys/
   251  tmp/
   252  usr/
   253  
   254  /etc/:
   255  ld.so.cache
   256  ld.so.conf
   257  ld.so.conf.d/
   258  passwd`
   259  	tu.WaitForResult(func() (bool, error) {
   260  		output := testExecCmd.stdout.String()
   261  		act := strings.TrimSpace(string(output))
   262  		if act != expected {
   263  			return false, fmt.Errorf("Command output incorrectly: want %v; got %v", expected, act)
   264  		}
   265  		return true, nil
   266  	}, func(err error) { t.Error(err) })
   267  }
   268  
   269  // TestExecutor_CgroupPaths asserts that process starts with independent cgroups
   270  // hierarchy created for this process
   271  func TestExecutor_CgroupPaths(t *testing.T) {
   272  	ci.Parallel(t)
   273  	testutil.ExecCompatible(t)
   274  
   275  	require := require.New(t)
   276  
   277  	testExecCmd := testExecutorCommandWithChroot(t)
   278  	execCmd, allocDir := testExecCmd.command, testExecCmd.allocDir
   279  	execCmd.Cmd = "/bin/bash"
   280  	execCmd.Args = []string{"-c", "sleep 0.2; cat /proc/self/cgroup"}
   281  	defer allocDir.Destroy()
   282  
   283  	execCmd.ResourceLimits = true
   284  
   285  	executor := NewExecutorWithIsolation(testlog.HCLogger(t), 0)
   286  	defer executor.Shutdown("SIGKILL", 0)
   287  
   288  	ps, err := executor.Launch(execCmd)
   289  	require.NoError(err)
   290  	require.NotZero(ps.Pid)
   291  
   292  	state, err := executor.Wait(context.Background())
   293  	require.NoError(err)
   294  	require.Zero(state.ExitCode)
   295  
   296  	tu.WaitForResult(func() (bool, error) {
   297  		output := strings.TrimSpace(testExecCmd.stdout.String())
   298  		switch cgutil.UseV2 {
   299  		case true:
   300  			isScope := strings.HasSuffix(output, ".scope")
   301  			require.True(isScope)
   302  		case false:
   303  			// Verify that we got some cgroups
   304  			if !strings.Contains(output, ":devices:") {
   305  				return false, fmt.Errorf("was expected cgroup files but found:\n%v", output)
   306  			}
   307  			lines := strings.Split(output, "\n")
   308  			for _, line := range lines {
   309  				// Every cgroup entry should be /nomad/$ALLOC_ID
   310  				if line == "" {
   311  					continue
   312  				}
   313  
   314  				// Skip rdma & misc subsystem; rdma was added in most recent kernels and libcontainer/docker
   315  				// don't isolate it by default.
   316  				// :: filters out odd empty cgroup found in latest Ubuntu lines, e.g. 0::/user.slice/user-1000.slice/session-17.scope
   317  				// that is also not used for isolation
   318  				if strings.Contains(line, ":rdma:") || strings.Contains(line, ":misc:") || strings.Contains(line, "::") {
   319  					continue
   320  				}
   321  				if !strings.Contains(line, ":/nomad/") {
   322  					return false, fmt.Errorf("Not a member of the alloc's cgroup: expected=...:/nomad/... -- found=%q", line)
   323  				}
   324  
   325  			}
   326  		}
   327  		return true, nil
   328  	}, func(err error) { t.Error(err) })
   329  }
   330  
   331  // TestExecutor_CgroupPaths asserts that all cgroups created for a task
   332  // are destroyed on shutdown
   333  func TestExecutor_CgroupPathsAreDestroyed(t *testing.T) {
   334  	ci.Parallel(t)
   335  	testutil.ExecCompatible(t)
   336  
   337  	require := require.New(t)
   338  
   339  	testExecCmd := testExecutorCommandWithChroot(t)
   340  	execCmd, allocDir := testExecCmd.command, testExecCmd.allocDir
   341  	execCmd.Cmd = "/bin/bash"
   342  	execCmd.Args = []string{"-c", "sleep 0.2; cat /proc/self/cgroup"}
   343  	defer allocDir.Destroy()
   344  
   345  	execCmd.ResourceLimits = true
   346  
   347  	executor := NewExecutorWithIsolation(testlog.HCLogger(t), 0)
   348  	defer executor.Shutdown("SIGKILL", 0)
   349  
   350  	ps, err := executor.Launch(execCmd)
   351  	require.NoError(err)
   352  	require.NotZero(ps.Pid)
   353  
   354  	state, err := executor.Wait(context.Background())
   355  	require.NoError(err)
   356  	require.Zero(state.ExitCode)
   357  
   358  	var cgroupsPaths string
   359  	tu.WaitForResult(func() (bool, error) {
   360  		output := strings.TrimSpace(testExecCmd.stdout.String())
   361  
   362  		switch cgutil.UseV2 {
   363  		case true:
   364  			isScope := strings.HasSuffix(output, ".scope")
   365  			require.True(isScope)
   366  		case false:
   367  			// Verify that we got some cgroups
   368  			if !strings.Contains(output, ":devices:") {
   369  				return false, fmt.Errorf("was expected cgroup files but found:\n%v", output)
   370  			}
   371  			lines := strings.Split(output, "\n")
   372  			for _, line := range lines {
   373  				// Every cgroup entry should be /nomad/$ALLOC_ID
   374  				if line == "" {
   375  					continue
   376  				}
   377  
   378  				// Skip rdma subsystem; rdma was added in most recent kernels and libcontainer/docker
   379  				// don't isolate it by default. And also misc.
   380  				if strings.Contains(line, ":rdma:") || strings.Contains(line, "::") || strings.Contains(line, ":misc:") {
   381  					continue
   382  				}
   383  
   384  				if !strings.Contains(line, ":/nomad/") {
   385  					return false, fmt.Errorf("Not a member of the alloc's cgroup: expected=...:/nomad/... -- found=%q", line)
   386  				}
   387  			}
   388  		}
   389  		cgroupsPaths = output
   390  		return true, nil
   391  	}, func(err error) { t.Error(err) })
   392  
   393  	// shutdown executor and test that cgroups are destroyed
   394  	executor.Shutdown("SIGKILL", 0)
   395  
   396  	// test that the cgroup paths are not visible
   397  	tmpFile, err := os.CreateTemp("", "")
   398  	require.NoError(err)
   399  	defer os.Remove(tmpFile.Name())
   400  
   401  	_, err = tmpFile.WriteString(cgroupsPaths)
   402  	require.NoError(err)
   403  	tmpFile.Close()
   404  
   405  	subsystems, err := cgroups.ParseCgroupFile(tmpFile.Name())
   406  	require.NoError(err)
   407  
   408  	for subsystem, cgroup := range subsystems {
   409  		if subsystem == "" || !strings.Contains(cgroup, "nomad/") {
   410  			continue
   411  		}
   412  		p, err := cgutil.GetCgroupPathHelperV1(subsystem, cgroup)
   413  		require.NoError(err)
   414  		require.Falsef(cgroups.PathExists(p), "cgroup for %s %s still exists", subsystem, cgroup)
   415  	}
   416  }
   417  
   418  func TestExecutor_LookupTaskBin(t *testing.T) {
   419  	ci.Parallel(t)
   420  
   421  	// Create a temp dir
   422  	taskDir := t.TempDir()
   423  	mountDir := t.TempDir()
   424  
   425  	// Create the command with mounts
   426  	cmd := &ExecCommand{
   427  		Env:     []string{"PATH=/bin"},
   428  		TaskDir: taskDir,
   429  		Mounts:  []*drivers.MountConfig{{TaskPath: "/srv", HostPath: mountDir}},
   430  	}
   431  
   432  	// Make a /foo /local/foo and /usr/local/bin subdirs under task dir
   433  	// and /bar under mountdir
   434  	must.NoError(t, os.MkdirAll(filepath.Join(taskDir, "foo"), 0700))
   435  	must.NoError(t, os.MkdirAll(filepath.Join(taskDir, "local/foo"), 0700))
   436  	must.NoError(t, os.MkdirAll(filepath.Join(taskDir, "usr/local/bin"), 0700))
   437  	must.NoError(t, os.MkdirAll(filepath.Join(mountDir, "bar"), 0700))
   438  
   439  	writeFile := func(paths ...string) {
   440  		t.Helper()
   441  		path := filepath.Join(paths...)
   442  		must.NoError(t, os.WriteFile(path, []byte("hello"), 0o700))
   443  	}
   444  
   445  	// Write some files
   446  	writeFile(taskDir, "usr/local/bin", "tmp0.txt") // under /usr/local/bin in taskdir
   447  	writeFile(taskDir, "foo", "tmp1.txt")           // under foo in taskdir
   448  	writeFile(taskDir, "local", "tmp2.txt")         // under root of task-local dir
   449  	writeFile(taskDir, "local/foo", "tmp3.txt")     // under foo in task-local dir
   450  	writeFile(mountDir, "tmp4.txt")                 // under root of mount dir
   451  	writeFile(mountDir, "bar/tmp5.txt")             // under bar in mount dir
   452  
   453  	testCases := []struct {
   454  		name           string
   455  		cmd            string
   456  		expectErr      string
   457  		expectTaskPath string
   458  		expectHostPath string
   459  	}{
   460  		{
   461  			name:           "lookup with file name in PATH",
   462  			cmd:            "tmp0.txt",
   463  			expectTaskPath: "/usr/local/bin/tmp0.txt",
   464  			expectHostPath: filepath.Join(taskDir, "usr/local/bin/tmp0.txt"),
   465  		},
   466  		{
   467  			name:           "lookup with absolute path to binary",
   468  			cmd:            "/foo/tmp1.txt",
   469  			expectTaskPath: "/foo/tmp1.txt",
   470  			expectHostPath: filepath.Join(taskDir, "foo/tmp1.txt"),
   471  		},
   472  		{
   473  			name:           "lookup in task local dir with absolute path to binary",
   474  			cmd:            "/local/tmp2.txt",
   475  			expectTaskPath: "/local/tmp2.txt",
   476  			expectHostPath: filepath.Join(taskDir, "local/tmp2.txt"),
   477  		},
   478  		{
   479  			name:           "lookup in task local dir with relative path to binary",
   480  			cmd:            "local/tmp2.txt",
   481  			expectTaskPath: "/local/tmp2.txt",
   482  			expectHostPath: filepath.Join(taskDir, "local/tmp2.txt"),
   483  		},
   484  		{
   485  			name:           "lookup in task local dir with file name",
   486  			cmd:            "tmp2.txt",
   487  			expectTaskPath: "/local/tmp2.txt",
   488  			expectHostPath: filepath.Join(taskDir, "local/tmp2.txt"),
   489  		},
   490  		{
   491  			name:           "lookup in task local subdir with absolute path to binary",
   492  			cmd:            "/local/foo/tmp3.txt",
   493  			expectTaskPath: "/local/foo/tmp3.txt",
   494  			expectHostPath: filepath.Join(taskDir, "local/foo/tmp3.txt"),
   495  		},
   496  		{
   497  			name:      "lookup host absolute path outside taskdir",
   498  			cmd:       "/bin/sh",
   499  			expectErr: "file /bin/sh not found under path " + taskDir,
   500  		},
   501  		{
   502  			name:           "lookup file from mount with absolute path",
   503  			cmd:            "/srv/tmp4.txt",
   504  			expectTaskPath: "/srv/tmp4.txt",
   505  			expectHostPath: filepath.Join(mountDir, "tmp4.txt"),
   506  		},
   507  		{
   508  			name:      "lookup file from mount with file name fails",
   509  			cmd:       "tmp4.txt",
   510  			expectErr: "file tmp4.txt not found under path",
   511  		},
   512  		{
   513  			name:           "lookup file from mount with subdir",
   514  			cmd:            "/srv/bar/tmp5.txt",
   515  			expectTaskPath: "/srv/bar/tmp5.txt",
   516  			expectHostPath: filepath.Join(mountDir, "bar/tmp5.txt"),
   517  		},
   518  	}
   519  
   520  	for _, tc := range testCases {
   521  		t.Run(tc.name, func(t *testing.T) {
   522  			cmd.Cmd = tc.cmd
   523  			taskPath, hostPath, err := lookupTaskBin(cmd)
   524  			if tc.expectErr == "" {
   525  				must.NoError(t, err)
   526  				test.Eq(t, tc.expectTaskPath, taskPath)
   527  				test.Eq(t, tc.expectHostPath, hostPath)
   528  			} else {
   529  				test.EqError(t, err, tc.expectErr)
   530  			}
   531  		})
   532  	}
   533  }
   534  
   535  // Exec Launch looks for the binary only inside the chroot
   536  func TestExecutor_EscapeContainer(t *testing.T) {
   537  	ci.Parallel(t)
   538  	testutil.ExecCompatible(t)
   539  	testutil.CgroupsCompatibleV1(t) // todo(shoenig) kills the terminal, probably defaulting to /
   540  
   541  	require := require.New(t)
   542  
   543  	testExecCmd := testExecutorCommandWithChroot(t)
   544  	execCmd, allocDir := testExecCmd.command, testExecCmd.allocDir
   545  	execCmd.Cmd = "/bin/kill" // missing from the chroot container
   546  	defer allocDir.Destroy()
   547  
   548  	execCmd.ResourceLimits = true
   549  
   550  	executor := NewExecutorWithIsolation(testlog.HCLogger(t), 0)
   551  	defer executor.Shutdown("SIGKILL", 0)
   552  
   553  	_, err := executor.Launch(execCmd)
   554  	require.Error(err)
   555  	require.Regexp("^file /bin/kill not found under path", err)
   556  
   557  	// Bare files are looked up using the system path, inside the container
   558  	allocDir.Destroy()
   559  	testExecCmd = testExecutorCommandWithChroot(t)
   560  	execCmd, allocDir = testExecCmd.command, testExecCmd.allocDir
   561  	execCmd.Cmd = "kill"
   562  	_, err = executor.Launch(execCmd)
   563  	require.Error(err)
   564  	require.Regexp("^file kill not found under path", err)
   565  
   566  	allocDir.Destroy()
   567  	testExecCmd = testExecutorCommandWithChroot(t)
   568  	execCmd, allocDir = testExecCmd.command, testExecCmd.allocDir
   569  	execCmd.Cmd = "echo"
   570  	_, err = executor.Launch(execCmd)
   571  	require.NoError(err)
   572  }
   573  
   574  // TestExecutor_DoesNotInheritOomScoreAdj asserts that the exec processes do not
   575  // inherit the oom_score_adj value of Nomad agent/executor process
   576  func TestExecutor_DoesNotInheritOomScoreAdj(t *testing.T) {
   577  	ci.Parallel(t)
   578  	testutil.ExecCompatible(t)
   579  
   580  	oomPath := "/proc/self/oom_score_adj"
   581  	origValue, err := os.ReadFile(oomPath)
   582  	require.NoError(t, err, "reading oom_score_adj")
   583  
   584  	err = os.WriteFile(oomPath, []byte("-100"), 0644)
   585  	require.NoError(t, err, "setting temporary oom_score_adj")
   586  
   587  	defer func() {
   588  		err := os.WriteFile(oomPath, origValue, 0644)
   589  		require.NoError(t, err, "restoring oom_score_adj")
   590  	}()
   591  
   592  	testExecCmd := testExecutorCommandWithChroot(t)
   593  	execCmd, allocDir := testExecCmd.command, testExecCmd.allocDir
   594  	defer allocDir.Destroy()
   595  
   596  	execCmd.ResourceLimits = true
   597  	execCmd.Cmd = "/bin/bash"
   598  	execCmd.Args = []string{"-c", "cat /proc/self/oom_score_adj"}
   599  
   600  	executor := NewExecutorWithIsolation(testlog.HCLogger(t), 0)
   601  	defer executor.Shutdown("SIGKILL", 0)
   602  
   603  	_, err = executor.Launch(execCmd)
   604  	require.NoError(t, err)
   605  
   606  	ch := make(chan interface{})
   607  	go func() {
   608  		executor.Wait(context.Background())
   609  		close(ch)
   610  	}()
   611  
   612  	select {
   613  	case <-ch:
   614  		// all good
   615  	case <-time.After(5 * time.Second):
   616  		require.Fail(t, "timeout waiting for exec to shutdown")
   617  	}
   618  
   619  	expected := "0"
   620  	tu.WaitForResult(func() (bool, error) {
   621  		output := strings.TrimSpace(testExecCmd.stdout.String())
   622  		if output != expected {
   623  			return false, fmt.Errorf("oom_score_adj didn't match: want\n%v\n; got:\n%v\n", expected, output)
   624  		}
   625  		return true, nil
   626  	}, func(err error) { require.NoError(t, err) })
   627  
   628  }
   629  
   630  func TestExecutor_Capabilities(t *testing.T) {
   631  	ci.Parallel(t)
   632  	testutil.ExecCompatible(t)
   633  
   634  	cases := []struct {
   635  		user         string
   636  		capAdd       []string
   637  		capDrop      []string
   638  		capsExpected string
   639  	}{
   640  		{
   641  			user: "nobody",
   642  			capsExpected: `
   643  CapInh: 00000000a80405fb
   644  CapPrm: 00000000a80405fb
   645  CapEff: 00000000a80405fb
   646  CapBnd: 00000000a80405fb
   647  CapAmb: 00000000a80405fb`,
   648  		},
   649  		{
   650  			user: "root",
   651  			capsExpected: `
   652  CapInh: 0000000000000000
   653  CapPrm: 0000003fffffffff
   654  CapEff: 0000003fffffffff
   655  CapBnd: 0000003fffffffff
   656  CapAmb: 0000000000000000`,
   657  		},
   658  		{
   659  			user:    "nobody",
   660  			capDrop: []string{"all"},
   661  			capAdd:  []string{"net_bind_service"},
   662  			capsExpected: `
   663  CapInh: 0000000000000400
   664  CapPrm: 0000000000000400
   665  CapEff: 0000000000000400
   666  CapBnd: 0000000000000400
   667  CapAmb: 0000000000000400`,
   668  		},
   669  	}
   670  
   671  	for _, c := range cases {
   672  		t.Run(c.user, func(t *testing.T) {
   673  
   674  			testExecCmd := testExecutorCommandWithChroot(t)
   675  			execCmd, allocDir := testExecCmd.command, testExecCmd.allocDir
   676  			defer allocDir.Destroy()
   677  
   678  			execCmd.User = c.user
   679  			execCmd.ResourceLimits = true
   680  			execCmd.Cmd = "/bin/bash"
   681  			execCmd.Args = []string{"-c", "cat /proc/$$/status"}
   682  
   683  			capsBasis := capabilities.NomadDefaults()
   684  			capsAllowed := capsBasis.Slice(true)
   685  			if c.capDrop != nil || c.capAdd != nil {
   686  				calcCaps, err := capabilities.Calculate(
   687  					capsBasis, capsAllowed, c.capAdd, c.capDrop)
   688  				require.NoError(t, err)
   689  				execCmd.Capabilities = calcCaps
   690  			} else {
   691  				execCmd.Capabilities = capsAllowed
   692  			}
   693  
   694  			executor := NewExecutorWithIsolation(testlog.HCLogger(t), 0)
   695  			defer executor.Shutdown("SIGKILL", 0)
   696  
   697  			_, err := executor.Launch(execCmd)
   698  			require.NoError(t, err)
   699  
   700  			ch := make(chan interface{})
   701  			go func() {
   702  				executor.Wait(context.Background())
   703  				close(ch)
   704  			}()
   705  
   706  			select {
   707  			case <-ch:
   708  				// all good
   709  			case <-time.After(5 * time.Second):
   710  				require.Fail(t, "timeout waiting for exec to shutdown")
   711  			}
   712  
   713  			canonical := func(s string) string {
   714  				s = strings.TrimSpace(s)
   715  				s = regexp.MustCompile("[ \t]+").ReplaceAllString(s, " ")
   716  				s = regexp.MustCompile("[\n\r]+").ReplaceAllString(s, "\n")
   717  				return s
   718  			}
   719  
   720  			expected := canonical(c.capsExpected)
   721  			tu.WaitForResult(func() (bool, error) {
   722  				output := canonical(testExecCmd.stdout.String())
   723  				if !strings.Contains(output, expected) {
   724  					return false, fmt.Errorf("capabilities didn't match: want\n%v\n; got:\n%v\n", expected, output)
   725  				}
   726  				return true, nil
   727  			}, func(err error) { require.NoError(t, err) })
   728  		})
   729  	}
   730  
   731  }
   732  
   733  func TestExecutor_ClientCleanup(t *testing.T) {
   734  	ci.Parallel(t)
   735  	testutil.ExecCompatible(t)
   736  	require := require.New(t)
   737  
   738  	testExecCmd := testExecutorCommandWithChroot(t)
   739  	execCmd, allocDir := testExecCmd.command, testExecCmd.allocDir
   740  	defer allocDir.Destroy()
   741  
   742  	executor := NewExecutorWithIsolation(testlog.HCLogger(t), 0)
   743  	defer executor.Shutdown("", 0)
   744  
   745  	// Need to run a command which will produce continuous output but not
   746  	// too quickly to ensure executor.Exit() stops the process.
   747  	execCmd.Cmd = "/bin/bash"
   748  	execCmd.Args = []string{"-c", "while true; do /bin/echo X; /bin/sleep 1; done"}
   749  	execCmd.ResourceLimits = true
   750  
   751  	ps, err := executor.Launch(execCmd)
   752  
   753  	require.NoError(err)
   754  	require.NotZero(ps.Pid)
   755  	time.Sleep(500 * time.Millisecond)
   756  	require.NoError(executor.Shutdown("SIGINT", 100*time.Millisecond))
   757  
   758  	ch := make(chan interface{})
   759  	go func() {
   760  		executor.Wait(context.Background())
   761  		close(ch)
   762  	}()
   763  
   764  	select {
   765  	case <-ch:
   766  		// all good
   767  	case <-time.After(5 * time.Second):
   768  		require.Fail("timeout waiting for exec to shutdown")
   769  	}
   770  
   771  	output := testExecCmd.stdout.String()
   772  	require.NotZero(len(output))
   773  	time.Sleep(2 * time.Second)
   774  	output1 := testExecCmd.stdout.String()
   775  	require.Equal(len(output), len(output1))
   776  }
   777  
   778  func TestExecutor_cmdDevices(t *testing.T) {
   779  	ci.Parallel(t)
   780  	input := []*drivers.DeviceConfig{
   781  		{
   782  			HostPath:    "/dev/null",
   783  			TaskPath:    "/task/dev/null",
   784  			Permissions: "rwm",
   785  		},
   786  	}
   787  
   788  	expected := &devices.Device{
   789  		Rule: devices.Rule{
   790  			Type:        99,
   791  			Major:       1,
   792  			Minor:       3,
   793  			Permissions: "rwm",
   794  		},
   795  		Path: "/task/dev/null",
   796  	}
   797  
   798  	found, err := cmdDevices(input)
   799  	require.NoError(t, err)
   800  	require.Len(t, found, 1)
   801  
   802  	// ignore file permission and ownership
   803  	// as they are host specific potentially
   804  	d := found[0]
   805  	d.FileMode = 0
   806  	d.Uid = 0
   807  	d.Gid = 0
   808  
   809  	require.EqualValues(t, expected, d)
   810  }
   811  
   812  func TestExecutor_cmdMounts(t *testing.T) {
   813  	ci.Parallel(t)
   814  	input := []*drivers.MountConfig{
   815  		{
   816  			HostPath: "/host/path-ro",
   817  			TaskPath: "/task/path-ro",
   818  			Readonly: true,
   819  		},
   820  		{
   821  			HostPath: "/host/path-rw",
   822  			TaskPath: "/task/path-rw",
   823  			Readonly: false,
   824  		},
   825  	}
   826  
   827  	expected := []*lconfigs.Mount{
   828  		{
   829  			Source:           "/host/path-ro",
   830  			Destination:      "/task/path-ro",
   831  			Flags:            unix.MS_BIND | unix.MS_RDONLY,
   832  			Device:           "bind",
   833  			PropagationFlags: []int{unix.MS_PRIVATE | unix.MS_REC},
   834  		},
   835  		{
   836  			Source:           "/host/path-rw",
   837  			Destination:      "/task/path-rw",
   838  			Flags:            unix.MS_BIND,
   839  			Device:           "bind",
   840  			PropagationFlags: []int{unix.MS_PRIVATE | unix.MS_REC},
   841  		},
   842  	}
   843  
   844  	require.EqualValues(t, expected, cmdMounts(input))
   845  }
   846  
   847  // TestUniversalExecutor_NoCgroup asserts that commands are executed in the
   848  // same cgroup as parent process
   849  func TestUniversalExecutor_NoCgroup(t *testing.T) {
   850  	ci.Parallel(t)
   851  	testutil.ExecCompatible(t)
   852  
   853  	expectedBytes, err := os.ReadFile("/proc/self/cgroup")
   854  	require.NoError(t, err)
   855  
   856  	expected := strings.TrimSpace(string(expectedBytes))
   857  
   858  	testExecCmd := testExecutorCommand(t)
   859  	execCmd, allocDir := testExecCmd.command, testExecCmd.allocDir
   860  	execCmd.Cmd = "/bin/cat"
   861  	execCmd.Args = []string{"/proc/self/cgroup"}
   862  	defer allocDir.Destroy()
   863  
   864  	execCmd.BasicProcessCgroup = false
   865  	execCmd.ResourceLimits = false
   866  
   867  	executor := NewExecutor(testlog.HCLogger(t), 0)
   868  	defer executor.Shutdown("SIGKILL", 0)
   869  
   870  	_, err = executor.Launch(execCmd)
   871  	require.NoError(t, err)
   872  
   873  	_, err = executor.Wait(context.Background())
   874  	require.NoError(t, err)
   875  
   876  	tu.WaitForResult(func() (bool, error) {
   877  		act := strings.TrimSpace(string(testExecCmd.stdout.String()))
   878  		if expected != act {
   879  			return false, fmt.Errorf("expected:\n%s actual:\n%s", expected, act)
   880  		}
   881  		return true, nil
   882  	}, func(err error) {
   883  		stderr := strings.TrimSpace(string(testExecCmd.stderr.String()))
   884  		t.Logf("stderr: %v", stderr)
   885  		require.NoError(t, err)
   886  	})
   887  
   888  }