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 }