Skip to content

Commit 7078b79

Browse files
Merge pull request #27020 from TomSweeneyRedHat/dev/tsweeney/CVE-2025-9566-v5.6-rhel
[v5.6-rhel] Backport CVE-2025-9566 fixes
2 parents 56f1962 + f4113c7 commit 7078b79

File tree

5 files changed

+101
-5
lines changed

5 files changed

+101
-5
lines changed

pkg/domain/infra/abi/play.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -810,8 +810,7 @@ func (ic *ContainerEngine) playKubePod(ctx context.Context, podName string, podY
810810
defaultMode := v.DefaultMode
811811
// Create files and add data to the volume mountpoint based on the Items in the volume
812812
for k, v := range v.Items {
813-
dataPath := filepath.Join(mountPoint, k)
814-
f, err := os.Create(dataPath)
813+
f, err := openPathSafely(mountPoint, k)
815814
if err != nil {
816815
return nil, nil, fmt.Errorf("cannot create file %q at volume mountpoint %q: %w", k, mountPoint, err)
817816
}
@@ -821,7 +820,7 @@ func (ic *ContainerEngine) playKubePod(ctx context.Context, podName string, podY
821820
return nil, nil, err
822821
}
823822
// Set file permissions
824-
if err := os.Chmod(f.Name(), os.FileMode(defaultMode)); err != nil {
823+
if err := f.Chmod(os.FileMode(defaultMode)); err != nil {
825824
return nil, nil, err
826825
}
827826
}

pkg/domain/infra/abi/play_linux.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
//go:build !remote
2+
3+
package abi
4+
5+
import (
6+
"os"
7+
8+
securejoin "github.com/cyphar/filepath-securejoin"
9+
)
10+
11+
// openSymlinkPath opens the path under root using securejoin.OpenatInRoot().
12+
func openSymlinkPath(root *os.File, unsafePath string, flags int) (*os.File, error) {
13+
file, err := securejoin.OpenatInRoot(root, unsafePath)
14+
if err != nil {
15+
return nil, err
16+
}
17+
return securejoin.Reopen(file, flags)
18+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
//go:build !linux && !remote
2+
3+
package abi
4+
5+
import (
6+
"errors"
7+
"os"
8+
)
9+
10+
// openSymlinkPath is not supported on this platform.
11+
func openSymlinkPath(root *os.File, unsafePath string, flags int) (*os.File, error) {
12+
return nil, errors.New("cannot safely open symlink on this platform")
13+
}

pkg/domain/infra/abi/play_utils.go

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,14 @@
22

33
package abi
44

5-
import "github.com/containers/podman/v5/libpod/define"
5+
import (
6+
"fmt"
7+
"os"
8+
"strings"
9+
10+
"github.com/containers/podman/v5/libpod/define"
11+
"golang.org/x/sys/unix"
12+
)
613

714
// getSdNotifyMode returns the `sdNotifyAnnotation/$name` for the specified
815
// 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)
1623
}
1724
return mode, define.ValidateSdNotifyMode(mode)
1825
}
26+
27+
// openPathSafely opens the given name under the trusted root path, the unsafeName
28+
// must be a single path component and not contain "/".
29+
// The resulting path will be opened or created if it does not exists.
30+
// Following of symlink is done within staying under root, escapes outsides
31+
// of root are not allowed and prevent.
32+
//
33+
// This custom function is needed because securejoin.SecureJoin() is not race safe
34+
// and the volume might be mounted in another container that could swap in a symlink
35+
// after the function ahs run. securejoin.OpenInRoot() doesn't work either because
36+
// it cannot create files and doesn't work on freebsd.
37+
func openPathSafely(root, unsafeName string) (*os.File, error) {
38+
if strings.Contains(unsafeName, "/") {
39+
return nil, fmt.Errorf("name %q must not contain path separator", unsafeName)
40+
}
41+
fdDir, err := os.OpenFile(root, unix.O_RDONLY, 0)
42+
if err != nil {
43+
return nil, err
44+
}
45+
defer fdDir.Close()
46+
flags := unix.O_CREAT | unix.O_WRONLY | unix.O_TRUNC | unix.O_CLOEXEC
47+
fd, err := unix.Openat(int(fdDir.Fd()), unsafeName, flags|unix.O_NOFOLLOW, 0o644)
48+
if err == nil {
49+
return os.NewFile(uintptr(fd), unsafeName), nil
50+
}
51+
if err == unix.ELOOP {
52+
return openSymlinkPath(fdDir, unsafeName, flags)
53+
}
54+
return nil, &os.PathError{Op: "openat", Path: unsafeName, Err: err}
55+
}

test/e2e/play_kube_test.go

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2188,7 +2188,7 @@ func getPersistentVolumeClaimVolume(vName string) *Volume {
21882188

21892189
// getConfigMapVolume returns a new ConfigMap Volume given the name and items
21902190
// of the ConfigMap.
2191-
func getConfigMapVolume(vName string, items []map[string]string, optional bool, defaultMode *int32) *Volume { //nolint:unparam
2191+
func getConfigMapVolume(vName string, items []map[string]string, optional bool, defaultMode *int32) *Volume {
21922192
vol := &Volume{
21932193
VolumeType: "ConfigMap",
21942194
Name: defaultVolName,
@@ -6310,4 +6310,33 @@ spec:
63106310
session.WaitWithDefaultTimeout()
63116311
Expect(session).Should(ExitWithError(125, "invalid signal: noSuchSignal"))
63126312
})
6313+
6314+
It("CVE-2025-9566 regression test - ConfigMap mount", func() {
6315+
testfile := filepath.Join(podmanTest.TempDir, "testfile")
6316+
volumeName := "cm-vol"
6317+
cm := getConfigMap(withConfigMapName(volumeName), withConfigMapData("foo", "content1"))
6318+
cmYaml, err := getKubeYaml("configmap", cm)
6319+
Expect(err).ToNot(HaveOccurred())
6320+
6321+
ctrName := "ctr1"
6322+
podName := "pod1"
6323+
// create a symlink at the volume mount location so we can make sure we don't resolve that to the host location.
6324+
ctr := getCtr(withName(ctrName), withVolumeMount("/test", "", false), withImage(CITEST_IMAGE), withCmd([]string{"sh", "-c", "ln -sf " + testfile + " /test/foo"}))
6325+
pod := getPod(withPodName(podName), withVolume(getConfigMapVolume(volumeName, nil, false, nil)), withCtr(ctr))
6326+
podYaml, err := getKubeYaml("pod", pod)
6327+
Expect(err).ToNot(HaveOccurred())
6328+
yamls := []string{cmYaml, podYaml}
6329+
err = generateMultiDocKubeYaml(yamls, kubeYaml)
6330+
Expect(err).ToNot(HaveOccurred())
6331+
6332+
podmanTest.PodmanExitCleanly("kube", "play", kubeYaml)
6333+
// wait for the container to finish to ensure the symlink was created
6334+
podmanTest.PodmanExitCleanly("wait", podName+"-"+ctrName)
6335+
podmanTest.PodmanExitCleanly("kube", "down", kubeYaml)
6336+
kube := podmanTest.Podman([]string{"kube", "play", kubeYaml})
6337+
kube.WaitWithDefaultTimeout()
6338+
Expect(kube).To(ExitWithError(125, `cannot create file "foo" at volume mountpoint`))
6339+
6340+
Expect(testfile).ToNot(BeAnExistingFile(), "file should never be created on the host")
6341+
})
63136342
})

0 commit comments

Comments
 (0)