diff --git a/.changelog/27717.txt b/.changelog/27717.txt new file mode 100644 index 00000000000..097423e359c --- /dev/null +++ b/.changelog/27717.txt @@ -0,0 +1,3 @@ +```release-note:bug +csi: improve check of StagePublishBaseDir being subdirectory of MountDir +``` diff --git a/helper/funcs.go b/helper/funcs.go index 9b87ef8eceb..2576d4e6fd0 100644 --- a/helper/funcs.go +++ b/helper/funcs.go @@ -568,3 +568,12 @@ func FindExecutableFiles(path string) (map[string]string, error) { } return executables, nil } + +// IsSubdirectory returns true if potentialParent is equal to or a parent directory of path. +func IsSubdirectory(potentialParent, path string) bool { + rel, err := filepath.Rel(potentialParent, path) + if err != nil { + return false + } + return !strings.HasPrefix(rel, "..") +} diff --git a/helper/funcs_test.go b/helper/funcs_test.go index 0d4af67354a..99e046a30d6 100644 --- a/helper/funcs_test.go +++ b/helper/funcs_test.go @@ -528,3 +528,23 @@ func TestFlattenMultiError(t *testing.T) { `, err.Error()) } + +func TestIsSubdirectory(t *testing.T) { + cases := []struct { + potentialParent string + path string + expected bool + }{ + {potentialParent: "/dir", path: "/dir/local", expected: true}, + {potentialParent: "/dir", path: "/other", expected: false}, + {potentialParent: "/dir", path: "/directory", expected: false}, + {potentialParent: "/dir", path: "/directory/../dir/local", expected: true}, + } + + for _, tc := range cases { + t.Run(fmt.Sprintf("parent=%q path=%q", tc.potentialParent, tc.path), func(t *testing.T) { + result := IsSubdirectory(tc.potentialParent, tc.path) + require.Equal(t, tc.expected, result) + }) + } +} diff --git a/nomad/structs/structs.go b/nomad/structs/structs.go index df142bab0dc..5f472105c49 100644 --- a/nomad/structs/structs.go +++ b/nomad/structs/structs.go @@ -8287,7 +8287,7 @@ func (t *Task) Validate(jobType string, tg *TaskGroup) error { } if t.CSIPluginConfig.StagePublishBaseDir != "" && t.CSIPluginConfig.MountDir != "" && - strings.HasPrefix(t.CSIPluginConfig.StagePublishBaseDir, t.CSIPluginConfig.MountDir) { + helper.IsSubdirectory(t.CSIPluginConfig.MountDir, t.CSIPluginConfig.StagePublishBaseDir) { mErr.Errors = append(mErr.Errors, fmt.Errorf("CSIPluginConfig StagePublishBaseDir must not be a subdirectory of MountDir, got: StagePublishBaseDir=\"%s\" MountDir=\"%s\"", t.CSIPluginConfig.StagePublishBaseDir, t.CSIPluginConfig.MountDir)) } diff --git a/nomad/structs/structs_test.go b/nomad/structs/structs_test.go index 6f77ff76b76..989ddc1d5be 100644 --- a/nomad/structs/structs_test.go +++ b/nomad/structs/structs_test.go @@ -3262,6 +3262,15 @@ func TestTask_Validate_CSIPluginConfig(t *testing.T) { }, expectedErr: "CSIPluginConfig StagePublishBaseDir must not be a subdirectory of MountDir, got: StagePublishBaseDir=\"/csi/local\" MountDir=\"/csi\"", }, + { + name: "handles valid staging publish base dir", + pc: &TaskCSIPluginConfig{ + ID: "com.hashicorp.csi", + Type: "monolith", + MountDir: "/csi", + StagePublishBaseDir: "/csilocal", + }, + }, } for _, tt := range table {