From 25211557c5e1438e69b28db8b97625cfcfeb3f0d Mon Sep 17 00:00:00 2001 From: Samuel K Date: Wed, 13 May 2026 14:55:35 -0500 Subject: [PATCH 1/2] fix(delivery): use podman unshare for rootless volume direct copy Rootless Podman volumes have mountpoints inside a user namespace that the host process cannot write to directly. When the helper-container path fails and we fall back to populateVolumeDirectCopy, detect Podman via filepath.Base and pipe the binary through `podman unshare sh -c` instead of os.WriteFile. Docker behavior is unchanged. --- pkg/agent/delivery/local_docker.go | 25 +++++++ pkg/agent/delivery/local_docker_test.go | 98 +++++++++++++++++++++++++ 2 files changed, 123 insertions(+) diff --git a/pkg/agent/delivery/local_docker.go b/pkg/agent/delivery/local_docker.go index e236b78b8..df2c72384 100644 --- a/pkg/agent/delivery/local_docker.go +++ b/pkg/agent/delivery/local_docker.go @@ -19,6 +19,7 @@ var _ AgentDelivery = (*LocalDockerDelivery)(nil) const ( defaultDockerCmd = "docker" + podmanCmd = "podman" volumePrefix = "devsy-agent-" volumeMountPath = "/opt/devsy" defaultHelperImage = "busybox:latest" @@ -156,6 +157,10 @@ func (d *LocalDockerDelivery) populateVolumeDirectCopy( destPath := filepath.Join(mountpoint, binaryName()) + if d.isPodman() { + return d.populateVolumeViaUnshare(ctx, destPath, data) + } + if err := os.WriteFile(destPath, data, 0o600); err != nil { return fmt.Errorf("write binary to volume: %w", err) } @@ -167,6 +172,26 @@ func (d *LocalDockerDelivery) populateVolumeDirectCopy( return nil } +func (d *LocalDockerDelivery) populateVolumeViaUnshare( + ctx context.Context, + destPath string, + data []byte, +) error { + script := fmt.Sprintf("cat > %s && chmod 755 %s", destPath, destPath) + cmd := d.cmd(ctx, "unshare", "sh", "-c", script) + cmd.Stdin = bytes.NewReader(data) + + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("podman unshare write failed: %s: %w", string(out), err) + } + return nil +} + +func (d *LocalDockerDelivery) isPodman() bool { + return filepath.Base(d.dockerCommand()) == podmanCmd +} + func (d *LocalDockerDelivery) volumeMountpoint( ctx context.Context, volumeName string, diff --git a/pkg/agent/delivery/local_docker_test.go b/pkg/agent/delivery/local_docker_test.go index 6d9861707..744fcb46c 100644 --- a/pkg/agent/delivery/local_docker_test.go +++ b/pkg/agent/delivery/local_docker_test.go @@ -124,3 +124,101 @@ func TestPopulateVolume_FallbackToDirectCopy(t *testing.T) { require.NoError(t, err) assert.Equal(t, os.FileMode(0o755), info.Mode().Perm()) } + +func TestIsPodman(t *testing.T) { + tests := []struct { + name string + cmd string + want bool + }{ + {"default docker", "", false}, + {"explicit docker", "docker", false}, + {"explicit podman", podmanCmd, true}, + {"full path podman", "/usr/bin/podman", true}, + {"full path docker", "/usr/bin/docker", false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := &LocalDockerDelivery{DockerCommand: tt.cmd} + assert.Equal(t, tt.want, d.isPodman()) + }) + } +} + +func TestPopulateVolumeDirectCopy_PodmanUsesUnshare(t *testing.T) { + tmpDir := t.TempDir() + mountDir := filepath.Join(tmpDir, "mount") + require.NoError(t, os.MkdirAll(mountDir, 0o750)) + + destPath := filepath.Join(mountDir, binaryName()) + binaryContent := []byte("fake-agent-binary-content") + + scriptPath := filepath.Join(tmpDir, "podman") + script := "#!/bin/sh\n" + + "case \"$1\" in\n" + + " unshare) shift; exec \"$@\" ;;\n" + + " volume) echo \"" + mountDir + "\" ;;\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} + err := d.populateVolumeDirectCopy(context.Background(), "test-vol", binaryContent) + require.NoError(t, err) + + 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_DockerUsesDirectWrite(t *testing.T) { + tmpDir := t.TempDir() + mountDir := filepath.Join(tmpDir, "mount") + require.NoError(t, os.MkdirAll(mountDir, 0o750)) + + binaryContent := []byte("fake-agent-binary-content") + + scriptPath := filepath.Join(tmpDir, "fake-docker.sh") + script := "#!/bin/sh\n" + + "case \"$1\" in\n" + + " volume) echo \"" + mountDir + "\" ;;\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} + 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 TestPopulateVolumeViaUnshare_FailureReturnsError(t *testing.T) { + tmpDir := t.TempDir() + + scriptPath := filepath.Join(tmpDir, "podman") + script := "#!/bin/sh\necho 'unshare failed: permission denied' >&2; exit 1\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} + err := d.populateVolumeViaUnshare(context.Background(), "/fake/path/devsy", []byte("data")) + require.Error(t, err) + assert.Contains(t, err.Error(), "podman unshare write failed") +} From a03b1e3db0ad62909a3be1be3046550be9c05d5b Mon Sep 17 00:00:00 2001 From: Samuel K Date: Wed, 13 May 2026 15:02:56 -0500 Subject: [PATCH 2/2] fix(delivery): use positional arg in podman unshare to prevent injection Pass destPath as a positional argument ($1) to sh -c instead of interpolating it into the script string, preventing shell injection if the volume mountpoint contains metacharacters. --- pkg/agent/delivery/local_docker.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/agent/delivery/local_docker.go b/pkg/agent/delivery/local_docker.go index df2c72384..3149b3a24 100644 --- a/pkg/agent/delivery/local_docker.go +++ b/pkg/agent/delivery/local_docker.go @@ -177,8 +177,7 @@ func (d *LocalDockerDelivery) populateVolumeViaUnshare( destPath string, data []byte, ) error { - script := fmt.Sprintf("cat > %s && chmod 755 %s", destPath, destPath) - cmd := d.cmd(ctx, "unshare", "sh", "-c", script) + cmd := d.cmd(ctx, "unshare", "sh", "-c", `cat > "$1" && chmod 755 "$1"`, "--", destPath) cmd.Stdin = bytes.NewReader(data) out, err := cmd.CombinedOutput()