diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e71be5..864559e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ IMPROVEMENTS: BUG FIXES: +* Fixed `permission denied` when a task reads files under `/proc` (e.g. `r:/proc/self/mountinfo`) via an explicit `unveil` entry. [[GH-100](https://github.com/hashicorp/nomad-driver-exec2/pull/100)] * Fixed mount propagation so host mounts remain visible while task-internal mounts stay isolated from the host. [[GH-99](https://github.com/hashicorp/nomad-driver-exec2/pull/99)] * Fixed `GOMAXPROCS` to prevent Go workloads from being over-threaded against the host CPU capacity. [[GH-98](https://github.com/hashicorp/nomad-driver-exec2/pull/98)] * Error messages from the `unshare`/`nsenter` shim processes now appear in the allocation logs. [[GH-95](https://github.com/hashicorp/nomad-driver-exec2/pull/95)] diff --git a/pkg/shim/sandbox.go b/pkg/shim/sandbox.go index 30f0de7..64d8320 100644 --- a/pkg/shim/sandbox.go +++ b/pkg/shim/sandbox.go @@ -11,6 +11,11 @@ import ( "github.com/shoenig/go-landlock" ) +// isProcSelfPath reports whether path is a descendant of /proc/self or /proc/thread-self +func isProcSelfPath(path string) bool { + return strings.HasPrefix(path, "/proc/self/") || strings.HasPrefix(path, "/proc/thread-self/") +} + // When the nomad binary is invoked as exec2-shim, the format is // nomad exec2-shim [path, [...]] -- [commands, [...]] // so basically we need to find the first instance of '--' and split on that @@ -74,6 +79,18 @@ func convert(elements []string) ([]*landlock.Path, error) { mode := path[0:idx] filepath := path[idx+1:] + // /proc/self/* and /proc/thread-self/* contain PID-scoped magic symlinks. + // go-landlock registers rules via O_PATH which pins the inode at the + // time of the open — resolving /proc/self to /proc/. After + // unshare --mount-proc the task's private /proc has different inodes, + // making the pinned inode unreachable (EPERM). Promote these paths to + // Dir("/proc", mode) so the rule covers the whole /proc tree by its + // stable directory inode instead. + if isProcSelfPath(filepath) { + paths = append(paths, landlock.Dir("/proc", mode)) + continue + } + info, err := os.Stat(filepath) if err != nil { return nil, fmt.Errorf("failed to stat unveil path: %w", err) diff --git a/pkg/shim/z_shim_cmd.go b/pkg/shim/z_shim_cmd.go index c09baff..9a400fe 100644 --- a/pkg/shim/z_shim_cmd.go +++ b/pkg/shim/z_shim_cmd.go @@ -4,6 +4,7 @@ package shim import ( + "errors" "fmt" "io" "os" @@ -104,8 +105,17 @@ func init() { var code = 0 if err = cmd.Run(); err != nil { - ee := err.(*exec.ExitError) - code = ee.ExitCode() + // cmd.Run() can return errors other than *exec.ExitError — for + // example *fs.PathError when chdir into cmd.Dir fails because an + // explicit work_dir is not unveiled or does not exist. A bare type + // assertion panics in that case; use errors.As instead. + var ee *exec.ExitError + if errors.As(err, &ee) { + code = ee.ExitCode() + } else { + debug("task command failed: %v", err) + code = subproc.ExitFailure + } } _ = stdout.Close() diff --git a/plugin/driver_test.go b/plugin/driver_test.go index 1f613d7..6a4900e 100644 --- a/plugin/driver_test.go +++ b/plugin/driver_test.go @@ -324,21 +324,21 @@ func TestFunctional_cases(t *testing.T) { user: "nomad-80000", command: "/usr/bin/env", unveilDefaults: false, - exp: &drivers.ExitResult{ExitCode: 2}, + exp: &drivers.ExitResult{ExitCode: 1}, }, { name: "run 'env' as nobody without default paths", user: "nobody", command: "/usr/bin/env", unveilDefaults: false, - exp: &drivers.ExitResult{ExitCode: 2}, + exp: &drivers.ExitResult{ExitCode: 1}, }, { name: "run 'env' as root without default paths", user: "root", command: "/usr/bin/env", unveilDefaults: false, - exp: &drivers.ExitResult{ExitCode: 2}, + exp: &drivers.ExitResult{ExitCode: 1}, }, // write to task directory { @@ -376,7 +376,7 @@ func TestFunctional_cases(t *testing.T) { unveilDefaults: false, unveilPaths: []string{"r:/etc/hosts"}, args: []string{"-c", "cp /etc/hosts ${NOMAD_TASK_DIR}"}, - exp: &drivers.ExitResult{ExitCode: 2}, + exp: &drivers.ExitResult{ExitCode: 1}, }, { name: "write to alloc directory no defaults", @@ -385,7 +385,7 @@ func TestFunctional_cases(t *testing.T) { unveilDefaults: false, unveilPaths: []string{"r:/etc/hosts"}, args: []string{"-c", "cp /etc/hosts ${NOMAD_ALLOC_DIR}"}, - exp: &drivers.ExitResult{ExitCode: 2}, + exp: &drivers.ExitResult{ExitCode: 1}, }, { name: "write to secrets directory no defaults", @@ -394,7 +394,7 @@ func TestFunctional_cases(t *testing.T) { unveilDefaults: false, unveilPaths: []string{"r:/etc/hosts"}, args: []string{"-c", "cp /etc/hosts ${NOMAD_SECRETS_DIR}"}, - exp: &drivers.ExitResult{ExitCode: 2}, + exp: &drivers.ExitResult{ExitCode: 1}, }, // dyanmic id { @@ -485,6 +485,56 @@ func TestFunctional_cases(t *testing.T) { unveilByTask: false, // no gate needed — inside sandbox exp: &drivers.ExitResult{ExitCode: 0}, }, + // /proc/self/mountinfo via explicit task unveil + // convert() detects /proc/self/* via isProcSelfPath and promotes the entry to Dir("/proc","r") + { + name: "read /proc/self/mountinfo via task unveil", + user: "nomad-87000", + command: "sh", + args: []string{"-c", "head -1 /proc/self/mountinfo"}, + unveilDefaults: true, + unveilByTask: true, + unveil: []string{"r:/proc/self/mountinfo"}, + exp: &drivers.ExitResult{ExitCode: 0}, + stdoutRe: regexp.MustCompile(`\d+ \d+ \d+:\d+`), // mountinfo line format + }, + // /proc/cpuinfo via explicit task unveil + // IsDir=false → File("/proc/cpuinfo","r") emitted directly. + { + name: "read /proc/cpuinfo via task unveil", + user: "nomad-87000", + command: "sh", + args: []string{"-c", "head -1 /proc/cpuinfo"}, + unveilDefaults: true, + unveilByTask: true, + unveil: []string{"r:/proc/cpuinfo"}, + exp: &drivers.ExitResult{ExitCode: 0}, + stdoutRe: regexp.MustCompile(`.+`), + }, + // Multiple specific /proc paths together — mirrors the exact DSE jobspec. + { + name: "read multiple /proc paths via task unveil", + user: "nomad-87000", + command: "sh", + args: []string{"-c", "head -1 /proc/self/mountinfo && head -1 /proc/cpuinfo && head -1 /proc/meminfo"}, + unveilDefaults: true, + unveilByTask: true, + unveil: []string{"r:/proc/self/mountinfo", "r:/proc/cpuinfo", "r:/proc/meminfo"}, + exp: &drivers.ExitResult{ExitCode: 0}, + }, + // /proc root via task unveil — directory form. os.Stat("/proc") succeeds + // and IsDir=true so Dir("/proc","r") is emitted; all sub-paths accessible. + { + name: "read /proc/self/mountinfo via /proc root unveil", + user: "nomad-87000", + command: "sh", + args: []string{"-c", "head -1 /proc/self/mountinfo && head -1 /proc/cpuinfo"}, + unveilDefaults: true, + unveilByTask: true, + unveil: []string{"r:/proc"}, + exp: &drivers.ExitResult{ExitCode: 0}, + stdoutRe: regexp.MustCompile(`\d+ \d+ \d+:\d+`), + }, // work_dir outside sandbox without unveil_by_task — must be rejected { name: "work_dir outside sandbox rejected without unveil_by_task",