From 7fa2e5aae8da28d1299ba2b01213de8d6d9426dd Mon Sep 17 00:00:00 2001 From: Samuel K Date: Wed, 13 May 2026 14:57:38 -0500 Subject: [PATCH 1/2] fix(delivery): use podman unshare for rootless volume population populateVolumeDirectCopy writes directly to the volume mountpoint path from `volume inspect`. Under rootless Podman, this path is inside a user namespace and not accessible from the host filesystem, causing WriteFile to fail. Detect rootless Podman via `podman info --format {{.Host.Security.Rootless}}` and route to a new populateVolumeUnshare method that pipes the binary through `podman unshare sh -c 'cat > && chmod 755 '` to write within the correct namespace. Docker path is unchanged. --- pkg/agent/delivery/local_docker.go | 53 +++++++++ pkg/agent/delivery/local_docker_test.go | 139 ++++++++++++++++++++++++ 2 files changed, 192 insertions(+) diff --git a/pkg/agent/delivery/local_docker.go b/pkg/agent/delivery/local_docker.go index e236b78b8..6af9f112c 100644 --- a/pkg/agent/delivery/local_docker.go +++ b/pkg/agent/delivery/local_docker.go @@ -149,6 +149,10 @@ func (d *LocalDockerDelivery) populateVolumeDirectCopy( volumeName string, data []byte, ) error { + if d.isRootlessPodman(ctx) { + return d.populateVolumeUnshare(ctx, volumeName, data) + } + mountpoint, err := d.volumeMountpoint(ctx, volumeName) if err != nil { return fmt.Errorf("inspect volume mountpoint: %w", err) @@ -167,6 +171,36 @@ func (d *LocalDockerDelivery) populateVolumeDirectCopy( return nil } +func (d *LocalDockerDelivery) populateVolumeUnshare( + ctx context.Context, + volumeName string, + data []byte, +) error { + mountpoint, err := d.volumeMountpoint(ctx, volumeName) + if err != nil { + return fmt.Errorf("inspect volume mountpoint: %w", err) + } + + destPath := filepath.Join(mountpoint, binaryName()) + + script := fmt.Sprintf( + "cat > %s && chmod 755 %s", + destPath, destPath, + ) + // #nosec G204 -- args are constructed internally, not from user input + cmd := exec.CommandContext(ctx, d.dockerCommand(), "unshare", "sh", "-c", script) + cmd.Stdin = bytes.NewReader(data) + if d.Environment != nil { + cmd.Env = append(os.Environ(), d.Environment...) + } + + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("podman unshare write: %s: %w", string(out), err) + } + return nil +} + func (d *LocalDockerDelivery) volumeMountpoint( ctx context.Context, volumeName string, @@ -191,6 +225,25 @@ func (d *LocalDockerDelivery) removeVolume(ctx context.Context, workspaceID stri return nil } +func (d *LocalDockerDelivery) isPodman(ctx context.Context) bool { + out, err := d.cmd(ctx, "--version").CombinedOutput() + if err != nil { + return false + } + return strings.Contains(string(out), "podman") +} + +func (d *LocalDockerDelivery) isRootlessPodman(ctx context.Context) bool { + if !d.isPodman(ctx) { + return false + } + out, err := d.cmd(ctx, "info", "--format", "{{.Host.Security.Rootless}}").CombinedOutput() + if err != nil { + return false + } + return strings.TrimSpace(string(out)) == "true" +} + func (d *LocalDockerDelivery) cmd(ctx context.Context, args ...string) *exec.Cmd { // #nosec G204 -- args are constructed internally, not from user input cmd := exec.CommandContext(ctx, d.dockerCommand(), args...) diff --git a/pkg/agent/delivery/local_docker_test.go b/pkg/agent/delivery/local_docker_test.go index 6d9861707..512d62d7c 100644 --- a/pkg/agent/delivery/local_docker_test.go +++ b/pkg/agent/delivery/local_docker_test.go @@ -124,3 +124,142 @@ func TestPopulateVolume_FallbackToDirectCopy(t *testing.T) { require.NoError(t, err) assert.Equal(t, os.FileMode(0o755), info.Mode().Perm()) } + +func TestIsPodman_Docker(t *testing.T) { + tmpDir := t.TempDir() + scriptPath := filepath.Join(tmpDir, "fake-docker.sh") + script := "#!/bin/sh\necho 'Docker version 24.0.7, build afdd53b'\n" + require.NoError(t, os.WriteFile(scriptPath, []byte(script), 0o600)) + // #nosec G302 -- test script must be executable + require.NoError(t, os.Chmod(scriptPath, 0o755)) + + d := &LocalDockerDelivery{DockerCommand: scriptPath} + assert.False(t, d.isPodman(context.Background())) +} + +func TestIsPodman_Podman(t *testing.T) { + tmpDir := t.TempDir() + scriptPath := filepath.Join(tmpDir, "fake-podman.sh") + script := "#!/bin/sh\necho 'podman version 4.9.3'\n" + require.NoError(t, os.WriteFile(scriptPath, []byte(script), 0o600)) + // #nosec G302 -- test script must be executable + require.NoError(t, os.Chmod(scriptPath, 0o755)) + + d := &LocalDockerDelivery{DockerCommand: scriptPath} + assert.True(t, d.isPodman(context.Background())) +} + +func TestIsRootlessPodman_DockerRuntime(t *testing.T) { + tmpDir := t.TempDir() + scriptPath := filepath.Join(tmpDir, "fake-docker.sh") + script := "#!/bin/sh\necho 'Docker version 24.0.7, build afdd53b'\n" + require.NoError(t, os.WriteFile(scriptPath, []byte(script), 0o600)) + // #nosec G302 -- test script must be executable + require.NoError(t, os.Chmod(scriptPath, 0o755)) + + d := &LocalDockerDelivery{DockerCommand: scriptPath} + assert.False(t, d.isRootlessPodman(context.Background())) +} + +func TestIsRootlessPodman_PodmanRootful(t *testing.T) { + tmpDir := t.TempDir() + scriptPath := filepath.Join(tmpDir, "fake-podman.sh") + script := "#!/bin/sh\n" + + "case \"$1\" in\n" + + " --version) echo 'podman version 4.9.3' ;;\n" + + " info) echo 'false' ;;\n" + + " *) exit 1 ;;\n" + + "esac\n" + require.NoError(t, os.WriteFile(scriptPath, []byte(script), 0o600)) + // #nosec G302 -- test script must be executable + require.NoError(t, os.Chmod(scriptPath, 0o755)) + + d := &LocalDockerDelivery{DockerCommand: scriptPath} + assert.False(t, d.isRootlessPodman(context.Background())) +} + +func TestIsRootlessPodman_PodmanRootless(t *testing.T) { + tmpDir := t.TempDir() + scriptPath := filepath.Join(tmpDir, "fake-podman.sh") + script := "#!/bin/sh\n" + + "case \"$1\" in\n" + + " --version) echo 'podman version 4.9.3' ;;\n" + + " info) echo 'true' ;;\n" + + " *) exit 1 ;;\n" + + "esac\n" + require.NoError(t, os.WriteFile(scriptPath, []byte(script), 0o600)) + // #nosec G302 -- test script must be executable + require.NoError(t, os.Chmod(scriptPath, 0o755)) + + d := &LocalDockerDelivery{DockerCommand: scriptPath} + assert.True(t, d.isRootlessPodman(context.Background())) +} + +func TestPopulateVolumeDirectCopy_RootlessPodman_UsesUnshare(t *testing.T) { + tmpDir := t.TempDir() + mountDir := filepath.Join(tmpDir, "mount") + require.NoError(t, os.MkdirAll(mountDir, 0o750)) + + scriptPath := filepath.Join(tmpDir, "fake-podman.sh") + script := "#!/bin/sh\n" + + "case \"$1\" in\n" + + " --version) echo 'podman version 4.9.3' ;;\n" + + " info) echo 'true' ;;\n" + + " volume) echo '" + mountDir + "' ;;\n" + + " unshare) shift; exec \"$@\" ;;\n" + + " *) exit 1 ;;\n" + + "esac\n" + require.NoError(t, os.WriteFile(scriptPath, []byte(script), 0o600)) + // #nosec G302 -- test script must be executable + require.NoError(t, os.Chmod(scriptPath, 0o755)) + + binaryContent := []byte("fake-agent-binary-rootless") + + d := &LocalDockerDelivery{DockerCommand: scriptPath} + + err := d.populateVolumeDirectCopy(context.Background(), "test-vol", binaryContent) + require.NoError(t, err) + + destPath := filepath.Join(mountDir, binaryName()) + data, err := os.ReadFile(destPath) //nolint:gosec // test reads from a temp directory we control + require.NoError(t, err) + assert.Equal(t, binaryContent, data) + + info, err := os.Stat(destPath) + require.NoError(t, err) + assert.Equal(t, os.FileMode(0o755), info.Mode().Perm()) +} + +func TestPopulateVolumeDirectCopy_Docker_SkipsUnshare(t *testing.T) { + tmpDir := t.TempDir() + mountDir := filepath.Join(tmpDir, "mount") + require.NoError(t, os.MkdirAll(mountDir, 0o750)) + + scriptPath := filepath.Join(tmpDir, "fake-docker.sh") + script := "#!/bin/sh\n" + + "case \"$1\" in\n" + + " --version) echo 'Docker version 24.0.7, build afdd53b' ;;\n" + + " volume) echo '" + mountDir + "' ;;\n" + + " unshare) echo 'unshare should not be called' >&2; exit 1 ;;\n" + + " *) exit 1 ;;\n" + + "esac\n" + require.NoError(t, os.WriteFile(scriptPath, []byte(script), 0o600)) + // #nosec G302 -- test script must be executable + require.NoError(t, os.Chmod(scriptPath, 0o755)) + + binaryContent := []byte("fake-agent-binary-docker") + + d := &LocalDockerDelivery{DockerCommand: scriptPath} + + err := d.populateVolumeDirectCopy(context.Background(), "test-vol", binaryContent) + require.NoError(t, err) + + destPath := filepath.Join(mountDir, binaryName()) + data, err := os.ReadFile(destPath) //nolint:gosec // test reads from a temp directory we control + require.NoError(t, err) + assert.Equal(t, binaryContent, data) + + info, err := os.Stat(destPath) + require.NoError(t, err) + assert.Equal(t, os.FileMode(0o755), info.Mode().Perm()) +} From c5f148c84f3f90ae11d0df1e7b868e43f29318c2 Mon Sep 17 00:00:00 2001 From: Samuel K Date: Wed, 13 May 2026 15:03:58 -0500 Subject: [PATCH 2/2] fix(delivery): use d.cmd() and quoted positional args in unshare path Route populateVolumeUnshare through d.cmd() instead of raw exec.CommandContext to stay consistent with all other methods and inherit environment handling. Pass destPath as a positional argument to the shell script to prevent breakage from special characters in volume mountpoint paths. --- pkg/agent/delivery/local_docker.go | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/pkg/agent/delivery/local_docker.go b/pkg/agent/delivery/local_docker.go index 6af9f112c..62e365042 100644 --- a/pkg/agent/delivery/local_docker.go +++ b/pkg/agent/delivery/local_docker.go @@ -183,16 +183,8 @@ func (d *LocalDockerDelivery) populateVolumeUnshare( destPath := filepath.Join(mountpoint, binaryName()) - script := fmt.Sprintf( - "cat > %s && chmod 755 %s", - destPath, destPath, - ) - // #nosec G204 -- args are constructed internally, not from user input - cmd := exec.CommandContext(ctx, d.dockerCommand(), "unshare", "sh", "-c", script) + cmd := d.cmd(ctx, "unshare", "sh", "-c", `cat > "$1" && chmod 755 "$1"`, "--", destPath) cmd.Stdin = bytes.NewReader(data) - if d.Environment != nil { - cmd.Env = append(os.Environ(), d.Environment...) - } out, err := cmd.CombinedOutput() if err != nil {