From 086fcc6cb3ba901873bd8d300c241be264517fe3 Mon Sep 17 00:00:00 2001
From: Paul Holzinger <pholzing@redhat.com>
Date: Fri, 29 Aug 2025 15:39:38 +0200
Subject: [PATCH 4/6] CVE-2025-9566: kube play: don't follow volume symlinks
 onto the host

For ConfigMap and Secret kube play volumes podman populates the data
from the yaml. However the volume content is not controlled by us and we
can be tricked following a symlink to a file on the host instead.

Bugs: bsc#1249154
Fixes: CVE-2025-9566

Signed-off-by: Paul Holzinger <pholzing@redhat.com>
Signed-off-by: tomsweeneyredhat <tsweeney@redhat.com>
Signed-off-by: Danish Prakash <contact@danishpraka.sh>
---
 pkg/domain/infra/abi/play.go             |  5 ++-
 pkg/domain/infra/abi/play_linux.go       | 18 +++++++++++
 pkg/domain/infra/abi/play_unsupported.go | 13 ++++++++
 pkg/domain/infra/abi/play_utils.go       | 39 +++++++++++++++++++++++-
 4 files changed, 71 insertions(+), 4 deletions(-)
 create mode 100644 pkg/domain/infra/abi/play_linux.go
 create mode 100644 pkg/domain/infra/abi/play_unsupported.go

diff --git a/pkg/domain/infra/abi/play.go b/pkg/domain/infra/abi/play.go
index 6ffbf4cf5..2fa2752d7 100644
--- a/pkg/domain/infra/abi/play.go
+++ b/pkg/domain/infra/abi/play.go
@@ -808,8 +808,7 @@ func (ic *ContainerEngine) playKubePod(ctx context.Context, podName string, podY
 			defaultMode := v.DefaultMode
 			// Create files and add data to the volume mountpoint based on the Items in the volume
 			for k, v := range v.Items {
-				dataPath := filepath.Join(mountPoint, k)
-				f, err := os.Create(dataPath)
+				f, err := openPathSafely(mountPoint, k)
 				if err != nil {
 					return nil, nil, fmt.Errorf("cannot create file %q at volume mountpoint %q: %w", k, mountPoint, err)
 				}
@@ -819,7 +818,7 @@ func (ic *ContainerEngine) playKubePod(ctx context.Context, podName string, podY
 					return nil, nil, err
 				}
 				// Set file permissions
-				if err := os.Chmod(f.Name(), os.FileMode(defaultMode)); err != nil {
+				if err := f.Chmod(os.FileMode(defaultMode)); err != nil {
 					return nil, nil, err
 				}
 			}
diff --git a/pkg/domain/infra/abi/play_linux.go b/pkg/domain/infra/abi/play_linux.go
new file mode 100644
index 000000000..a0f981151
--- /dev/null
+++ b/pkg/domain/infra/abi/play_linux.go
@@ -0,0 +1,18 @@
+//go:build !remote
+
+package abi
+
+import (
+	"os"
+
+	securejoin "github.com/cyphar/filepath-securejoin"
+)
+
+// openSymlinkPath opens the path under root using securejoin.OpenatInRoot().
+func openSymlinkPath(root *os.File, unsafePath string, flags int) (*os.File, error) {
+	file, err := securejoin.OpenatInRoot(root, unsafePath)
+	if err != nil {
+		return nil, err
+	}
+	return securejoin.Reopen(file, flags)
+}
diff --git a/pkg/domain/infra/abi/play_unsupported.go b/pkg/domain/infra/abi/play_unsupported.go
new file mode 100644
index 000000000..3ecbae7cc
--- /dev/null
+++ b/pkg/domain/infra/abi/play_unsupported.go
@@ -0,0 +1,13 @@
+//go:build !linux && !remote
+
+package abi
+
+import (
+	"errors"
+	"os"
+)
+
+// openSymlinkPath is not supported on this platform.
+func openSymlinkPath(root *os.File, unsafePath string, flags int) (*os.File, error) {
+	return nil, errors.New("cannot safely open symlink on this platform")
+}
diff --git a/pkg/domain/infra/abi/play_utils.go b/pkg/domain/infra/abi/play_utils.go
index 7285d9c9b..217b65699 100644
--- a/pkg/domain/infra/abi/play_utils.go
+++ b/pkg/domain/infra/abi/play_utils.go
@@ -2,7 +2,14 @@
 
 package abi
 
-import "github.com/containers/podman/v5/libpod/define"
+import (
+	"fmt"
+	"os"
+	"strings"
+
+	"github.com/containers/podman/v5/libpod/define"
+	"golang.org/x/sys/unix"
+)
 
 // getSdNotifyMode returns the `sdNotifyAnnotation/$name` for the specified
 // name. If name is empty, it'll only look for `sdNotifyAnnotation`.
@@ -16,3 +23,33 @@ func getSdNotifyMode(annotations map[string]string, name string) (string, error)
 	}
 	return mode, define.ValidateSdNotifyMode(mode)
 }
+
+// openPathSafely opens the given name under the trusted root path, the unsafeName
+// must be a single path component and not contain "/".
+// The resulting path will be opened or created if it does not exists.
+// Following of symlink is done within staying under root, escapes outsides
+// of root are not allowed and prevent.
+//
+// This custom function is needed because securejoin.SecureJoin() is not race safe
+// and the volume might be mounted in another container that could swap in a symlink
+// after the function ahs run. securejoin.OpenInRoot() doesn't work either because
+// it cannot create files and doesn't work on freebsd.
+func openPathSafely(root, unsafeName string) (*os.File, error) {
+	if strings.Contains(unsafeName, "/") {
+		return nil, fmt.Errorf("name %q must not contain path separator", unsafeName)
+	}
+	fdDir, err := os.OpenFile(root, unix.O_RDONLY, 0)
+	if err != nil {
+		return nil, err
+	}
+	defer fdDir.Close()
+	flags := unix.O_CREAT | unix.O_WRONLY | unix.O_TRUNC | unix.O_CLOEXEC
+	fd, err := unix.Openat(int(fdDir.Fd()), unsafeName, flags|unix.O_NOFOLLOW, 0o644)
+	if err == nil {
+		return os.NewFile(uintptr(fd), unsafeName), nil
+	}
+	if err == unix.ELOOP {
+		return openSymlinkPath(fdDir, unsafeName, flags)
+	}
+	return nil, &os.PathError{Op: "openat", Path: unsafeName, Err: err}
+}
-- 
2.53.0

