Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
17 changes: 17 additions & 0 deletions pkg/shim/sandbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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/<shim-pid>. 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)
Expand Down
14 changes: 12 additions & 2 deletions pkg/shim/z_shim_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package shim

import (
"errors"
"fmt"
"io"
"os"
Expand Down Expand Up @@ -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
}
Comment on lines +112 to +118

@ritesh-harihar ritesh-harihar Aug 18, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not related to this PR.

If cmd.Dir points to an un-unveiled directory, cmd.Run() returns a *fs.PathError from the kernel-level chdir(2), not an *exec.ExitError. This bare type assertion panicked on the wrong type, leaving the allocation stuck. Replaced with errors.As and returns ExitFailure = 1 .

}

_ = stdout.Close()
Expand Down
62 changes: 56 additions & 6 deletions plugin/driver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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
{
Expand Down Expand Up @@ -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",
Expand Down
Loading