From df0c005af854496789ade43e19e8671aeb17e4ed Mon Sep 17 00:00:00 2001 From: User Date: Sun, 22 Feb 2026 04:41:17 +0800 Subject: [PATCH] fix: validate volume mount source paths before spawning detached process Add pre-flight validation in processVolumeMounts() to check that source paths exist on the host filesystem before proceeding. This catches invalid volume mounts early with a clear error message, instead of silently failing in the detached process where the error is only visible in log files. The validation: - Uses os.Stat() (follows symlinks) to check path existence - Resolves relative paths to absolute before checking - Skips validation for Kubernetes operator context (paths are container-relative) - Skips validation for resource:// URIs Fixes #2485 --- pkg/runner/config_builder.go | 18 ++++++++++++++++++ pkg/runner/config_builder_test.go | 25 +++++++++++++++++++------ 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/pkg/runner/config_builder.go b/pkg/runner/config_builder.go index 289abcc64d..49e99e30a3 100644 --- a/pkg/runner/config_builder.go +++ b/pkg/runner/config_builder.go @@ -9,6 +9,8 @@ import ( "log/slog" "maps" "net/url" + "os" + "path/filepath" "slices" "strings" @@ -1042,6 +1044,22 @@ func (b *runConfigBuilder) processVolumeMounts() error { return fmt.Errorf("invalid volume format: %s (%w)", volume, err) } + // Validate source path exists on the host filesystem (CLI context only). + // Skip for Kubernetes operator context where paths are container-relative, + // and for resource URIs which are not filesystem paths. + if b.buildContext != BuildContextOperator && !strings.HasPrefix(source, "resource://") { + absSource := source + if !filepath.IsAbs(absSource) { + absSource, err = filepath.Abs(absSource) + if err != nil { + return fmt.Errorf("failed to resolve volume mount source path %q: %w", source, err) + } + } + if _, err := os.Stat(absSource); err != nil { + return fmt.Errorf("volume mount source path does not exist: %s", source) + } + } + // Check for duplicate mount target if existingSource, isDuplicate := existingMounts[target]; isDuplicate { slog.Warn("Skipping duplicate mount target", "target", target, "existing_source", existingSource) diff --git a/pkg/runner/config_builder_test.go b/pkg/runner/config_builder_test.go index dfacecf531..aad98ea2ee 100644 --- a/pkg/runner/config_builder_test.go +++ b/pkg/runner/config_builder_test.go @@ -244,6 +244,11 @@ func TestRunConfigBuilder_Build_WithVolumeMounts(t *testing.T) { // Create a mock environment variable validator mockValidator := &mockEnvVarValidator{} + // Create temporary directories for volume mount source paths + tmpDir1 := t.TempDir() + tmpDir2 := t.TempDir() + tmpDir3 := t.TempDir() + testCases := []struct { name string builderOptions []RunConfigBuilderOption @@ -263,7 +268,7 @@ func TestRunConfigBuilder_Build_WithVolumeMounts(t *testing.T) { { name: "Volumes without permission profile but with profile name", builderOptions: []RunConfigBuilderOption{ - WithVolumes([]string{"/host:/container"}), + WithVolumes([]string{tmpDir1 + ":/container"}), WithPermissionProfileNameOrPath(permissions.ProfileNone), }, expectError: false, @@ -273,7 +278,7 @@ func TestRunConfigBuilder_Build_WithVolumeMounts(t *testing.T) { { name: "Read-only volume with existing profile", builderOptions: []RunConfigBuilderOption{ - WithVolumes([]string{"/host:/container:ro"}), + WithVolumes([]string{tmpDir1 + ":/container:ro"}), WithPermissionProfile(permissions.BuiltinNoneProfile()), }, expectError: false, @@ -283,7 +288,7 @@ func TestRunConfigBuilder_Build_WithVolumeMounts(t *testing.T) { { name: "Read-write volume with existing profile", builderOptions: []RunConfigBuilderOption{ - WithVolumes([]string{"/host:/container"}), + WithVolumes([]string{tmpDir1 + ":/container"}), WithPermissionProfile(permissions.BuiltinNoneProfile()), }, expectError: false, @@ -294,9 +299,9 @@ func TestRunConfigBuilder_Build_WithVolumeMounts(t *testing.T) { name: "Multiple volumes with existing profile", builderOptions: []RunConfigBuilderOption{ WithVolumes([]string{ - "/host1:/container1:ro", - "/host2:/container2", - "/host3:/container3:ro", + tmpDir1 + ":/container1:ro", + tmpDir2 + ":/container2", + tmpDir3 + ":/container3:ro", }), WithPermissionProfile(permissions.BuiltinNoneProfile()), }, @@ -312,6 +317,14 @@ func TestRunConfigBuilder_Build_WithVolumeMounts(t *testing.T) { }, expectError: true, }, + { + name: "Non-existent source path", + builderOptions: []RunConfigBuilderOption{ + WithVolumes([]string{"/nonexistent/path/that/does/not/exist:/container"}), + WithPermissionProfile(permissions.BuiltinNoneProfile()), + }, + expectError: true, + }, } for _, tc := range testCases {