From b8934268a85d792ac0965eb105485331c7afc321 Mon Sep 17 00:00:00 2001 From: Ritesh Harihar Date: Mon, 17 Aug 2026 10:54:10 +0530 Subject: [PATCH 1/6] skip os.Stat for /proc and /sys unveil --- pkg/shim/sandbox.go | 30 +++++++++++++++++++++ pkg/shim/sandbox_test.go | 56 ++++++++++++++++++++++++++++++++++++++++ pkg/shim/z_shim_cmd.go | 14 ++++++++-- 3 files changed, 98 insertions(+), 2 deletions(-) diff --git a/pkg/shim/sandbox.go b/pkg/shim/sandbox.go index 30f0de7..55e2c0d 100644 --- a/pkg/shim/sandbox.go +++ b/pkg/shim/sandbox.go @@ -11,6 +11,27 @@ import ( "github.com/shoenig/go-landlock" ) +// virtualFS lists filesystem roots that are virtual (kernel-generated). +// Their inodes are namespace-scoped: os.Stat on a path like /proc/self/mountinfo +// follows the /proc/self symlink to the shim's own PID inode, which does not +// exist in the task's private namespace after unshare --mount-proc. +// All paths under these roots must be treated as directories without stat(2). +var virtualFS = []string{ + "/proc", + "/sys", +} + +// virtualFSRoot returns the virtual filesystem root that contains path, +// or "" if path is not under any known virtual filesystem. +func virtualFSRoot(path string) string { + for _, root := range virtualFS { + if path == root || strings.HasPrefix(path, root+"/") { + return root + } + } + return "" +} + // 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 +95,15 @@ func convert(elements []string) ([]*landlock.Path, error) { mode := path[0:idx] filepath := path[idx+1:] + // Virtual filesystems (/proc, /sys) have namespace-scoped inodes. + // os.Stat would resolve /proc/self to the shim's PID inode, which + // does not exist in the task's private namespace after unshare --mount-proc. + // Skip stat entirely and register the path as a directory. + if virtualFSRoot(filepath) != "" { + paths = append(paths, landlock.Dir(filepath, 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/sandbox_test.go b/pkg/shim/sandbox_test.go index be1f936..841e423 100644 --- a/pkg/shim/sandbox_test.go +++ b/pkg/shim/sandbox_test.go @@ -6,6 +6,7 @@ package shim import ( "testing" + "github.com/shoenig/go-landlock" "github.com/shoenig/test/must" ) @@ -38,3 +39,58 @@ func Test_split(t *testing.T) { }) } } + +func Test_virtualFSRoot(t *testing.T) { + cases := []struct { + path string + expect string + }{ + {"/proc/self/mountinfo", "/proc"}, + {"/proc/self/cgroup", "/proc"}, + {"/proc/cpuinfo", "/proc"}, + {"/proc", "/proc"}, + {"/sys/fs/cgroup", "/sys"}, + {"/sys", "/sys"}, + {"/etc/passwd", ""}, + {"/procfake", ""}, + {"/usr/local/bin", ""}, + } + for _, tc := range cases { + t.Run(tc.path, func(t *testing.T) { + must.Eq(t, tc.expect, virtualFSRoot(tc.path)) + }) + } +} + +func Test_convert_virtual(t *testing.T) { + // /proc/self/mountinfo must not be stat(2)'d — the shim's PID inode does + // not survive unshare --mount-proc. convert() must emit Dir without error. + paths, err := convert([]string{ + "r:/proc/self/mountinfo", + "r:/proc/cpuinfo", + "r:/sys/fs/cgroup", + }) + must.NoError(t, err) + must.Len(t, 3, paths) + + // All three must be Dir rules (never File), regardless of what they look + // like on the host filesystem. + expected := []*landlock.Path{ + landlock.Dir("/proc/self/mountinfo", "r"), + landlock.Dir("/proc/cpuinfo", "r"), + landlock.Dir("/sys/fs/cgroup", "r"), + } + must.Eq(t, expected, paths) +} + +func Test_convert_missing(t *testing.T) { + // A non-virtual path that doesn't exist on disk must return an error. + _, err := convert([]string{"r:/nonexistent/path/xyz"}) + must.Error(t, err) +} + +func Test_convert_no_mode(t *testing.T) { + // A path with no mode prefix must return an error. + _, err := convert([]string{"/proc/cpuinfo"}) + must.Error(t, err) +} diff --git a/pkg/shim/z_shim_cmd.go b/pkg/shim/z_shim_cmd.go index c09baff..260e249 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 the + // working directory 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 = 1 + } } _ = stdout.Close() From 7e64d79f2a83caf2a878365faf75418f9c491d25 Mon Sep 17 00:00:00 2001 From: Ritesh Harihar Date: Mon, 17 Aug 2026 22:46:01 +0530 Subject: [PATCH 2/6] updated virtualFSRoot logic --- pkg/shim/sandbox.go | 24 ++++++++------ pkg/shim/sandbox_test.go | 56 --------------------------------- pkg/shim/z_shim_cmd.go | 8 ++--- plugin/driver_test.go | 68 ++++++++++++++++++++++++++++++++++++---- 4 files changed, 80 insertions(+), 76 deletions(-) diff --git a/pkg/shim/sandbox.go b/pkg/shim/sandbox.go index 55e2c0d..60cd0e2 100644 --- a/pkg/shim/sandbox.go +++ b/pkg/shim/sandbox.go @@ -95,18 +95,22 @@ func convert(elements []string) ([]*landlock.Path, error) { mode := path[0:idx] filepath := path[idx+1:] - // Virtual filesystems (/proc, /sys) have namespace-scoped inodes. - // os.Stat would resolve /proc/self to the shim's PID inode, which - // does not exist in the task's private namespace after unshare --mount-proc. - // Skip stat entirely and register the path as a directory. - if virtualFSRoot(filepath) != "" { - paths = append(paths, landlock.Dir(filepath, mode)) - continue - } - info, err := os.Stat(filepath) + + // Virtual filesystems (/proc, /sys) have namespace-scoped inodes. + // /proc/self is a magic symlink that resolves to /proc/; + // that inode does not survive unshare --mount-proc into the task's + // private namespace. We ignore stat errors for these paths and fall + // back to File, which is correct for any leaf entry under /proc or + // /sys (e.g. /proc/self/mountinfo). Paths that stat successfully + // (e.g. "/proc", "/sys", "/proc/cpuinfo") take the normal dir/file + // branch below. if err != nil { - return nil, fmt.Errorf("failed to stat unveil path: %w", err) + if virtualFSRoot(filepath) == "" { + return nil, fmt.Errorf("failed to stat unveil path: %w", err) + } + paths = append(paths, landlock.File(filepath, mode)) + continue } if info.IsDir() { diff --git a/pkg/shim/sandbox_test.go b/pkg/shim/sandbox_test.go index 841e423..be1f936 100644 --- a/pkg/shim/sandbox_test.go +++ b/pkg/shim/sandbox_test.go @@ -6,7 +6,6 @@ package shim import ( "testing" - "github.com/shoenig/go-landlock" "github.com/shoenig/test/must" ) @@ -39,58 +38,3 @@ func Test_split(t *testing.T) { }) } } - -func Test_virtualFSRoot(t *testing.T) { - cases := []struct { - path string - expect string - }{ - {"/proc/self/mountinfo", "/proc"}, - {"/proc/self/cgroup", "/proc"}, - {"/proc/cpuinfo", "/proc"}, - {"/proc", "/proc"}, - {"/sys/fs/cgroup", "/sys"}, - {"/sys", "/sys"}, - {"/etc/passwd", ""}, - {"/procfake", ""}, - {"/usr/local/bin", ""}, - } - for _, tc := range cases { - t.Run(tc.path, func(t *testing.T) { - must.Eq(t, tc.expect, virtualFSRoot(tc.path)) - }) - } -} - -func Test_convert_virtual(t *testing.T) { - // /proc/self/mountinfo must not be stat(2)'d — the shim's PID inode does - // not survive unshare --mount-proc. convert() must emit Dir without error. - paths, err := convert([]string{ - "r:/proc/self/mountinfo", - "r:/proc/cpuinfo", - "r:/sys/fs/cgroup", - }) - must.NoError(t, err) - must.Len(t, 3, paths) - - // All three must be Dir rules (never File), regardless of what they look - // like on the host filesystem. - expected := []*landlock.Path{ - landlock.Dir("/proc/self/mountinfo", "r"), - landlock.Dir("/proc/cpuinfo", "r"), - landlock.Dir("/sys/fs/cgroup", "r"), - } - must.Eq(t, expected, paths) -} - -func Test_convert_missing(t *testing.T) { - // A non-virtual path that doesn't exist on disk must return an error. - _, err := convert([]string{"r:/nonexistent/path/xyz"}) - must.Error(t, err) -} - -func Test_convert_no_mode(t *testing.T) { - // A path with no mode prefix must return an error. - _, err := convert([]string{"/proc/cpuinfo"}) - must.Error(t, err) -} diff --git a/pkg/shim/z_shim_cmd.go b/pkg/shim/z_shim_cmd.go index 260e249..bed4b89 100644 --- a/pkg/shim/z_shim_cmd.go +++ b/pkg/shim/z_shim_cmd.go @@ -97,7 +97,7 @@ func init() { // invoke the task command with its args // the environment has already been set for us by the exec2 driver; - // NOMAD_WORK_DIR is set to work_dir if configured, otherwise NOMAD_TASK_DIR + // NOMAD_WORK_DIR is set to work_dir if configured, otherwise NOMAD_TASK_DIR. cmd := exec.Command(cmdpath, commands[1:]...) cmd.Dir = os.Getenv("NOMAD_WORK_DIR") cmd.Stdout = stdout @@ -106,15 +106,15 @@ func init() { var code = 0 if err = cmd.Run(); err != nil { // cmd.Run() can return errors other than *exec.ExitError — for - // example *fs.PathError when chdir into cmd.Dir fails because the - // working directory is not unveiled or does not exist. A bare type + // 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 = 1 + code = subproc.ExitFailure } } diff --git a/plugin/driver_test.go b/plugin/driver_test.go index 1f613d7..2b16465 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,62 @@ 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 — the reported bug. + // Before fix: convert() called os.Stat("/proc/self/mountinfo") which + // followed the /proc/self symlink to the shim PID inode; after + // unshare --mount-proc the task's private /proc had different inodes, + // so Landlock returned EPERM. After fix: stat fails → virtualFSRoot + // fires → File rule registered by path string, survives namespace change. + { + 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. + // Before fix: convert() emitted Dir("/proc/cpuinfo","r") — Landlock + // rejects Dir on a file inode with EINVAL. After fix: stat succeeds, + // IsDir=false → File("/proc/cpuinfo","r") — correct. + { + 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", From 0c84cf266fb563741f66b3921063cb1117ac8219 Mon Sep 17 00:00:00 2001 From: Ritesh Harihar Date: Mon, 17 Aug 2026 23:01:31 +0530 Subject: [PATCH 3/6] updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e71be5..b92c828 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ IMPROVEMENTS: BUG FIXES: +* Fixed `permission denied` when a task reads files under `/proc` or `/sys` (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)] From a748c38a8554f9af0701590f21b103325934d74f Mon Sep 17 00:00:00 2001 From: Ritesh Harihar Date: Tue, 18 Aug 2026 17:55:44 +0530 Subject: [PATCH 4/6] removed /sys --- pkg/shim/sandbox.go | 42 +++++++++++------------------------------- 1 file changed, 11 insertions(+), 31 deletions(-) diff --git a/pkg/shim/sandbox.go b/pkg/shim/sandbox.go index 60cd0e2..5514340 100644 --- a/pkg/shim/sandbox.go +++ b/pkg/shim/sandbox.go @@ -11,25 +11,9 @@ import ( "github.com/shoenig/go-landlock" ) -// virtualFS lists filesystem roots that are virtual (kernel-generated). -// Their inodes are namespace-scoped: os.Stat on a path like /proc/self/mountinfo -// follows the /proc/self symlink to the shim's own PID inode, which does not -// exist in the task's private namespace after unshare --mount-proc. -// All paths under these roots must be treated as directories without stat(2). -var virtualFS = []string{ - "/proc", - "/sys", -} - -// virtualFSRoot returns the virtual filesystem root that contains path, -// or "" if path is not under any known virtual filesystem. -func virtualFSRoot(path string) string { - for _, root := range virtualFS { - if path == root || strings.HasPrefix(path, root+"/") { - return root - } - } - return "" +// isProcPath reports whether path is /proc or a descendant of /proc. +func isProcPath(path string) bool { + return path == "/proc" || strings.HasPrefix(path, "/proc/") } // When the nomad binary is invoked as exec2-shim, the format is @@ -97,20 +81,16 @@ func convert(elements []string) ([]*landlock.Path, error) { info, err := os.Stat(filepath) - // Virtual filesystems (/proc, /sys) have namespace-scoped inodes. - // /proc/self is a magic symlink that resolves to /proc/; - // that inode does not survive unshare --mount-proc into the task's - // private namespace. We ignore stat errors for these paths and fall - // back to File, which is correct for any leaf entry under /proc or - // /sys (e.g. /proc/self/mountinfo). Paths that stat successfully - // (e.g. "/proc", "/sys", "/proc/cpuinfo") take the normal dir/file - // branch below. + // /proc/self/* paths fail to stat in the shim: /proc/self is a magic + // symlink resolved to /proc/, which does not exist after + // unshare --mount-proc. Register by path string so the kernel + // re-resolves it inside the task's private mount namespace. if err != nil { - if virtualFSRoot(filepath) == "" { - return nil, fmt.Errorf("failed to stat unveil path: %w", err) + if isProcPath(filepath) { + paths = append(paths, landlock.File(filepath, mode)) + continue } - paths = append(paths, landlock.File(filepath, mode)) - continue + return nil, fmt.Errorf("failed to stat unveil path: %w", err) } if info.IsDir() { From e65a9c8ad7b1ac372e2dd337a5b60a5e37975138 Mon Sep 17 00:00:00 2001 From: Ritesh Harihar Date: Tue, 18 Aug 2026 18:02:03 +0530 Subject: [PATCH 5/6] updated changelog --- CHANGELOG.md | 2 +- pkg/shim/z_shim_cmd.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b92c828..864559e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ IMPROVEMENTS: BUG FIXES: -* Fixed `permission denied` when a task reads files under `/proc` or `/sys` (e.g. `r:/proc/self/mountinfo`) via an explicit `unveil` entry. [[GH-100](https://github.com/hashicorp/nomad-driver-exec2/pull/100)] +* 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/z_shim_cmd.go b/pkg/shim/z_shim_cmd.go index bed4b89..9a400fe 100644 --- a/pkg/shim/z_shim_cmd.go +++ b/pkg/shim/z_shim_cmd.go @@ -97,7 +97,7 @@ func init() { // invoke the task command with its args // the environment has already been set for us by the exec2 driver; - // NOMAD_WORK_DIR is set to work_dir if configured, otherwise NOMAD_TASK_DIR. + // NOMAD_WORK_DIR is set to work_dir if configured, otherwise NOMAD_TASK_DIR cmd := exec.Command(cmdpath, commands[1:]...) cmd.Dir = os.Getenv("NOMAD_WORK_DIR") cmd.Stdout = stdout From b9315451995513239d13e2715de18a0618ae4bc9 Mon Sep 17 00:00:00 2001 From: Ritesh Harihar Date: Tue, 18 Aug 2026 21:07:17 +0530 Subject: [PATCH 6/6] updated logic in convert() --- pkg/shim/sandbox.go | 27 +++++++++++++++------------ plugin/driver_test.go | 14 ++++---------- 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/pkg/shim/sandbox.go b/pkg/shim/sandbox.go index 5514340..64d8320 100644 --- a/pkg/shim/sandbox.go +++ b/pkg/shim/sandbox.go @@ -11,9 +11,9 @@ import ( "github.com/shoenig/go-landlock" ) -// isProcPath reports whether path is /proc or a descendant of /proc. -func isProcPath(path string) bool { - return path == "/proc" || strings.HasPrefix(path, "/proc/") +// 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 @@ -79,17 +79,20 @@ func convert(elements []string) ([]*landlock.Path, error) { mode := path[0:idx] filepath := path[idx+1:] - info, err := os.Stat(filepath) + // /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 + } - // /proc/self/* paths fail to stat in the shim: /proc/self is a magic - // symlink resolved to /proc/, which does not exist after - // unshare --mount-proc. Register by path string so the kernel - // re-resolves it inside the task's private mount namespace. + info, err := os.Stat(filepath) if err != nil { - if isProcPath(filepath) { - paths = append(paths, landlock.File(filepath, mode)) - continue - } return nil, fmt.Errorf("failed to stat unveil path: %w", err) } diff --git a/plugin/driver_test.go b/plugin/driver_test.go index 2b16465..6a4900e 100644 --- a/plugin/driver_test.go +++ b/plugin/driver_test.go @@ -485,12 +485,8 @@ 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 — the reported bug. - // Before fix: convert() called os.Stat("/proc/self/mountinfo") which - // followed the /proc/self symlink to the shim PID inode; after - // unshare --mount-proc the task's private /proc had different inodes, - // so Landlock returned EPERM. After fix: stat fails → virtualFSRoot - // fires → File rule registered by path string, survives namespace change. + // /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", @@ -502,10 +498,8 @@ func TestFunctional_cases(t *testing.T) { exp: &drivers.ExitResult{ExitCode: 0}, stdoutRe: regexp.MustCompile(`\d+ \d+ \d+:\d+`), // mountinfo line format }, - // /proc/cpuinfo via explicit task unveil. - // Before fix: convert() emitted Dir("/proc/cpuinfo","r") — Landlock - // rejects Dir on a file inode with EINVAL. After fix: stat succeeds, - // IsDir=false → File("/proc/cpuinfo","r") — correct. + // /proc/cpuinfo via explicit task unveil + // IsDir=false → File("/proc/cpuinfo","r") emitted directly. { name: "read /proc/cpuinfo via task unveil", user: "nomad-87000",