VYPR
Low severityNVD Advisory· Published May 30, 2023· Updated Jan 10, 2025

In Lima, a malicious disk image could read a single file on the host filesystem as a qcow2/vmdk backing file

CVE-2023-32684

Description

Lima launches Linux virtual machines, typically on macOS, for running containerd. Prior to version 0.16.0, a virtual machine instance with a malicious disk image could read a single file on the host filesystem, even when no filesystem is mounted from the host. The official templates of Lima and the well-known third party products (Colima, Rancher Desktop, and Finch) are unlikely to be affected by this issue. To exploit this issue, the attacker has to embed the target file path (an absolute or a relative path from the instance directory) in a malicious disk image, as the qcow2 (or vmdk) backing file path string. As Lima refuses to run as the root, it is practically impossible for the attacker to read the entire host disk via /dev/rdiskN. Also, practically, the attacker cannot read at least the first 512 bytes (MBR) of the target file. The issue has been patched in Lima in version 0.16.0 by prohibiting using a backing file path in the VM base image.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/lima-vm/limaGo
< 0.16.00.16.0

Affected products

1

Patches

1
01dbd4d9cabe

Merge pull request from GHSA-f7qw-jj9c-rpq9

https://github.com/lima-vm/limaAkihiro SudaMay 30, 2023via ghsa
5 files changed · +367 42
  • pkg/qemu/imgutil/imgutil.go+112 21 modified
    @@ -5,14 +5,78 @@ import (
     	"encoding/json"
     	"fmt"
     	"os/exec"
    -	"path/filepath"
    -	"strings"
    +
    +	"github.com/sirupsen/logrus"
     )
     
    +type InfoChild struct {
    +	Name string `json:"name,omitempty"` // since QEMU 8.0
    +	Info Info   `json:"info,omitempty"` // since QEMU 8.0
    +}
    +
    +type InfoFormatSpecific struct {
    +	Type string          `json:"type,omitempty"` // since QEMU 1.7
    +	Data json.RawMessage `json:"data,omitempty"` // since QEMU 1.7
    +}
    +
    +func (sp *InfoFormatSpecific) Qcow2() *InfoFormatSpecificDataQcow2 {
    +	if sp.Type != "qcow2" {
    +		return nil
    +	}
    +	var x InfoFormatSpecificDataQcow2
    +	if err := json.Unmarshal(sp.Data, &x); err != nil {
    +		panic(err)
    +	}
    +	return &x
    +}
    +
    +func (sp *InfoFormatSpecific) Vmdk() *InfoFormatSpecificDataVmdk {
    +	if sp.Type != "vmdk" {
    +		return nil
    +	}
    +	var x InfoFormatSpecificDataVmdk
    +	if err := json.Unmarshal(sp.Data, &x); err != nil {
    +		panic(err)
    +	}
    +	return &x
    +}
    +
    +type InfoFormatSpecificDataQcow2 struct {
    +	Compat          string `json:"compat,omitempty"`           // since QEMU 1.7
    +	LazyRefcounts   bool   `json:"lazy-refcounts,omitempty"`   // since QEMU 1.7
    +	Corrupt         bool   `json:"corrupt,omitempty"`          // since QEMU 2.2
    +	RefcountBits    int    `json:"refcount-bits,omitempty"`    // since QEMU 2.3
    +	CompressionType string `json:"compression-type,omitempty"` // since QEMU 5.1
    +	ExtendedL2      bool   `json:"extended-l2,omitempty"`      // since QEMU 5.2
    +}
    +
    +type InfoFormatSpecificDataVmdk struct {
    +	CreateType string                             `json:"create-type,omitempty"` // since QEMU 1.7
    +	CID        int                                `json:"cid,omitempty"`         // since QEMU 1.7
    +	ParentCID  int                                `json:"parent-cid,omitempty"`  // since QEMU 1.7
    +	Extents    []InfoFormatSpecificDataVmdkExtent `json:"extents,omitempty"`     // since QEMU 1.7
    +}
    +
    +type InfoFormatSpecificDataVmdkExtent struct {
    +	Filename    string `json:"filename,omitempty"`     // since QEMU 1.7
    +	Format      string `json:"format,omitempty"`       // since QEMU 1.7
    +	VSize       int64  `json:"virtual-size,omitempty"` // since QEMU 1.7
    +	ClusterSize int    `json:"cluster-size,omitempty"` // since QEMU 1.7
    +}
    +
     // Info corresponds to the output of `qemu-img info --output=json FILE`
     type Info struct {
    -	Format string `json:"format,omitempty"` // since QEMU 1.3
    -	VSize  int64  `json:"virtual-size,omitempty"`
    +	Filename              string              `json:"filename,omitempty"`                // since QEMU 1.3
    +	Format                string              `json:"format,omitempty"`                  // since QEMU 1.3
    +	VSize                 int64               `json:"virtual-size,omitempty"`            // since QEMU 1.3
    +	ActualSize            int64               `json:"actual-size,omitempty"`             // since QEMU 1.3
    +	DirtyFlag             bool                `json:"dirty-flag,omitempty"`              // since QEMU 5.2
    +	ClusterSize           int                 `json:"cluster-size,omitempty"`            // since QEMU 1.3
    +	BackingFilename       string              `json:"backing-filename,omitempty"`        // since QEMU 1.3
    +	FullBackingFilename   string              `json:"full-backing-filename,omitempty"`   // since QEMU 1.3
    +	BackingFilenameFormat string              `json:"backing-filename-format,omitempty"` // since QEMU 1.3
    +	FormatSpecific        *InfoFormatSpecific `json:"format-specific,omitempty"`         // since QEMU 1.7
    +	Children              []InfoChild         `json:"children,omitempty"`                // since QEMU 8.0
     }
     
     func ConvertToRaw(source string, dest string) error {
    @@ -27,6 +91,14 @@ func ConvertToRaw(source string, dest string) error {
     	return nil
     }
     
    +func ParseInfo(b []byte) (*Info, error) {
    +	var imgInfo Info
    +	if err := json.Unmarshal(b, &imgInfo); err != nil {
    +		return nil, err
    +	}
    +	return &imgInfo, nil
    +}
    +
     func GetInfo(f string) (*Info, error) {
     	var stdout, stderr bytes.Buffer
     	cmd := exec.Command("qemu-img", "info", "--output=json", "--force-share", f)
    @@ -36,26 +108,45 @@ func GetInfo(f string) (*Info, error) {
     		return nil, fmt.Errorf("failed to run %v: stdout=%q, stderr=%q: %w",
     			cmd.Args, stdout.String(), stderr.String(), err)
     	}
    -	var imgInfo Info
    -	if err := json.Unmarshal(stdout.Bytes(), &imgInfo); err != nil {
    -		return nil, err
    -	}
    -	return &imgInfo, nil
    +	return ParseInfo(stdout.Bytes())
     }
     
    -func DetectFormat(f string) (string, error) {
    -	switch ext := strings.ToLower(filepath.Ext(f)); ext {
    -	case ".qcow2":
    -		return "qcow2", nil
    -	case ".raw":
    -		return "raw", nil
    +func AcceptableAsBasedisk(info *Info) error {
    +	switch info.Format {
    +	case "qcow2", "raw":
    +		// NOP
    +	default:
    +		logrus.WithField("filename", info.Filename).
    +			Warnf("Unsupported image format %q. The image may not boot, or may have an extra privilege to access the host filesystem. Use with caution.", info.Format)
    +	}
    +	if info.BackingFilename != "" {
    +		return fmt.Errorf("base disk (%q) must not have a backing file (%q)", info.Filename, info.BackingFilename)
     	}
    -	imgInfo, err := GetInfo(f)
    -	if err != nil {
    -		return "", err
    +	if info.FullBackingFilename != "" {
    +		return fmt.Errorf("base disk (%q) must not have a backing file (%q)", info.Filename, info.FullBackingFilename)
     	}
    -	if imgInfo.Format == "" {
    -		return "", fmt.Errorf("failed to detect format of %q", f)
    +	if info.FormatSpecific != nil {
    +		if vmdk := info.FormatSpecific.Vmdk(); vmdk != nil {
    +			for _, e := range vmdk.Extents {
    +				if e.Filename != info.Filename {
    +					return fmt.Errorf("base disk (%q) must not have an extent file (%q)", info.Filename, e.Filename)
    +				}
    +			}
    +		}
     	}
    -	return imgInfo.Format, nil
    +	// info.Children is set since QEMU 8.0
    +	switch len(info.Children) {
    +	case 0:
    +	// NOP
    +	case 1:
    +		if info.Filename != info.Children[0].Info.Filename {
    +			return fmt.Errorf("base disk (%q) child must not have a different filename (%q)", info.Filename, info.Children[0].Info.Filename)
    +		}
    +		if len(info.Children[0].Info.Children) > 0 {
    +			return fmt.Errorf("base disk (%q) child must not have children of its own", info.Filename)
    +		}
    +	default:
    +		return fmt.Errorf("base disk (%q) must not have multiple children: %+v", info.Filename, info.Children)
    +	}
    +	return nil
     }
    
  • pkg/qemu/imgutil/imgutil_test.go+212 0 added
    @@ -0,0 +1,212 @@
    +package imgutil
    +
    +import (
    +	"testing"
    +
    +	"gotest.tools/v3/assert"
    +)
    +
    +func TestParseInfo(t *testing.T) {
    +	t.Run("qcow2", func(t *testing.T) {
    +		// qemu-img create -f qcow2 foo.qcow2 4G
    +		// (QEMU 8.0)
    +		const s = `{
    +    "children": [
    +        {
    +            "name": "file",
    +            "info": {
    +                "children": [
    +                ],
    +                "virtual-size": 197120,
    +                "filename": "foo.qcow2",
    +                "format": "file",
    +                "actual-size": 200704,
    +                "format-specific": {
    +                    "type": "file",
    +                    "data": {
    +                    }
    +                },
    +                "dirty-flag": false
    +            }
    +        }
    +    ],
    +    "virtual-size": 4294967296,
    +    "filename": "foo.qcow2",
    +    "cluster-size": 65536,
    +    "format": "qcow2",
    +    "actual-size": 200704,
    +    "format-specific": {
    +        "type": "qcow2",
    +        "data": {
    +            "compat": "1.1",
    +            "compression-type": "zlib",
    +            "lazy-refcounts": false,
    +            "refcount-bits": 16,
    +            "corrupt": false,
    +            "extended-l2": false
    +        }
    +    },
    +    "dirty-flag": false
    +}`
    +
    +		info, err := ParseInfo([]byte(s))
    +		assert.NilError(t, err)
    +		assert.Equal(t, 1, len(info.Children))
    +		assert.Check(t, info.FormatSpecific != nil)
    +		qcow2 := info.FormatSpecific.Qcow2()
    +		assert.Check(t, qcow2 != nil)
    +		assert.Equal(t, qcow2.Compat, "1.1")
    +
    +		t.Run("diff", func(t *testing.T) {
    +			// qemu-img create -f qcow2 -F qcow2 -b foo.qcow2 bar.qcow2
    +			// (QEMU 8.0)
    +			const s = `{
    +    "children": [
    +        {
    +            "name": "file",
    +            "info": {
    +                "children": [
    +                ],
    +                "virtual-size": 197120,
    +                "filename": "bar.qcow2",
    +                "format": "file",
    +                "actual-size": 200704,
    +                "format-specific": {
    +                    "type": "file",
    +                    "data": {
    +                    }
    +                },
    +                "dirty-flag": false
    +            }
    +        }
    +    ],
    +    "backing-filename-format": "qcow2",
    +    "virtual-size": 4294967296,
    +    "filename": "bar.qcow2",
    +    "cluster-size": 65536,
    +    "format": "qcow2",
    +    "actual-size": 200704,
    +    "format-specific": {
    +        "type": "qcow2",
    +        "data": {
    +            "compat": "1.1",
    +            "compression-type": "zlib",
    +            "lazy-refcounts": false,
    +            "refcount-bits": 16,
    +            "corrupt": false,
    +            "extended-l2": false
    +        }
    +    },
    +    "full-backing-filename": "foo.qcow2",
    +    "backing-filename": "foo.qcow2",
    +    "dirty-flag": false
    +}`
    +			info, err := ParseInfo([]byte(s))
    +			assert.NilError(t, err)
    +			assert.Equal(t, 1, len(info.Children))
    +			assert.Equal(t, "foo.qcow2", info.BackingFilename)
    +			assert.Equal(t, "bar.qcow2", info.Filename)
    +			assert.Check(t, info.FormatSpecific != nil)
    +			qcow2 := info.FormatSpecific.Qcow2()
    +			assert.Check(t, qcow2 != nil)
    +			assert.Equal(t, qcow2.Compat, "1.1")
    +		})
    +	})
    +	t.Run("vmdk", func(t *testing.T) {
    +		t.Run("twoGbMaxExtentSparse", func(t *testing.T) {
    +			// qemu-img create -f vmdk foo.vmdk 4G -o subformat=twoGbMaxExtentSparse
    +			// (QEMU 8.0)
    +			const s = `{
    +    "children": [
    +        {
    +            "name": "extents.1",
    +            "info": {
    +                "children": [
    +                ],
    +                "virtual-size": 327680,
    +                "filename": "foo-s002.vmdk",
    +                "format": "file",
    +                "actual-size": 327680,
    +                "format-specific": {
    +                    "type": "file",
    +                    "data": {
    +                    }
    +                },
    +                "dirty-flag": false
    +            }
    +        },
    +        {
    +            "name": "extents.0",
    +            "info": {
    +                "children": [
    +                ],
    +                "virtual-size": 327680,
    +                "filename": "foo-s001.vmdk",
    +                "format": "file",
    +                "actual-size": 327680,
    +                "format-specific": {
    +                    "type": "file",
    +                    "data": {
    +                    }
    +                },
    +                "dirty-flag": false
    +            }
    +        },
    +        {
    +            "name": "file",
    +            "info": {
    +                "children": [
    +                ],
    +                "virtual-size": 512,
    +                "filename": "foo.vmdk",
    +                "format": "file",
    +                "actual-size": 4096,
    +                "format-specific": {
    +                    "type": "file",
    +                    "data": {
    +                    }
    +                },
    +                "dirty-flag": false
    +            }
    +        }
    +    ],
    +    "virtual-size": 4294967296,
    +    "filename": "foo.vmdk",
    +    "cluster-size": 65536,
    +    "format": "vmdk",
    +    "actual-size": 659456,
    +    "format-specific": {
    +        "type": "vmdk",
    +        "data": {
    +            "cid": 918420663,
    +            "parent-cid": 4294967295,
    +            "create-type": "twoGbMaxExtentSparse",
    +            "extents": [
    +                {
    +                    "virtual-size": 2147483648,
    +                    "filename": "foo-s001.vmdk",
    +                    "cluster-size": 65536,
    +                    "format": "SPARSE"
    +                },
    +                {
    +                    "virtual-size": 2147483648,
    +                    "filename": "foo-s002.vmdk",
    +                    "cluster-size": 65536,
    +                    "format": "SPARSE"
    +                }
    +            ]
    +        }
    +    },
    +    "dirty-flag": false
    +}`
    +			info, err := ParseInfo([]byte(s))
    +			assert.NilError(t, err)
    +			assert.Equal(t, 3, len(info.Children))
    +			assert.Equal(t, "foo.vmdk", info.Filename)
    +			assert.Check(t, info.FormatSpecific != nil)
    +			vmdk := info.FormatSpecific.Vmdk()
    +			assert.Check(t, vmdk != nil)
    +			assert.Equal(t, vmdk.CreateType, "twoGbMaxExtentSparse")
    +		})
    +	})
    +}
    
  • pkg/qemu/qemu.go+23 7 modified
    @@ -96,13 +96,19 @@ func EnsureDisk(cfg Config) error {
     	if err != nil {
     		return err
     	}
    +	baseDiskInfo, err := imgutil.GetInfo(baseDisk)
    +	if err != nil {
    +		return fmt.Errorf("failed to get the information of base disk %q: %w", baseDisk, err)
    +	}
    +	if err = imgutil.AcceptableAsBasedisk(baseDiskInfo); err != nil {
    +		return fmt.Errorf("file %q is not acceptable as the base disk: %w", baseDisk, err)
    +	}
    +	if baseDiskInfo.Format == "" {
    +		return fmt.Errorf("failed to inspect the format of %q", baseDisk)
    +	}
     	args := []string{"create", "-f", "qcow2"}
     	if !isBaseDiskISO {
    -		baseDiskFormat, err := imgutil.DetectFormat(baseDisk)
    -		if err != nil {
    -			return err
    -		}
    -		args = append(args, "-F", baseDiskFormat, "-b", baseDisk)
    +		args = append(args, "-F", baseDiskInfo.Format, "-b", baseDisk)
     	}
     	args = append(args, diffDisk, strconv.Itoa(int(diskSize)))
     	cmd := exec.Command("qemu-img", args...)
    @@ -570,14 +576,24 @@ func Cmdline(cfg Config) (string, []string, error) {
     	}
     	if isBaseDiskCDROM {
     		args = appendArgsIfNoConflict(args, "-boot", "order=d,splash-time=0,menu=on")
    -		args = append(args, "-drive", fmt.Sprintf("file=%s,media=cdrom,readonly=on", baseDisk))
    +		args = append(args, "-drive", fmt.Sprintf("file=%s,format=raw,media=cdrom,readonly=on", baseDisk))
     	} else {
     		args = appendArgsIfNoConflict(args, "-boot", "order=c,splash-time=0,menu=on")
     	}
     	if diskSize, _ := units.RAMInBytes(*cfg.LimaYAML.Disk); diskSize > 0 {
     		args = append(args, "-drive", fmt.Sprintf("file=%s,if=virtio,discard=on", diffDisk))
     	} else if !isBaseDiskCDROM {
    -		args = append(args, "-drive", fmt.Sprintf("file=%s,if=virtio,discard=on", baseDisk))
    +		baseDiskInfo, err := imgutil.GetInfo(baseDisk)
    +		if err != nil {
    +			return "", nil, fmt.Errorf("failed to get the information of %q: %w", baseDisk, err)
    +		}
    +		if err = imgutil.AcceptableAsBasedisk(baseDiskInfo); err != nil {
    +			return "", nil, fmt.Errorf("file %q is not acceptable as the base disk: %w", baseDisk, err)
    +		}
    +		if baseDiskInfo.Format == "" {
    +			return "", nil, fmt.Errorf("failed to inspect the format of %q", baseDisk)
    +		}
    +		args = append(args, "-drive", fmt.Sprintf("file=%s,format=%s,if=virtio,discard=on", baseDisk, baseDiskInfo.Format))
     	}
     	for _, extraDisk := range extraDisks {
     		args = append(args, "-drive", fmt.Sprintf("file=%s,if=virtio,discard=on", extraDisk))
    
  • pkg/vz/disk.go+11 5 modified
    @@ -49,13 +49,19 @@ func EnsureDisk(driver *driver.BaseDriver) error {
     	if err != nil {
     		return err
     	}
    +	baseDiskInfo, err := imgutil.GetInfo(baseDisk)
    +	if err != nil {
    +		return fmt.Errorf("failed to get the information of base disk %q: %w", baseDisk, err)
    +	}
    +	if err = imgutil.AcceptableAsBasedisk(baseDiskInfo); err != nil {
    +		return fmt.Errorf("file %q is not acceptable as the base disk: %w", baseDisk, err)
    +	}
    +	if baseDiskInfo.Format == "" {
    +		return fmt.Errorf("failed to inspect the format of %q", baseDisk)
    +	}
     	args := []string{"create", "-f", "qcow2"}
     	if !isBaseDiskISO {
    -		baseDiskFormat, err := imgutil.DetectFormat(baseDisk)
    -		if err != nil {
    -			return err
    -		}
    -		args = append(args, "-F", baseDiskFormat, "-b", baseDisk)
    +		args = append(args, "-F", baseDiskInfo.Format, "-b", baseDisk)
     	}
     	args = append(args, diffDisk, strconv.Itoa(int(diskSize)))
     	cmd := exec.Command("qemu-img", args...)
    
  • pkg/vz/vm_darwin.go+9 9 modified
    @@ -373,12 +373,12 @@ func attachNetwork(driver *driver.BaseDriver, vmConfig *vz.VirtualMachineConfigu
     }
     
     func validateDiskFormat(diskPath string) error {
    -	format, err := imgutil.DetectFormat(diskPath)
    +	info, err := imgutil.GetInfo(diskPath)
     	if err != nil {
     		return fmt.Errorf("failed to detect the format of %q: %w", diskPath, err)
     	}
    -	if format != "raw" {
    -		return fmt.Errorf("expected the format of %q to be \"raw\", got %q", diskPath, format)
    +	if info.Format != "raw" {
    +		return fmt.Errorf("expected the format of %q to be \"raw\", got %q", diskPath, info.Format)
     	}
     	// TODO: ensure that the disk is formatted with GPT or ISO9660
     	return nil
    @@ -437,23 +437,23 @@ func attachDisks(driver *driver.BaseDriver, vmConfig *vz.VirtualMachineConfigura
     		}
     		extraDiskPath := filepath.Join(d.Dir, filenames.DataDisk)
     
    -		extraDiskFormat, err := imgutil.DetectFormat(extraDiskPath)
    +		extraDiskInfo, err := imgutil.GetInfo(extraDiskPath)
     		if err != nil {
     			return fmt.Errorf("failed to run detect disk format %q: %q", diskName, err)
     		}
    -		if extraDiskFormat != "raw" {
    -			if strings.Contains(extraDiskFormat, string(os.PathSeparator)) {
    +		if extraDiskInfo.Format != "raw" {
    +			if strings.Contains(extraDiskInfo.Format, string(os.PathSeparator)) {
     				return fmt.Errorf(
     					"failed to convert disk %q to raw for vz driver because extraDiskFormat %q contains a path separator %q",
     					diskName,
    -					extraDiskFormat,
    +					extraDiskInfo.Format,
     					os.PathSeparator,
     				)
     			}
     			rawPath := fmt.Sprintf("%s.raw", extraDiskPath)
    -			oldFormatPath := fmt.Sprintf("%s.%s", extraDiskPath, extraDiskFormat)
    +			oldFormatPath := fmt.Sprintf("%s.%s", extraDiskPath, extraDiskInfo.Format)
     			if err = imgutil.ConvertToRaw(extraDiskPath, rawPath); err != nil {
    -				return fmt.Errorf("failed to convert %s disk %q to raw for vz driver: %w", extraDiskFormat, diskName, err)
    +				return fmt.Errorf("failed to convert %s disk %q to raw for vz driver: %w", extraDiskInfo.Format, diskName, err)
     			}
     			if err = os.Rename(extraDiskPath, oldFormatPath); err != nil {
     				return fmt.Errorf("failed to rename additional disk for vz driver: %w", err)
    

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

5

News mentions

0

No linked articles in our index yet.