From df0c005af854496789ade43e19e8671aeb17e4ed Mon Sep 17 00:00:00 2001 From: User Date: Sun, 22 Feb 2026 04:41:17 +0800 Subject: [PATCH 1/2] 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 { From 8f701de438aeb257d9d662bc07af489274cbe787 Mon Sep 17 00:00:00 2001 From: User Date: Thu, 26 Feb 2026 14:28:09 +0800 Subject: [PATCH 2/2] fix: use t.TempDir() for volume mount paths in tests The volume path validation added in the previous commit checks that source paths exist on the host filesystem. Tests using hardcoded paths like /host and /host/read fail on CI runners where those paths don't exist. Use t.TempDir() to create real temporary directories instead. --- pkg/runner/config_test.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pkg/runner/config_test.go b/pkg/runner/config_test.go index 1a2b21621d..2df2a40050 100644 --- a/pkg/runner/config_test.go +++ b/pkg/runner/config_test.go @@ -752,7 +752,8 @@ func TestRunConfigBuilder(t *testing.T) { } host := localhostStr debug := true - volumes := []string{"/host:/container"} + tmpVolumeDir := t.TempDir() + volumes := []string{tmpVolumeDir + ":/container"} secretsList := []string{"secret1,target=ENV_VAR1"} authzConfigPath := "" // Empty to skip loading the authorization configuration permissionProfile := permissions.ProfileNone @@ -1304,9 +1305,11 @@ func TestRunConfigBuilder_VolumeProcessing(t *testing.T) { runtime := &runtimemocks.MockRuntime{} validator := &mockEnvVarValidator{} + tmpReadDir := t.TempDir() + tmpWriteDir := t.TempDir() volumes := []string{ - "/host/read:/container/read:ro", - "/host/write:/container/write", + tmpReadDir + ":/container/read:ro", + tmpWriteDir + ":/container/write", } config, err := NewRunConfigBuilder(context.Background(), nil, nil, validator,