KubeVirt Arbitrary Container File Read
Description
KubeVirt is a virtual machine management add-on for Kubernetes. Prior to 1.5.3 and 1.6.1, a vulnerability was discovered that allows a VM to read arbitrary files from the virt-launcher pod's file system. This issue stems from improper symlink handling when mounting PVC disks into a VM. Specifically, if a malicious user has full or partial control over the contents of a PVC, they can create a symbolic link that points to a file within the virt-launcher pod's file system. Since libvirt can treat regular files as block devices, any file on the pod's file system that is symlinked in this way can be mounted into the VM and subsequently read. Although a security mechanism exists where VMs are executed as an unprivileged user with UID 107 inside the virt-launcher container, limiting the scope of accessible resources, this restriction is bypassed due to a second vulnerability. The latter causes the ownership of any file intended for mounting to be changed to the unprivileged user with UID 107 prior to mounting. As a result, an attacker can gain access to and read arbitrary files located within the virt-launcher pod's file system or on a mounted PVC from within the guest VM. This vulnerability is fixed in 1.5.3 and 1.6.1.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
kubevirt.io/kubevirtGo | < 1.5.3 | 1.5.3 |
kubevirt.io/kubevirtGo | >= 1.6.0-alpha.0, < 1.6.1 | 1.6.1 |
Affected products
1Patches
39dc798cb1efeHost-disk & PVC: Contain disk inside volume
3 files changed · +56 −23
pkg/host-disk/BUILD.bazel+1 −0 modified@@ -13,6 +13,7 @@ go_library( "//pkg/util:go_default_library", "//staging/src/kubevirt.io/api/core/v1:go_default_library", "//staging/src/kubevirt.io/client-go/log:go_default_library", + "//vendor/golang.org/x/sys/unix:go_default_library", "//vendor/k8s.io/api/core/v1:go_default_library", "//vendor/k8s.io/client-go/tools/record:go_default_library", ],
pkg/host-disk/host-disk.go+46 −20 modified@@ -20,12 +20,14 @@ package hostdisk import ( + "errors" "fmt" "os" "path" "path/filepath" "syscall" + "golang.org/x/sys/unix" "kubevirt.io/client-go/log" ephemeraldiskutils "kubevirt.io/kubevirt/pkg/ephemeral-disk-utils" @@ -170,13 +172,34 @@ func dirBytesAvailable(path string, reserve uint64) (uint64, error) { return stat.Bavail*uint64(stat.Bsize) - reserve, nil } -func createSparseRaw(fullPath string, size int64) (err error) { +func createSparseRaw(diskdir *safepath.Path, diskName string, size int64) (err error) { offset := size - 1 - f, err := os.Create(fullPath) + if filepath.Base(diskName) != diskName { + return fmt.Errorf("Disk name needs to be base") + } + + err = safepath.TouchAtNoFollow(diskdir, filepath.Base(diskName), 0666) + if err != nil { + return err + } + + diskPath, err := safepath.JoinNoFollow(diskdir, diskName) + if err != nil { + return err + } + + sFile, err := safepath.OpenAtNoFollow(diskPath) + if err != nil { + return err + } + defer util.CloseIOAndCheckErr(sFile, &err) + + f, err := os.OpenFile(sFile.SafePath(), os.O_WRONLY, 0666) if err != nil { return err } defer util.CloseIOAndCheckErr(f, &err) + _, err = f.WriteAt([]byte{0}, offset) if err != nil { return err @@ -188,14 +211,6 @@ func getPVCDiskImgPath(volumeName string, diskName string) string { return path.Join(pvcBaseDir, volumeName, diskName) } -func GetMountedHostDiskPathFromHandler(mountRoot, volumeName, path string) string { - return filepath.Join(mountRoot, getPVCDiskImgPath(volumeName, filepath.Base(path))) -} - -func GetMountedHostDiskDirFromHandler(mountRoot, volumeName string) string { - return filepath.Join(mountRoot, getPVCDiskImgPath(volumeName, "")) -} - func GetMountedHostDiskPath(volumeName string, path string) string { return getPVCDiskImgPath(volumeName, filepath.Base(path)) } @@ -242,27 +257,37 @@ func shouldMountHostDisk(hostDisk *v1.HostDisk) bool { } func (hdc *DiskImgCreator) mountHostDiskAndSetOwnership(vmi *v1.VirtualMachineInstance, volumeName string, hostDisk *v1.HostDisk) error { - diskPath := GetMountedHostDiskPathFromHandler(unsafepath.UnsafeAbsolute(hdc.mountRoot.Raw()), volumeName, hostDisk.Path) - diskDir := GetMountedHostDiskDirFromHandler(unsafepath.UnsafeAbsolute(hdc.mountRoot.Raw()), volumeName) - fileExists, err := ephemeraldiskutils.FileExists(diskPath) + diskDir, err := hdc.mountRoot.AppendAndResolveWithRelativeRoot(GetMountedHostDiskDir(volumeName)) if err != nil { return err } - if !fileExists { - if err := hdc.handleRequestedSizeAndCreateSparseRaw(vmi, diskDir, diskPath, hostDisk); err != nil { + + diskPath, err := safepath.JoinNoFollow(diskDir, filepath.Base(hostDisk.Path)) + fileNotExists := errors.Is(err, unix.ENOENT) + if err != nil && !fileNotExists { + return err + } + + if fileNotExists { + if err := hdc.handleRequestedSizeAndCreateSparseRaw(vmi, diskDir, filepath.Base(hostDisk.Path), hostDisk); err != nil { + return err + } + + diskPath, err = safepath.JoinNoFollow(diskDir, filepath.Base(hostDisk.Path)) + if err != nil { return err } } // Change file ownership to the qemu user. - if err := ephemeraldiskutils.DefaultOwnershipManager.UnsafeSetFileOwnership(diskPath); err != nil { + if err := ephemeraldiskutils.DefaultOwnershipManager.SetFileOwnership(diskPath); err != nil { log.Log.Reason(err).Errorf("Couldn't set Ownership on %s: %v", diskPath, err) return err } return nil } -func (hdc *DiskImgCreator) handleRequestedSizeAndCreateSparseRaw(vmi *v1.VirtualMachineInstance, diskDir string, diskPath string, hostDisk *v1.HostDisk) error { - size, err := hdc.dirBytesAvailableFunc(diskDir, hdc.minimumPVCReserveBytes) +func (hdc *DiskImgCreator) handleRequestedSizeAndCreateSparseRaw(vmi *v1.VirtualMachineInstance, diskDir *safepath.Path, diskName string, hostDisk *v1.HostDisk) error { + size, err := hdc.dirBytesAvailableFunc(unsafepath.UnsafeAbsolute(diskDir.Raw()), hdc.minimumPVCReserveBytes) availableSize := int64(size) if err != nil { return err @@ -274,9 +299,10 @@ func (hdc *DiskImgCreator) handleRequestedSizeAndCreateSparseRaw(vmi *v1.Virtual return err } } - err = createSparseRaw(diskPath, requestedSize) + err = createSparseRaw(diskDir, diskName, requestedSize) if err != nil { - log.Log.Reason(err).Errorf("Couldn't create a sparse raw file for disk path: %s, error: %v", diskPath, err) + fullPath := filepath.Join(unsafepath.UnsafeAbsolute(diskDir.Raw()), diskName) + log.Log.Reason(err).Errorf("Couldn't create a sparse raw file for disk path: %s, error: %v", fullPath, err) return err } return nil
pkg/host-disk/host-disk_test.go+9 −3 modified@@ -24,6 +24,7 @@ import ( "fmt" "os" "path" + "path/filepath" "strings" "github.com/golang/mock/gomock" @@ -56,11 +57,16 @@ var _ = Describe("HostDisk", func() { createTempDiskImg := func(volumeName string) os.FileInfo { imgPath := path.Join(tempDir, volumeName, "disk.img") - err := os.Mkdir(path.Join(tempDir, volumeName), 0755) + // 67108864 = 64Mi + dir := filepath.Dir(imgPath) + + err := os.Mkdir(dir, 0755) Expect(err).NotTo(HaveOccurred()) - // 67108864 = 64Mi - err = createSparseRaw(imgPath, 67108864) + sDir, err := safepath.NewPathNoFollow(dir) + Expect(err).To(Not(HaveOccurred())) + + err = createSparseRaw(sDir, filepath.Base(imgPath), 67108864) Expect(err).NotTo(HaveOccurred()) file, err := os.Stat(imgPath)
09eafa068ec0Host-disk & PVC: Contain disk inside volume
3 files changed · +56 −23
pkg/host-disk/BUILD.bazel+1 −0 modified@@ -13,6 +13,7 @@ go_library( "//pkg/util:go_default_library", "//staging/src/kubevirt.io/api/core/v1:go_default_library", "//staging/src/kubevirt.io/client-go/log:go_default_library", + "//vendor/golang.org/x/sys/unix:go_default_library", "//vendor/k8s.io/api/core/v1:go_default_library", "//vendor/k8s.io/client-go/tools/record:go_default_library", ],
pkg/host-disk/host-disk.go+46 −20 modified@@ -20,12 +20,14 @@ package hostdisk import ( + "errors" "fmt" "os" "path" "path/filepath" "syscall" + "golang.org/x/sys/unix" "kubevirt.io/client-go/log" ephemeraldiskutils "kubevirt.io/kubevirt/pkg/ephemeral-disk-utils" @@ -170,13 +172,34 @@ func dirBytesAvailable(path string, reserve uint64) (uint64, error) { return stat.Bavail*uint64(stat.Bsize) - reserve, nil } -func createSparseRaw(fullPath string, size int64) (err error) { +func createSparseRaw(diskdir *safepath.Path, diskName string, size int64) (err error) { offset := size - 1 - f, err := os.Create(fullPath) + if filepath.Base(diskName) != diskName { + return fmt.Errorf("Disk name needs to be base") + } + + err = safepath.TouchAtNoFollow(diskdir, filepath.Base(diskName), 0666) + if err != nil { + return err + } + + diskPath, err := safepath.JoinNoFollow(diskdir, diskName) + if err != nil { + return err + } + + sFile, err := safepath.OpenAtNoFollow(diskPath) + if err != nil { + return err + } + defer util.CloseIOAndCheckErr(sFile, &err) + + f, err := os.OpenFile(sFile.SafePath(), os.O_WRONLY, 0666) if err != nil { return err } defer util.CloseIOAndCheckErr(f, &err) + _, err = f.WriteAt([]byte{0}, offset) if err != nil { return err @@ -188,14 +211,6 @@ func getPVCDiskImgPath(volumeName string, diskName string) string { return path.Join(pvcBaseDir, volumeName, diskName) } -func GetMountedHostDiskPathFromHandler(mountRoot, volumeName, path string) string { - return filepath.Join(mountRoot, getPVCDiskImgPath(volumeName, filepath.Base(path))) -} - -func GetMountedHostDiskDirFromHandler(mountRoot, volumeName string) string { - return filepath.Join(mountRoot, getPVCDiskImgPath(volumeName, "")) -} - func GetMountedHostDiskPath(volumeName string, path string) string { return getPVCDiskImgPath(volumeName, filepath.Base(path)) } @@ -242,27 +257,37 @@ func shouldMountHostDisk(hostDisk *v1.HostDisk) bool { } func (hdc *DiskImgCreator) mountHostDiskAndSetOwnership(vmi *v1.VirtualMachineInstance, volumeName string, hostDisk *v1.HostDisk) error { - diskPath := GetMountedHostDiskPathFromHandler(unsafepath.UnsafeAbsolute(hdc.mountRoot.Raw()), volumeName, hostDisk.Path) - diskDir := GetMountedHostDiskDirFromHandler(unsafepath.UnsafeAbsolute(hdc.mountRoot.Raw()), volumeName) - fileExists, err := ephemeraldiskutils.FileExists(diskPath) + diskDir, err := hdc.mountRoot.AppendAndResolveWithRelativeRoot(GetMountedHostDiskDir(volumeName)) if err != nil { return err } - if !fileExists { - if err := hdc.handleRequestedSizeAndCreateSparseRaw(vmi, diskDir, diskPath, hostDisk); err != nil { + + diskPath, err := safepath.JoinNoFollow(diskDir, filepath.Base(hostDisk.Path)) + fileNotExists := errors.Is(err, unix.ENOENT) + if err != nil && !fileNotExists { + return err + } + + if fileNotExists { + if err := hdc.handleRequestedSizeAndCreateSparseRaw(vmi, diskDir, filepath.Base(hostDisk.Path), hostDisk); err != nil { + return err + } + + diskPath, err = safepath.JoinNoFollow(diskDir, filepath.Base(hostDisk.Path)) + if err != nil { return err } } // Change file ownership to the qemu user. - if err := ephemeraldiskutils.DefaultOwnershipManager.UnsafeSetFileOwnership(diskPath); err != nil { + if err := ephemeraldiskutils.DefaultOwnershipManager.SetFileOwnership(diskPath); err != nil { log.Log.Reason(err).Errorf("Couldn't set Ownership on %s: %v", diskPath, err) return err } return nil } -func (hdc *DiskImgCreator) handleRequestedSizeAndCreateSparseRaw(vmi *v1.VirtualMachineInstance, diskDir string, diskPath string, hostDisk *v1.HostDisk) error { - size, err := hdc.dirBytesAvailableFunc(diskDir, hdc.minimumPVCReserveBytes) +func (hdc *DiskImgCreator) handleRequestedSizeAndCreateSparseRaw(vmi *v1.VirtualMachineInstance, diskDir *safepath.Path, diskName string, hostDisk *v1.HostDisk) error { + size, err := hdc.dirBytesAvailableFunc(unsafepath.UnsafeAbsolute(diskDir.Raw()), hdc.minimumPVCReserveBytes) availableSize := int64(size) if err != nil { return err @@ -274,9 +299,10 @@ func (hdc *DiskImgCreator) handleRequestedSizeAndCreateSparseRaw(vmi *v1.Virtual return err } } - err = createSparseRaw(diskPath, requestedSize) + err = createSparseRaw(diskDir, diskName, requestedSize) if err != nil { - log.Log.Reason(err).Errorf("Couldn't create a sparse raw file for disk path: %s, error: %v", diskPath, err) + fullPath := filepath.Join(unsafepath.UnsafeAbsolute(diskDir.Raw()), diskName) + log.Log.Reason(err).Errorf("Couldn't create a sparse raw file for disk path: %s, error: %v", fullPath, err) return err } return nil
pkg/host-disk/host-disk_test.go+9 −3 modified@@ -24,6 +24,7 @@ import ( "fmt" "os" "path" + "path/filepath" "strings" . "github.com/onsi/ginkgo/v2" @@ -56,11 +57,16 @@ var _ = Describe("HostDisk", func() { createTempDiskImg := func(volumeName string) os.FileInfo { imgPath := path.Join(tempDir, volumeName, "disk.img") - err := os.Mkdir(path.Join(tempDir, volumeName), 0755) + // 67108864 = 64Mi + dir := filepath.Dir(imgPath) + + err := os.Mkdir(dir, 0755) Expect(err).NotTo(HaveOccurred()) - // 67108864 = 64Mi - err = createSparseRaw(imgPath, 67108864) + sDir, err := safepath.NewPathNoFollow(dir) + Expect(err).To(Not(HaveOccurred())) + + err = createSparseRaw(sDir, filepath.Base(imgPath), 67108864) Expect(err).NotTo(HaveOccurred()) file, err := os.Stat(imgPath)
a81b27d4600cHost-disk & PVC: Contain disk inside volume
3 files changed · +57 −24
pkg/host-disk/BUILD.bazel+1 −0 modified@@ -13,6 +13,7 @@ go_library( "//pkg/util:go_default_library", "//staging/src/kubevirt.io/api/core/v1:go_default_library", "//staging/src/kubevirt.io/client-go/log:go_default_library", + "//vendor/golang.org/x/sys/unix:go_default_library", "//vendor/k8s.io/api/core/v1:go_default_library", "//vendor/k8s.io/client-go/tools/record:go_default_library", ],
pkg/host-disk/host-disk.go+47 −21 modified@@ -20,12 +20,14 @@ package hostdisk import ( + "errors" "fmt" "os" "path" "path/filepath" "syscall" + "golang.org/x/sys/unix" "kubevirt.io/client-go/log" ephemeraldiskutils "kubevirt.io/kubevirt/pkg/ephemeral-disk-utils" @@ -170,13 +172,34 @@ func dirBytesAvailable(path string, reserve uint64) (uint64, error) { return stat.Bavail*uint64(stat.Bsize) - reserve, nil } -func createSparseRaw(fullPath string, size int64) (err error) { +func createSparseRaw(diskdir *safepath.Path, diskName string, size int64) (err error) { offset := size - 1 - f, err := os.Create(fullPath) + if filepath.Base(diskName) != diskName { + return fmt.Errorf("Disk name needs to be base") + } + + err = safepath.TouchAtNoFollow(diskdir, filepath.Base(diskName), 0666) + if err != nil { + return fmt.Errorf("Failed touch %s,%s : %v", diskdir, diskName, err) + } + + diskPath, err := safepath.JoinNoFollow(diskdir, diskName) + if err != nil { + return fmt.Errorf("Failed append %s,%s : %v", diskdir, diskName, err) + } + + sFile, err := safepath.OpenAtNoFollow(diskPath) + if err != nil { + return fmt.Errorf("Failed NewFile %s,%s : %v", diskdir, diskName, err) + } + defer util.CloseIOAndCheckErr(sFile, &err) + + f, err := os.OpenFile(sFile.SafePath(), os.O_WRONLY, 0666) if err != nil { return err } defer util.CloseIOAndCheckErr(f, &err) + _, err = f.WriteAt([]byte{0}, offset) if err != nil { return err @@ -188,14 +211,6 @@ func getPVCDiskImgPath(volumeName string, diskName string) string { return path.Join(pvcBaseDir, volumeName, diskName) } -func GetMountedHostDiskPathFromHandler(mountRoot, volumeName, path string) string { - return filepath.Join(mountRoot, getPVCDiskImgPath(volumeName, filepath.Base(path))) -} - -func GetMountedHostDiskDirFromHandler(mountRoot, volumeName string) string { - return filepath.Join(mountRoot, getPVCDiskImgPath(volumeName, "")) -} - func GetMountedHostDiskPath(volumeName string, path string) string { return getPVCDiskImgPath(volumeName, filepath.Base(path)) } @@ -242,27 +257,37 @@ func shouldMountHostDisk(hostDisk *v1.HostDisk) bool { } func (hdc *DiskImgCreator) mountHostDiskAndSetOwnership(vmi *v1.VirtualMachineInstance, volumeName string, hostDisk *v1.HostDisk) error { - diskPath := GetMountedHostDiskPathFromHandler(unsafepath.UnsafeAbsolute(hdc.mountRoot.Raw()), volumeName, hostDisk.Path) - diskDir := GetMountedHostDiskDirFromHandler(unsafepath.UnsafeAbsolute(hdc.mountRoot.Raw()), volumeName) - fileExists, err := ephemeraldiskutils.FileExists(diskPath) + diskDir, err := hdc.mountRoot.AppendAndResolveWithRelativeRoot(GetMountedHostDiskDir(volumeName)) if err != nil { - return err + return fmt.Errorf("Resolve diskdir : %v", err) } - if !fileExists { - if err = hdc.handleRequestedSizeAndCreateSparseRaw(vmi, diskDir, diskPath, hostDisk); err != nil { + + diskPath, err := safepath.JoinNoFollow(diskDir, filepath.Base(hostDisk.Path)) + fileNotExists := errors.Is(err, unix.ENOENT) + if err != nil && !fileNotExists { + return fmt.Errorf("Resolve diskPath :%v", err) + } + + if fileNotExists { + if err := hdc.handleRequestedSizeAndCreateSparseRaw(vmi, diskDir, filepath.Base(hostDisk.Path), hostDisk); err != nil { return err } + + diskPath, err = safepath.JoinNoFollow(diskDir, filepath.Base(hostDisk.Path)) + if err != nil { + return fmt.Errorf("last %v", err) + } // Change file ownership to the qemu user. - if err = ephemeraldiskutils.DefaultOwnershipManager.UnsafeSetFileOwnership(diskPath); err != nil { + if err := ephemeraldiskutils.DefaultOwnershipManager.SetFileOwnership(diskPath); err != nil { log.Log.Reason(err).Errorf("Couldn't set Ownership on %s: %v", diskPath, err) return err } } return nil } -func (hdc *DiskImgCreator) handleRequestedSizeAndCreateSparseRaw(vmi *v1.VirtualMachineInstance, diskDir string, diskPath string, hostDisk *v1.HostDisk) error { - size, err := hdc.dirBytesAvailableFunc(diskDir, hdc.minimumPVCReserveBytes) +func (hdc *DiskImgCreator) handleRequestedSizeAndCreateSparseRaw(vmi *v1.VirtualMachineInstance, diskDir *safepath.Path, diskName string, hostDisk *v1.HostDisk) error { + size, err := hdc.dirBytesAvailableFunc(unsafepath.UnsafeAbsolute(diskDir.Raw()), hdc.minimumPVCReserveBytes) availableSize := int64(size) if err != nil { return err @@ -274,9 +299,10 @@ func (hdc *DiskImgCreator) handleRequestedSizeAndCreateSparseRaw(vmi *v1.Virtual return err } } - err = createSparseRaw(diskPath, requestedSize) + err = createSparseRaw(diskDir, diskName, requestedSize) if err != nil { - log.Log.Reason(err).Errorf("Couldn't create a sparse raw file for disk path: %s, error: %v", diskPath, err) + fullPath := filepath.Join(unsafepath.UnsafeAbsolute(diskDir.Raw()), diskName) + log.Log.Reason(err).Errorf("Couldn't create a sparse raw file for disk path: %s, error: %v", fullPath, err) return err } return nil
pkg/host-disk/host-disk_test.go+9 −3 modified@@ -24,6 +24,7 @@ import ( "fmt" "os" "path" + "path/filepath" "strings" . "github.com/onsi/ginkgo/v2" @@ -54,11 +55,16 @@ var _ = Describe("HostDisk", func() { createTempDiskImg := func(volumeName string) os.FileInfo { imgPath := path.Join(tempDir, volumeName, "disk.img") - err := os.Mkdir(path.Join(tempDir, volumeName), 0755) + // 67108864 = 64Mi + dir := filepath.Dir(imgPath) + + err := os.Mkdir(dir, 0755) Expect(err).NotTo(HaveOccurred()) - // 67108864 = 64Mi - err = createSparseRaw(imgPath, 67108864) + sDir, err := safepath.NewPathNoFollow(dir) + Expect(err).To(Not(HaveOccurred())) + + err = createSparseRaw(sDir, filepath.Base(imgPath), 67108864) Expect(err).NotTo(HaveOccurred()) file, err := os.Stat(imgPath)
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-qw6q-3pgr-5cwqghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-64433ghsaADVISORY
- github.com/kubevirt/kubevirt/commit/09eafa068ec01eca0e96ebafeeb9522a878dbf64ghsax_refsource_MISCWEB
- github.com/kubevirt/kubevirt/commit/9dc798cb1efe924a9a2b97b6e016452dec5e3849ghsax_refsource_MISCWEB
- github.com/kubevirt/kubevirt/commit/a81b27d4600cf654274dd197119658382affdb08ghsax_refsource_MISCWEB
- github.com/kubevirt/kubevirt/security/advisories/GHSA-qw6q-3pgr-5cwqghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.