Repository: lima-vm/lima
Pull request #3650 (2025-06-21 16:45:22)

defined a common interface for nativeimgutil & qemuimgutil 
fixes : #3518

Define the common interface for qemuimgutil and nativeimgutil



Patch:
diff --git a/.github/workflows/codeql.yaml b/.github/workflows/codeql.yaml
index e2af9dc7..15d1155f 100644
--- a/.github/workflows/codeql.yaml
+++ b/.github/workflows/codeql.yaml
@@ -32,7 +32,7 @@ jobs:
       uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4.2.2
 
     - name: Initialize CodeQL
-      uses: github/codeql-action/init@181d5eefc20863364f96762470ba6f862bdef56b  # v3.29.2
+      uses: github/codeql-action/init@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858  # v3.29.0
       with:
         languages: ${{ matrix.language }}
         build-mode: ${{ matrix.build-mode }}
@@ -48,6 +48,6 @@ jobs:
         exit 1
 
     - name: Perform CodeQL Analysis
-      uses: github/codeql-action/analyze@181d5eefc20863364f96762470ba6f862bdef56b  # v3.29.2
+      uses: github/codeql-action/analyze@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858  # v3.29.0
       with:
         category: "/language:${{matrix.language}}"
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 510516ad..0cf4435d 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -92,12 +92,12 @@ jobs:
 
         ## Usage
         \`\`\`console
-        $ limactl create
-        $ limactl start
+        [macOS]$ limactl create
+        [macOS]$ limactl start
         ...
         INFO[0029] READY. Run \`lima\` to open the shell.
 
-        $ lima uname
+        [macOS]$ lima uname
         Linux
         \`\`\`
 
diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml
index fe2a1e20..49f7a95e 100644
--- a/.github/workflows/scorecard.yml
+++ b/.github/workflows/scorecard.yml
@@ -60,6 +60,6 @@ jobs:
     # Upload the results to GitHub's code scanning dashboard (optional).
     # Commenting out will disable upload of results to your repo's Code Scanning dashboard
     - name: "Upload to code-scanning"
-      uses: github/codeql-action/upload-sarif@181d5eefc20863364f96762470ba6f862bdef56b  # v3.29.2
+      uses: github/codeql-action/upload-sarif@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858  # v3.29.0
       with:
         sarif_file: results.sarif
diff --git a/README.md b/README.md
index fcf1b45e..a264a197 100644
--- a/README.md
+++ b/README.md
@@ -21,7 +21,7 @@ to Mac users, but Lima can be used for non-container applications as well.
 Lima also supports other container engines (Docker, Podman, Kubernetes, etc.) and non-macOS hosts (Linux, NetBSD, etc.).
 
 ## Getting started
-Set up (Homebrew):
+Set up (on macOS):
 ```bash
 brew install lima
 limactl start
diff --git a/cmd/limactl/disk.go b/cmd/limactl/disk.go
index cf95c016..7f19b069 100644
--- a/cmd/limactl/disk.go
+++ b/cmd/limactl/disk.go
@@ -18,8 +18,7 @@ import (
 	"github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
 
-	"github.com/lima-vm/lima/pkg/nativeimgutil"
-	"github.com/lima-vm/lima/pkg/qemu/imgutil"
+	"github.com/lima-vm/lima/pkg/imgutil/proxyimgutil"
 	"github.com/lima-vm/lima/pkg/store"
 	"github.com/lima-vm/lima/pkg/store/filenames"
 )
@@ -113,11 +112,8 @@ func diskCreateAction(cmd *cobra.Command, args []string) error {
 
 	// qemu may not be available, use it only if needed.
 	dataDisk := filepath.Join(diskDir, filenames.DataDisk)
-	if format == "raw" {
-		err = nativeimgutil.CreateRawDisk(dataDisk, int(diskSize))
-	} else {
-		err = imgutil.CreateDisk(dataDisk, format, int(diskSize))
-	}
+	diskUtil := proxyimgutil.NewDiskUtil()
+	err = diskUtil.CreateDisk(dataDisk, diskSize)
 	if err != nil {
 		rerr := os.RemoveAll(diskDir)
 		if rerr != nil {
@@ -410,11 +406,8 @@ func diskResizeAction(cmd *cobra.Command, args []string) error {
 
 	// qemu may not be available, use it only if needed.
 	dataDisk := filepath.Join(disk.Dir, filenames.DataDisk)
-	if disk.Format == "raw" {
-		err = nativeimgutil.ResizeRawDisk(dataDisk, int(diskSize))
-	} else {
-		err = imgutil.ResizeDisk(dataDisk, disk.Format, int(diskSize))
-	}
+	diskUtil := proxyimgutil.NewDiskUtil()
+	err = diskUtil.ResizeDisk(dataDisk, diskSize)
 	if err != nil {
 		return fmt.Errorf("failed to resize disk %q: %w", diskName, err)
 	}
diff --git a/cmd/limactl/edit.go b/cmd/limactl/edit.go
index 6474d5e8..33ee9581 100644
--- a/cmd/limactl/edit.go
+++ b/cmd/limactl/edit.go
@@ -118,13 +118,13 @@ func editAction(cmd *cobra.Command, args []string) error {
 		return err
 	}
 	if err := limayaml.Validate(y, true); err != nil {
-		return saveRejectedYAML(yBytes, err)
-	}
-
-	if err := limayaml.ValidateAgainstLatestConfig(yBytes, yContent); err != nil {
-		return saveRejectedYAML(yBytes, err)
+		rejectedYAML := "lima.REJECTED.yaml"
+		if writeErr := os.WriteFile(rejectedYAML, yBytes, 0o644); writeErr != nil {
+			return fmt.Errorf("the YAML is invalid, attempted to save the buffer as %q but failed: %w: %w", rejectedYAML, writeErr, err)
+		}
+		// TODO: may need to support editing the rejected YAML
+		return fmt.Errorf("the YAML is invalid, saved the buffer as %q: %w", rejectedYAML, err)
 	}
-
 	if err := os.WriteFile(filePath, yBytes, 0o644); err != nil {
 		return err
 	}
@@ -171,13 +171,3 @@ func askWhetherToStart() (bool, error) {
 func editBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
 	return bashCompleteInstanceNames(cmd)
 }
-
-// saveRejectedYAML writes the rejected config and returns an error.
-func saveRejectedYAML(y []byte, origErr error) error {
-	rejectedYAML := "lima.REJECTED.yaml"
-	if writeErr := os.WriteFile(rejectedYAML, y, 0o644); writeErr != nil {
-		return fmt.Errorf("the YAML is invalid, attempted to save the buffer as %q but failed: %w", rejectedYAML, errors.Join(writeErr, origErr))
-	}
-	// TODO: may need to support editing the rejected YAML
-	return fmt.Errorf("the YAML is invalid, saved the buffer as %q: %w", rejectedYAML, origErr)
-}
diff --git a/cmd/limactl/start.go b/cmd/limactl/start.go
index 0d78bc1e..3f90f66e 100644
--- a/cmd/limactl/start.go
+++ b/cmd/limactl/start.go
@@ -4,7 +4,6 @@
 package main
 
 import (
-	"context"
 	"errors"
 	"fmt"
 	"os"
@@ -224,7 +223,7 @@ func loadOrCreateInstance(cmd *cobra.Command, args []string, createOnly bool) (*
 	yq := yqutil.Join(yqExprs)
 	if tty {
 		var err error
-		tmpl, err = chooseNextCreatorState(cmd.Context(), tmpl, yq)
+		tmpl, err = chooseNextCreatorState(tmpl, yq)
 		if err != nil {
 			return nil, err
 		}
@@ -295,7 +294,7 @@ func (exitSuccessError) ExitCode() int {
 	return 0
 }
 
-func chooseNextCreatorState(ctx context.Context, tmpl *limatmpl.Template, yq string) (*limatmpl.Template, error) {
+func chooseNextCreatorState(tmpl *limatmpl.Template, yq string) (*limatmpl.Template, error) {
 	for {
 		if err := modifyInPlace(tmpl, yq); err != nil {
 			logrus.WithError(err).Warn("Failed to evaluate yq expression")
@@ -362,11 +361,7 @@ func chooseNextCreatorState(ctx context.Context, tmpl *limatmpl.Template, yq str
 					return nil, err
 				}
 			}
-			tmpl, err = limatmpl.Read(ctx, tmpl.Name, yamlPath)
-			if err != nil {
-				return nil, err
-			}
-			err = tmpl.Embed(ctx, true, true)
+			tmpl.Bytes, err = os.ReadFile(yamlPath)
 			if err != nil {
 				return nil, err
 			}
diff --git a/go.mod b/go.mod
index 3747bc32..14a53959 100644
--- a/go.mod
+++ b/go.mod
@@ -22,7 +22,7 @@ require (
 	github.com/foxcpp/go-mockdns v1.1.0
 	github.com/goccy/go-yaml v1.18.0
 	github.com/google/go-cmp v0.7.0
-	github.com/google/yamlfmt v0.17.2
+	github.com/google/yamlfmt v0.17.1
 	github.com/invopop/jsonschema v0.13.0
 	github.com/lima-vm/go-qcow2reader v0.6.0
 	github.com/lima-vm/sshocker v0.3.8 // gomodjail:unconfined
diff --git a/go.sum b/go.sum
index 2f211757..fb72ed4d 100644
--- a/go.sum
+++ b/go.sum
@@ -123,8 +123,8 @@ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaU
 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/google/yamlfmt v0.17.2 h1:TkXxhmj7dnpmOnlWGOXog92Gs6MWcTZqnf3kuyp8yFQ=
-github.com/google/yamlfmt v0.17.2/go.mod h1:gs0UEklJOYkUJ+OOCG0hg9n+DzucKDPlJElTUasVNK8=
+github.com/google/yamlfmt v0.17.1 h1:NgKi21+MUVntxhPhIsstylN6QNCCfDv3wKNKwlm/3Hs=
+github.com/google/yamlfmt v0.17.1/go.mod h1:gs0UEklJOYkUJ+OOCG0hg9n+DzucKDPlJElTUasVNK8=
 github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
 github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
 github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
diff --git a/pkg/imgutil/manager.go b/pkg/imgutil/manager.go
new file mode 100644
index 00000000..a7f16ad7
--- /dev/null
+++ b/pkg/imgutil/manager.go
@@ -0,0 +1,23 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package imgutil
+
+import (
+	"os"
+)
+
+// ImageDiskManager defines the common operations for disk image utilities.
+type ImageDiskManager interface {
+	// CreateDisk creates a new disk image with the specified size.
+	CreateDisk(disk string, size int64) error
+
+	// ResizeDisk resizes an existing disk image to the specified size.
+	ResizeDisk(disk string, size int64) error
+
+	// ConvertToRaw converts a disk image to raw format.
+	ConvertToRaw(source, dest string, size *int64, allowSourceWithBackingFile bool) error
+
+	// MakeSparse makes a file sparse, starting from the specified offset.
+	MakeSparse(f *os.File, offset int64) error
+}
diff --git a/pkg/nativeimgutil/nativeimgutil.go b/pkg/imgutil/nativeimgutil/nativeimgutil.go
similarity index 72%
rename from pkg/nativeimgutil/nativeimgutil.go
rename to pkg/imgutil/nativeimgutil/nativeimgutil.go
index b4ccb0ea..910512d9 100644
--- a/pkg/nativeimgutil/nativeimgutil.go
+++ b/pkg/imgutil/nativeimgutil/nativeimgutil.go
@@ -28,36 +28,19 @@ import (
 // aligned to 512 bytes.
 const sectorSize = 512
 
-// RoundUp rounds size up to sectorSize.
-func RoundUp(size int) int {
+// NativeImageUtil is the native implementation of the imgutil.ImageDiskManager.
+type NativeImageUtil struct{}
+
+// roundUp rounds size up to sectorSize.
+func roundUp(size int64) int64 {
 	sectors := (size + sectorSize - 1) / sectorSize
 	return sectors * sectorSize
 }
 
-// CreateRawDisk creates an empty raw data disk.
-func CreateRawDisk(disk string, size int) error {
-	if _, err := os.Stat(disk); err == nil || !errors.Is(err, fs.ErrNotExist) {
-		return err
-	}
-	f, err := os.Create(disk)
-	if err != nil {
-		return err
-	}
-	defer f.Close()
-	roundedSize := RoundUp(size)
-	return f.Truncate(int64(roundedSize))
-}
-
-// ResizeRawDisk resizes a raw data disk.
-func ResizeRawDisk(disk string, size int) error {
-	roundedSize := RoundUp(size)
-	return os.Truncate(disk, int64(roundedSize))
-}
-
-// ConvertToRaw converts a source disk into a raw disk.
+// convertToRaw converts a source disk into a raw disk.
 // source and dest may be same.
-// ConvertToRaw is a NOP if source == dest, and no resizing is needed.
-func ConvertToRaw(source, dest string, size *int64, allowSourceWithBackingFile bool) error {
+// convertToRaw is a NOP if source == dest, and no resizing is needed.
+func convertToRaw(source, dest string, size *int64, allowSourceWithBackingFile bool) error {
 	srcF, err := os.Open(source)
 	if err != nil {
 		return err
@@ -106,7 +89,7 @@ func ConvertToRaw(source, dest string, size *int64, allowSourceWithBackingFile b
 	// Truncating before copy eliminates the seeks during copy and provide a
 	// hint to the file system that may minimize allocations and fragmentation
 	// of the file.
-	if err := MakeSparse(destTmpF, srcImg.Size()); err != nil {
+	if err := makeSparse(destTmpF, srcImg.Size()); err != nil {
 		return err
 	}
 
@@ -125,7 +108,7 @@ func ConvertToRaw(source, dest string, size *int64, allowSourceWithBackingFile b
 	// Resize
 	if size != nil {
 		logrus.Infof("Expanding to %s", units.BytesSize(float64(*size)))
-		if err = MakeSparse(destTmpF, *size); err != nil {
+		if err = makeSparse(destTmpF, *size); err != nil {
 			return err
 		}
 	}
@@ -153,7 +136,7 @@ func convertRawToRaw(source, dest string, size *int64) error {
 		if err != nil {
 			return err
 		}
-		if err = MakeSparse(destF, *size); err != nil {
+		if err = makeSparse(destF, *size); err != nil {
 			_ = destF.Close()
 			return err
 		}
@@ -162,9 +145,39 @@ func convertRawToRaw(source, dest string, size *int64) error {
 	return nil
 }
 
-func MakeSparse(f *os.File, n int64) error {
-	if _, err := f.Seek(n, io.SeekStart); err != nil {
+func makeSparse(f *os.File, offset int64) error {
+	if _, err := f.Seek(offset, io.SeekStart); err != nil {
 		return err
 	}
-	return f.Truncate(n)
+	return f.Truncate(offset)
+}
+
+// CreateDisk creates a new disk image with the specified size.
+func (n *NativeImageUtil) CreateDisk(disk string, size int64) error {
+	if _, err := os.Stat(disk); err == nil || !errors.Is(err, fs.ErrNotExist) {
+		return err
+	}
+	f, err := os.Create(disk)
+	if err != nil {
+		return err
+	}
+	defer f.Close()
+	roundedSize := roundUp(size)
+	return f.Truncate(int64(roundedSize))
+}
+
+// ConvertToRaw converts a disk image to raw format.
+func (n *NativeImageUtil) ConvertToRaw(source, dest string, size *int64, allowSourceWithBackingFile bool) error {
+	return convertToRaw(source, dest, size, allowSourceWithBackingFile)
+}
+
+// ResizeDisk resizes an existing disk image to the specified size.
+func (n *NativeImageUtil) ResizeDisk(disk string, size int64) error {
+	roundedSize := roundUp(size)
+	return os.Truncate(disk, roundedSize)
+}
+
+// MakeSparse makes a file sparse, starting from the specified offset.
+func (n *NativeImageUtil) MakeSparse(f *os.File, offset int64) error {
+	return makeSparse(f, offset)
 }
diff --git a/pkg/imgutil/proxyimgutil/proxyimgutil.go b/pkg/imgutil/proxyimgutil/proxyimgutil.go
new file mode 100644
index 00000000..7ff88abc
--- /dev/null
+++ b/pkg/imgutil/proxyimgutil/proxyimgutil.go
@@ -0,0 +1,75 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package proxyimgutil
+
+import (
+	"errors"
+	"os"
+	"os/exec"
+
+	"github.com/lima-vm/lima/pkg/imgutil"
+	"github.com/lima-vm/lima/pkg/imgutil/nativeimgutil"
+	"github.com/lima-vm/lima/pkg/imgutil/qemuimgutil"
+)
+
+// ImageDiskManager is a proxy implementation of imgutil.ImageDiskManager that uses both QEMU and native image utilities.
+type ImageDiskManager struct {
+	qemu   imgutil.ImageDiskManager
+	native imgutil.ImageDiskManager
+}
+
+// NewDiskUtil returns a new instance of ImageDiskManager that uses both QEMU and native image utilities.
+func NewDiskUtil() imgutil.ImageDiskManager {
+	return &ImageDiskManager{
+		qemu:   &qemuimgutil.QemuImageUtil{DefaultFormat: qemuimgutil.QemuImgFormat},
+		native: &nativeimgutil.NativeImageUtil{},
+	}
+}
+
+// CreateDisk creates a new disk image with the specified size.
+func (p *ImageDiskManager) CreateDisk(disk string, size int64) error {
+	err := p.qemu.CreateDisk(disk, size)
+	if err == nil {
+		return nil
+	}
+	if errors.Is(err, exec.ErrNotFound) {
+		return p.native.CreateDisk(disk, size)
+	}
+	return err
+}
+
+// ResizeDisk resizes an existing disk image to the specified size.
+func (p *ImageDiskManager) ResizeDisk(disk string, size int64) error {
+	err := p.qemu.ResizeDisk(disk, size)
+	if err == nil {
+		return nil
+	}
+	if errors.Is(err, exec.ErrNotFound) {
+		return p.native.ResizeDisk(disk, size)
+	}
+	return err
+}
+
+// ConvertToRaw converts a disk image to raw format.
+func (p *ImageDiskManager) ConvertToRaw(source, dest string, size *int64, allowSourceWithBackingFile bool) error {
+	err := p.qemu.ConvertToRaw(source, dest, size, allowSourceWithBackingFile)
+	if err == nil {
+		return nil
+	}
+	if errors.Is(err, exec.ErrNotFound) {
+		return p.native.ConvertToRaw(source, dest, size, allowSourceWithBackingFile)
+	}
+	return err
+}
+
+func (p *ImageDiskManager) MakeSparse(f *os.File, offset int64) error {
+	err := p.qemu.MakeSparse(f, offset)
+	if err == nil {
+		return nil
+	}
+	if errors.Is(err, exec.ErrNotFound) {
+		return p.native.MakeSparse(f, offset)
+	}
+	return err
+}
diff --git a/pkg/qemu/imgutil/imgutil.go b/pkg/imgutil/qemuimgutil/qemuimgutil.go
similarity index 72%
rename from pkg/qemu/imgutil/imgutil.go
rename to pkg/imgutil/qemuimgutil/qemuimgutil.go
index 18512bf0..fc065450 100644
--- a/pkg/qemu/imgutil/imgutil.go
+++ b/pkg/imgutil/qemuimgutil/qemuimgutil.go
@@ -1,7 +1,7 @@
 // SPDX-FileCopyrightText: Copyright The Lima Authors
 // SPDX-License-Identifier: Apache-2.0
 
-package imgutil
+package qemuimgutil
 
 import (
 	"bytes"
@@ -16,6 +16,28 @@ import (
 	"github.com/sirupsen/logrus"
 )
 
+const QemuImgFormat = "qcow2"
+
+// QemuImageUtil is the QEMU implementation of the imgutil Interface.
+type QemuImageUtil struct {
+	DefaultFormat string // Default disk format, e.g., "qcow2"
+}
+
+// Info corresponds to the output of `qemu-img info --output=json FILE`.
+type Info struct {
+	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
+}
+
 type InfoChild struct {
 	Name string `json:"name,omitempty"` // since QEMU 8.0
 	Info Info   `json:"info,omitempty"` // since QEMU 8.0
@@ -26,22 +48,8 @@ type InfoFormatSpecific struct {
 	Data json.RawMessage `json:"data,omitempty"` // since QEMU 1.7
 }
 
-func CreateDisk(disk, format string, size int) error {
-	if _, err := os.Stat(disk); err == nil || !errors.Is(err, fs.ErrNotExist) {
-		// disk already exists
-		return err
-	}
-
-	args := []string{"create", "-f", format, disk, strconv.Itoa(size)}
-	cmd := exec.Command("qemu-img", args...)
-	if out, err := cmd.CombinedOutput(); err != nil {
-		return fmt.Errorf("failed to run %v: %q: %w", cmd.Args, string(out), err)
-	}
-	return nil
-}
-
-func ResizeDisk(disk, format string, size int) error {
-	args := []string{"resize", "-f", format, disk, strconv.Itoa(size)}
+func resizeDisk(disk, format string, size int64) error {
+	args := []string{"resize", "-f", format, disk, strconv.FormatInt(size, 10)}
 	cmd := exec.Command("qemu-img", args...)
 	if out, err := cmd.CombinedOutput(); err != nil {
 		return fmt.Errorf("failed to run %v: %q: %w", cmd.Args, string(out), err)
@@ -94,22 +102,7 @@ type InfoFormatSpecificDataVmdkExtent struct {
 	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 {
-	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, dest string) error {
+func convertToRaw(source, dest string) error {
 	var stdout, stderr bytes.Buffer
 	cmd := exec.Command("qemu-img", "convert", "-O", "raw", source, dest)
 	cmd.Stdout = &stdout
@@ -121,7 +114,7 @@ func ConvertToRaw(source, dest string) error {
 	return nil
 }
 
-func ParseInfo(b []byte) (*Info, error) {
+func parseInfo(b []byte) (*Info, error) {
 	var imgInfo Info
 	if err := json.Unmarshal(b, &imgInfo); err != nil {
 		return nil, err
@@ -129,7 +122,7 @@ func ParseInfo(b []byte) (*Info, error) {
 	return &imgInfo, nil
 }
 
-func GetInfo(f string) (*Info, error) {
+func getInfo(f string) (*Info, error) {
 	var stdout, stderr bytes.Buffer
 	cmd := exec.Command("qemu-img", "info", "--output=json", "--force-share", f)
 	cmd.Stdout = &stdout
@@ -138,10 +131,80 @@ 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)
 	}
-	return ParseInfo(stdout.Bytes())
+	return parseInfo(stdout.Bytes())
+}
+
+// CreateDisk creates a new disk image with the specified size.
+func (q *QemuImageUtil) CreateDisk(disk string, size int64) error {
+	if _, err := os.Stat(disk); err == nil || !errors.Is(err, fs.ErrNotExist) {
+		// disk already exists
+		return err
+	}
+
+	args := []string{"create", "-f", q.DefaultFormat, disk, strconv.FormatInt(size, 10)}
+	cmd := exec.Command("qemu-img", args...)
+	if out, err := cmd.CombinedOutput(); err != nil {
+		return fmt.Errorf("failed to run %v: %q: %w", cmd.Args, string(out), err)
+	}
+	return nil
+}
+
+// ResizeDisk resizes an existing disk image to the specified size.
+func (q *QemuImageUtil) ResizeDisk(disk string, size int64) error {
+	info, err := getInfo(disk)
+	if err != nil {
+		return fmt.Errorf("failed to get info for disk %q: %w", disk, err)
+	}
+	return resizeDisk(disk, info.Format, size)
+}
+
+// MakeSparse is a stub implementation as the qemu package doesn't provide this functionality.
+func (q *QemuImageUtil) MakeSparse(_ *os.File, _ int64) error {
+	return nil
+}
+
+// GetInfo retrieves the information of a disk image.
+func GetInfo(path string) (*Info, error) {
+	qemuInfo, err := getInfo(path)
+	if err != nil {
+		return nil, err
+	}
+
+	return qemuInfo, nil
+}
+
+// ConvertToRaw converts a disk image to raw format.
+func (q *QemuImageUtil) ConvertToRaw(source, dest string, size *int64, allowSourceWithBackingFile bool) error {
+	if !allowSourceWithBackingFile {
+		info, err := getInfo(source)
+		if err != nil {
+			return fmt.Errorf("failed to get info for source disk %q: %w", source, err)
+		}
+		if info.BackingFilename != "" || info.FullBackingFilename != "" {
+			return fmt.Errorf("qcow2 image %q has an unexpected backing file: %q", source, info.BackingFilename)
+		}
+	}
+
+	if err := convertToRaw(source, dest); err != nil {
+		return err
+	}
+
+	if size != nil {
+		destInfo, err := getInfo(dest)
+		if err != nil {
+			return fmt.Errorf("failed to get info for converted disk %q: %w", dest, err)
+		}
+
+		if *size > destInfo.VSize {
+			return resizeDisk(dest, "raw", *size)
+		}
+	}
+
+	return nil
 }
 
-func AcceptableAsBasedisk(info *Info) error {
+// AcceptableAsBaseDisk checks if a disk image is acceptable as a base disk.
+func AcceptableAsBaseDisk(info *Info) error {
 	switch info.Format {
 	case "qcow2", "raw":
 		// NOP
diff --git a/pkg/instance/start.go b/pkg/instance/start.go
index 1df00c78..acd3741e 100644
--- a/pkg/instance/start.go
+++ b/pkg/instance/start.go
@@ -27,10 +27,9 @@ import (
 	"github.com/lima-vm/lima/pkg/executil"
 	"github.com/lima-vm/lima/pkg/fileutils"
 	hostagentevents "github.com/lima-vm/lima/pkg/hostagent/events"
+	"github.com/lima-vm/lima/pkg/imgutil/proxyimgutil"
 	"github.com/lima-vm/lima/pkg/limayaml"
-	"github.com/lima-vm/lima/pkg/nativeimgutil"
 	"github.com/lima-vm/lima/pkg/osutil"
-	"github.com/lima-vm/lima/pkg/qemu/imgutil"
 	"github.com/lima-vm/lima/pkg/store"
 	"github.com/lima-vm/lima/pkg/store/filenames"
 	"github.com/lima-vm/lima/pkg/usrlocalsharelima"
@@ -414,7 +413,6 @@ func prepareDiffDisk(inst *store.Instance) error {
 	}
 
 	diskSize := img.Size()
-	format := string(img.Type())
 
 	if inst.Disk == diskSize {
 		return nil
@@ -427,12 +425,9 @@ func prepareDiffDisk(inst *store.Instance) error {
 		return errors.New("diffDisk: Shrinking is currently unavailable")
 	}
 
-	if format == "raw" {
-		err = nativeimgutil.ResizeRawDisk(diffDisk, int(inst.Disk))
-	} else {
-		err = imgutil.ResizeDisk(diffDisk, format, int(inst.Disk))
-	}
+	diskUtil := proxyimgutil.NewDiskUtil()
 
+	err = diskUtil.ResizeDisk(diffDisk, inst.Disk)
 	if err != nil {
 		return err
 	}
diff --git a/pkg/limayaml/defaults.go b/pkg/limayaml/defaults.go
index d9d2c622..a2193a28 100644
--- a/pkg/limayaml/defaults.go
+++ b/pkg/limayaml/defaults.go
@@ -699,12 +699,6 @@ func FillDefault(y, d, o *LimaYAML, filePath string, warn bool) {
 	for _, f := range y.MountTypesUnsupported {
 		mountTypesUnsupported[f] = struct{}{}
 	}
-
-	if runtime.GOOS == "windows" {
-		// QEMU for Windows does not support 9p
-		mountTypesUnsupported[NINEP] = struct{}{}
-	}
-
 	// MountType has to be resolved before resolving Mounts
 	if y.MountType == nil {
 		y.MountType = d.MountType
diff --git a/pkg/limayaml/load.go b/pkg/limayaml/load.go
index 3929d7ba..13f71e8d 100644
--- a/pkg/limayaml/load.go
+++ b/pkg/limayaml/load.go
@@ -63,7 +63,7 @@ func load(b []byte, filePath string, warn bool) (*LimaYAML, error) {
 	}
 
 	// It should be called before the `y` parameter is passed to FillDefault() that execute template.
-	if err := validateParamIsUsed(&y); err != nil {
+	if err := ValidateParamIsUsed(&y); err != nil {
 		return nil, err
 	}
 
diff --git a/pkg/limayaml/validate.go b/pkg/limayaml/validate.go
index 9b959f25..05aac468 100644
--- a/pkg/limayaml/validate.go
+++ b/pkg/limayaml/validate.go
@@ -29,6 +29,28 @@ import (
 	"github.com/lima-vm/lima/pkg/version/versionutil"
 )
 
+func validateFileObject(f File, fieldName string) error {
+	var errs error
+	if !strings.Contains(f.Location, "://") {
+		if _, err := localpathutil.Expand(f.Location); err != nil {
+			errs = errors.Join(errs, fmt.Errorf("field `%s.location` refers to an invalid local file path: %q: %w", fieldName, f.Location, err))
+		}
+		// f.Location does NOT need to be accessible, so we do NOT check os.Stat(f.Location)
+	}
+	if !slices.Contains(ArchTypes, f.Arch) {
+		errs = errors.Join(errs, fmt.Errorf("field `arch` must be one of %v; got %q", ArchTypes, f.Arch))
+	}
+	if f.Digest != "" {
+		if !f.Digest.Algorithm().Available() {
+			errs = errors.Join(errs, fmt.Errorf("field `%s.digest` refers to an unavailable digest algorithm", fieldName))
+		}
+		if err := f.Digest.Validate(); err != nil {
+			errs = errors.Join(errs, fmt.Errorf("field `%s.digest` is invalid: %s: %w", fieldName, f.Digest.String(), err))
+		}
+	}
+	return errs
+}
+
 func Validate(y *LimaYAML, warn bool) error {
 	var errs error
 
@@ -44,7 +66,7 @@ func Validate(y *LimaYAML, warn bool) error {
 		if err != nil {
 			errs = errors.Join(errs, fmt.Errorf("can't parse builtin Lima version %q: %w", version.Version, err))
 		}
-		if limaVersion != nil && versionutil.GreaterThan(*y.MinimumLimaVersion, limaVersion.String()) {
+		if versionutil.GreaterThan(*y.MinimumLimaVersion, limaVersion.String()) {
 			errs = errors.Join(errs, fmt.Errorf("template requires Lima version %q; this is only %q", *y.MinimumLimaVersion, limaVersion.String()))
 		}
 	}
@@ -419,28 +441,6 @@ func Validate(y *LimaYAML, warn bool) error {
 	return errs
 }
 
-func validateFileObject(f File, fieldName string) error {
-	var errs error
-	if !strings.Contains(f.Location, "://") {
-		if _, err := localpathutil.Expand(f.Location); err != nil {
-			errs = errors.Join(errs, fmt.Errorf("field `%s.location` refers to an invalid local file path: %q: %w", fieldName, f.Location, err))
-		}
-		// f.Location does NOT need to be accessible, so we do NOT check os.Stat(f.Location)
-	}
-	if !slices.Contains(ArchTypes, f.Arch) {
-		errs = errors.Join(errs, fmt.Errorf("field `arch` must be one of %v; got %q", ArchTypes, f.Arch))
-	}
-	if f.Digest != "" {
-		if !f.Digest.Algorithm().Available() {
-			errs = errors.Join(errs, fmt.Errorf("field `%s.digest` refers to an unavailable digest algorithm", fieldName))
-		}
-		if err := f.Digest.Validate(); err != nil {
-			errs = errors.Join(errs, fmt.Errorf("field `%s.digest` is invalid: %s: %w", fieldName, f.Digest.String(), err))
-		}
-	}
-	return errs
-}
-
 func validateNetwork(y *LimaYAML) error {
 	var errs error
 	interfaceName := make(map[string]int)
@@ -518,9 +518,9 @@ func validateNetwork(y *LimaYAML) error {
 	return errs
 }
 
-// validateParamIsUsed checks if the keys in the `param` field are used in any script, probe, copyToHost, or portForward.
+// ValidateParamIsUsed checks if the keys in the `param` field are used in any script, probe, copyToHost, or portForward.
 // It should be called before the `y` parameter is passed to FillDefault() that execute template.
-func validateParamIsUsed(y *LimaYAML) error {
+func ValidateParamIsUsed(y *LimaYAML) error {
 	for key := range y.Param {
 		re, err := regexp.Compile(`{{[^}]*\.Param\.` + key + `[^}]*}}|\bPARAM_` + key + `\b`)
 		if err != nil {
@@ -610,40 +610,3 @@ func warnExperimental(y *LimaYAML) {
 		logrus.Warn("`mountInotify` is experimental")
 	}
 }
-
-// ValidateAgainstLatestConfig validates the values between the latest YAML and the updated(New) YAML.
-// This validates configuration rules that disallow certain changes, such as shrinking the disk.
-func ValidateAgainstLatestConfig(yNew, yLatest []byte) error {
-	var n LimaYAML
-
-	// Load the latest YAML and fill in defaults
-	l, err := LoadWithWarnings(yLatest, "")
-	if err != nil {
-		return err
-	}
-	if err := Unmarshal(yNew, &n, "Unmarshal new YAML bytes"); err != nil {
-		return err
-	}
-
-	// Handle editing the template without a disk value
-	if n.Disk == nil || l.Disk == nil {
-		return nil
-	}
-
-	// Disk value must be provided, as it is required when creating an instance.
-	nDisk, err := units.RAMInBytes(*n.Disk)
-	if err != nil {
-		return err
-	}
-	lDisk, err := units.RAMInBytes(*l.Disk)
-	if err != nil {
-		return err
-	}
-
-	// Reject shrinking disk
-	if nDisk < lDisk {
-		return fmt.Errorf("field `disk`: shrinking the disk (%v --> %v) is not supported", *l.Disk, *n.Disk)
-	}
-
-	return nil
-}
diff --git a/pkg/qemu/qemu.go b/pkg/qemu/qemu.go
index 0ba01b61..8a54ba8a 100644
--- a/pkg/qemu/qemu.go
+++ b/pkg/qemu/qemu.go
@@ -28,12 +28,12 @@ import (
 	"github.com/sirupsen/logrus"
 
 	"github.com/lima-vm/lima/pkg/fileutils"
+	"github.com/lima-vm/lima/pkg/imgutil/qemuimgutil"
 	"github.com/lima-vm/lima/pkg/iso9660util"
 	"github.com/lima-vm/lima/pkg/limayaml"
 	"github.com/lima-vm/lima/pkg/networks"
 	"github.com/lima-vm/lima/pkg/networks/usernet"
 	"github.com/lima-vm/lima/pkg/osutil"
-	"github.com/lima-vm/lima/pkg/qemu/imgutil"
 	"github.com/lima-vm/lima/pkg/store"
 	"github.com/lima-vm/lima/pkg/store/filenames"
 )
@@ -137,11 +137,11 @@ func EnsureDisk(ctx context.Context, cfg Config) error {
 	if err != nil {
 		return err
 	}
-	baseDiskInfo, err := imgutil.GetInfo(baseDisk)
+	baseDiskInfo, err := qemuimgutil.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 {
+	if err = qemuimgutil.AcceptableAsBaseDisk(baseDiskInfo); err != nil {
 		return fmt.Errorf("file %q is not acceptable as the base disk: %w", baseDisk, err)
 	}
 	if baseDiskInfo.Format == "" {
@@ -691,11 +691,11 @@ func Cmdline(ctx context.Context, cfg Config) (exe string, args []string, err er
 	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 {
-		baseDiskInfo, err := imgutil.GetInfo(baseDisk)
+		baseDiskInfo, err := qemuimgutil.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 {
+		if err = qemuimgutil.AcceptableAsBaseDisk(baseDiskInfo); err != nil {
 			return "", nil, fmt.Errorf("file %q is not acceptable as the base disk: %w", baseDisk, err)
 		}
 		if baseDiskInfo.Format == "" {
diff --git a/pkg/usrlocalsharelima/usrlocalsharelima.go b/pkg/usrlocalsharelima/usrlocalsharelima.go
index 0f08e609..0c8e99c7 100644
--- a/pkg/usrlocalsharelima/usrlocalsharelima.go
+++ b/pkg/usrlocalsharelima/usrlocalsharelima.go
@@ -57,16 +57,8 @@ func Dir() (string, error) {
 	selfViaOS, err := os.Executable()
 	if err != nil {
 		logrus.WithError(err).Warn("failed to find os.Executable()")
-	} else {
-		selfFinalPathViaOS, err := filepath.EvalSymlinks(selfViaOS)
-		if err != nil {
-			logrus.WithError(err).Warn("failed to resolve symlinks")
-			selfFinalPathViaOS = selfViaOS // fallback to the original path
-		}
-
-		if len(selfPaths) == 0 || selfFinalPathViaOS != selfPaths[0] {
-			selfPaths = append(selfPaths, selfFinalPathViaOS)
-		}
+	} else if len(selfPaths) == 0 || selfViaOS != selfPaths[0] {
+		selfPaths = append(selfPaths, selfViaOS)
 	}
 
 	ostype := limayaml.NewOS("linux")
diff --git a/pkg/vz/disk.go b/pkg/vz/disk.go
index 3fff17c4..06194faf 100644
--- a/pkg/vz/disk.go
+++ b/pkg/vz/disk.go
@@ -14,8 +14,8 @@ import (
 
 	"github.com/lima-vm/lima/pkg/driver"
 	"github.com/lima-vm/lima/pkg/fileutils"
+	"github.com/lima-vm/lima/pkg/imgutil/proxyimgutil"
 	"github.com/lima-vm/lima/pkg/iso9660util"
-	"github.com/lima-vm/lima/pkg/nativeimgutil"
 	"github.com/lima-vm/lima/pkg/store/filenames"
 )
 
@@ -26,6 +26,8 @@ func EnsureDisk(ctx context.Context, driver *driver.BaseDriver) error {
 		return err
 	}
 
+	diskUtil := proxyimgutil.NewDiskUtil()
+
 	baseDisk := filepath.Join(driver.Instance.Dir, filenames.BaseDisk)
 	kernel := filepath.Join(driver.Instance.Dir, filenames.Kernel)
 	kernelCmdline := filepath.Join(driver.Instance.Dir, filenames.KernelCmdline)
@@ -78,13 +80,15 @@ func EnsureDisk(ctx context.Context, driver *driver.BaseDriver) error {
 		if err != nil {
 			return err
 		}
-		if err = nativeimgutil.MakeSparse(diffDiskF, diskSize); err != nil {
+
+		err = diskUtil.MakeSparse(diffDiskF, 0)
+		if err != nil {
 			diffDiskF.Close()
-			return err
+			return fmt.Errorf("failed to create sparse diff disk %q: %w", diffDisk, err)
 		}
 		return diffDiskF.Close()
 	}
-	if err = nativeimgutil.ConvertToRaw(baseDisk, diffDisk, &diskSize, false); err != nil {
+	if err = diskUtil.ConvertToRaw(baseDisk, diffDisk, &diskSize, false); err != nil {
 		return fmt.Errorf("failed to convert %q to a raw disk %q: %w", baseDisk, diffDisk, err)
 	}
 	return err
diff --git a/pkg/vz/vm_darwin.go b/pkg/vz/vm_darwin.go
index 1f21cf44..9ea33a86 100644
--- a/pkg/vz/vm_darwin.go
+++ b/pkg/vz/vm_darwin.go
@@ -25,9 +25,9 @@ import (
 	"github.com/sirupsen/logrus"
 
 	"github.com/lima-vm/lima/pkg/driver"
+	"github.com/lima-vm/lima/pkg/imgutil/proxyimgutil"
 	"github.com/lima-vm/lima/pkg/iso9660util"
 	"github.com/lima-vm/lima/pkg/limayaml"
-	"github.com/lima-vm/lima/pkg/nativeimgutil"
 	"github.com/lima-vm/lima/pkg/networks"
 	"github.com/lima-vm/lima/pkg/networks/usernet"
 	"github.com/lima-vm/lima/pkg/osutil"
@@ -471,6 +471,8 @@ func attachDisks(driver *driver.BaseDriver, vmConfig *vz.VirtualMachineConfigura
 	}
 	configurations = append(configurations, diffDisk)
 
+	diskUtil := proxyimgutil.NewDiskUtil()
+
 	for _, d := range driver.Instance.Config.AdditionalDisks {
 		diskName := d.Name
 		disk, err := store.InspectDisk(diskName)
@@ -489,7 +491,8 @@ func attachDisks(driver *driver.BaseDriver, vmConfig *vz.VirtualMachineConfigura
 		extraDiskPath := filepath.Join(disk.Dir, filenames.DataDisk)
 		// ConvertToRaw is a NOP if no conversion is needed
 		logrus.Debugf("Converting extra disk %q to a raw disk (if it is not a raw)", extraDiskPath)
-		if err = nativeimgutil.ConvertToRaw(extraDiskPath, extraDiskPath, nil, true); err != nil {
+
+		if err = diskUtil.ConvertToRaw(extraDiskPath, extraDiskPath, nil, true); err != nil {
 			return fmt.Errorf("failed to convert extra disk %q to a raw disk: %w", extraDiskPath, err)
 		}
 		extraDiskPathAttachment, err := vz.NewDiskImageStorageDeviceAttachmentWithCacheAndSync(extraDiskPath, false, diskImageCachingMode, vz.DiskImageSynchronizationModeFsync)
diff --git a/templates/README.md b/templates/README.md
index f6fe4c65..ace299c5 100644
--- a/templates/README.md
+++ b/templates/README.md
@@ -22,8 +22,8 @@ Distro:
 - [`centos-stream-10`](./centos-stream-10.yaml): CentOS Stream 10
 - [`debian-11`](./debian-11.yaml): Debian GNU/Linux 11(bullseye)
 - [`debian-12`](./debian-12.yaml), `debian.yaml`: ⭐Debian GNU/Linux 12(bookworm)
-- [`fedora-41`](./fedora-41.yaml): Fedora 41
-- [`fedora-42`](./fedora-42.yaml), `fedora.yaml`: ⭐Fedora 42
+- [`fedora-41`](./fedora-41.yaml), `fedora.yaml`: ⭐Fedora 41
+- [`fedora-42`](./fedora-42.yaml): Fedora 42
 - [`opensuse-leap`](./opensuse-leap.yaml), `opensuse.yaml`: ⭐openSUSE Leap
 - [`oraclelinux-8`](./oraclelinux-8.yaml): Oracle Linux 8
 - [`oraclelinux-9`](./oraclelinux-9.yaml), `oraclelinux.yaml`: Oracle Linux 9
diff --git a/templates/_images/fedora.yaml b/templates/_images/fedora.yaml
index 660722ae..98dcf55f 120000
--- a/templates/_images/fedora.yaml
+++ b/templates/_images/fedora.yaml
@@ -1,1 +1,1 @@
-fedora-42.yaml
\ No newline at end of file
+fedora-41.yaml
\ No newline at end of file
diff --git a/templates/_images/ubuntu.yaml b/templates/_images/ubuntu.yaml
index 3475af5c..9e033b6d 120000
--- a/templates/_images/ubuntu.yaml
+++ b/templates/_images/ubuntu.yaml
@@ -1,1 +1,1 @@
-ubuntu-25.04.yaml
\ No newline at end of file
+ubuntu-24.10.yaml
\ No newline at end of file
diff --git a/templates/default.yaml b/templates/default.yaml
index 11a88640..99f01ecc 100644
--- a/templates/default.yaml
+++ b/templates/default.yaml
@@ -91,11 +91,13 @@ mounts: []
 # The issue was fixed in Linux v6.12-rc5 (https://github.com/torvalds/linux/commit/be2ca38).
 #
 # 🟢 Builtin default: []
-mountTypesUnsupported: null
+# 🔵 This file: ["9p"] (as Ubuntu 24.10 uses kernel 6.11)
+mountTypesUnsupported:
+- "9p"
 
 # Mount type for above mounts, such as "reverse-sshfs" (from sshocker), "9p" (QEMU’s virtio-9p-pci, aka virtfs),
 # or "virtiofs" (experimental on Linux; needs `vmType: vz` on macOS).
-# 🟢 Builtin default: "default" (resolved to be "9p" for QEMU since Lima v1.0 on non-Windows, "virtiofs" for vz)
+# 🟢 Builtin default: "default" (resolved to be "9p" for QEMU since Lima v1.0, "virtiofs" for vz)
 mountType: null
 
 # Enable inotify support for mounted directories (EXPERIMENTAL)
diff --git a/templates/fedora.yaml b/templates/fedora.yaml
index 660722ae..98dcf55f 120000
--- a/templates/fedora.yaml
+++ b/templates/fedora.yaml
@@ -1,1 +1,1 @@
-fedora-42.yaml
\ No newline at end of file
+fedora-41.yaml
\ No newline at end of file
diff --git a/templates/podman-rootful.yaml b/templates/podman-rootful.yaml
index ba85ce09..c0dcebe9 100644
--- a/templates/podman-rootful.yaml
+++ b/templates/podman-rootful.yaml
@@ -13,7 +13,7 @@
 minimumLimaVersion: 1.1.0
 
 base:
-- template://_images/fedora
+- template://_images/fedora-41
 - template://_default/mounts
 
 containerd:
diff --git a/templates/podman.yaml b/templates/podman.yaml
index 968a4646..e50cf370 100644
--- a/templates/podman.yaml
+++ b/templates/podman.yaml
@@ -13,7 +13,7 @@
 minimumLimaVersion: 1.1.0
 
 base:
-- template://_images/fedora
+- template://_images/fedora-41
 - template://_default/mounts
 
 containerd:
diff --git a/templates/ubuntu.yaml b/templates/ubuntu.yaml
index 3475af5c..9e033b6d 120000
--- a/templates/ubuntu.yaml
+++ b/templates/ubuntu.yaml
@@ -1,1 +1,1 @@
-ubuntu-25.04.yaml
\ No newline at end of file
+ubuntu-24.10.yaml
\ No newline at end of file
diff --git a/website/content/en/docs/config/disk.md b/website/content/en/docs/config/disk.md
index 6f2207c8..db687930 100644
--- a/website/content/en/docs/config/disk.md
+++ b/website/content/en/docs/config/disk.md
@@ -2,7 +2,7 @@
 title: Disks
 ---
 
-This guide explains how to increase the disk size of a Lima VM when you've run out of space, as well as how to edit the disk size using the `limactl` CLI.
+This guide explains how to increase the disk size of a Lima VM running on macOS when you've run out of space, as well as how to edit the disk size using the `limactl` CLI.
 
 ## Resize Disk Using limactl
 
diff --git a/website/content/en/docs/config/mount.md b/website/content/en/docs/config/mount.md
index 4478db26..dd76e6e5 100644
--- a/website/content/en/docs/config/mount.md
+++ b/website/content/en/docs/config/mount.md
@@ -12,7 +12,7 @@ The default mount type is shown in the following table:
 | < 0.10       | reverse-sshfs + Builtin SFTP server                           |
 | >= 0.10      | reverse-sshfs + OpenSSH SFTP server                           |
 | >= 0.17      | reverse-sshfs + OpenSSH SFTP server for QEMU, virtiofs for VZ |
-| >= 1.0       | 9p for QEMU (on non-Windows), virtiofs for VZ                 |
+| >= 1.0       | 9p for QEMU, virtiofs for VZ                                  |
 
 ## Mount types
 


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 72a95673..c3670fd3 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -63,7 +63,9 @@ jobs:
       # the allow list corresponds to https://github.com/cncf/foundation/blob/e5db022a0009f4db52b89d9875640cf3137153fe/allowed-third-party-license-policy.md
       run: go-licenses check --include_tests  ./... --allowed_licenses=$(cat ./hack/allowed-licenses.txt)
     - name: Install ltag
-      run: go install github.com/containerd/ltag@v0.3.0
+      # The GitHub repo has been moved from kunalkushwaha/ltag to containerd/ltag,
+      # but the Go module name is not changed yet: https://github.com/containerd/ltag/issues/17
+      run: go install github.com/kunalkushwaha/ltag@v0.2.5
     - name: Check license boilerplates
       run: ltag -t ./hack/ltag --check -v
 
@@ -303,9 +305,11 @@ jobs:
       matrix:
         # Most templates use 9p as the mount type
         template:
+        - ubuntu-25.04.yaml  # The default version of Ubuntu is still 24.10 due to https://github.com/lima-vm/lima/issues/3334
         - alpine.yaml
         - debian.yaml  # reverse-sshfs
-        - fedora.yaml
+        - fedora.yaml  # The default version of Fedora is still 41 due to https://github.com/lima-vm/lima/issues/3334
+        - fedora-42.yaml
         - archlinux.yaml
         - opensuse.yaml
         - docker.yaml
diff --git a/hack/test-templates.sh b/hack/test-templates.sh
index 2935907c..9bba8ba3 100755
--- a/hack/test-templates.sh
+++ b/hack/test-templates.sh
@@ -252,13 +252,6 @@ limactl shell "$NAME" bash -c "echo 'foo \"bar\"'"
 
 if [[ -n ${CHECKS["systemd"]} ]]; then
 	set -x
-	# agetty segfaults on Ubuntu 25.04 (x86_64)
-	# > __strncmp_evex () at ../sysdeps/x86_64/multiarch/strcmp-evex.S:316
-	# Should be fixed in Ubuntu 25.10 with util-linux >= 2.41
-	# https://github.com/lima-vm/lima/pull/3643#issuecomment-3006788732
-	if limactl shell "$NAME" systemctl -q is-failed serial-getty@ttyS0.service; then
-		limactl shell "$NAME" sudo systemctl reset-failed serial-getty@ttyS0.service
-	fi
 	if ! limactl shell "$NAME" systemctl is-system-running --wait; then
 		ERROR '"systemctl is-system-running" failed'
 		diagnose "$NAME"
diff --git a/pkg/nativeimgutil/fuzz_test.go b/pkg/imgutil/nativeimgutil/fuzz_test.go
similarity index 89%
rename from pkg/nativeimgutil/fuzz_test.go
rename to pkg/imgutil/nativeimgutil/fuzz_test.go
index 7a3face5..204d5583 100644
--- a/pkg/nativeimgutil/fuzz_test.go
+++ b/pkg/imgutil/nativeimgutil/fuzz_test.go
@@ -17,6 +17,6 @@ func FuzzConvertToRaw(f *testing.F) {
 		destPath := filepath.Join(t.TempDir(), "dest.img")
 		err := os.WriteFile(srcPath, imgData, 0o600)
 		assert.NilError(t, err)
-		_ = ConvertToRaw(srcPath, destPath, &size, withBacking)
+		_ = convertToRaw(srcPath, destPath, &size, withBacking)
 	})
 }
diff --git a/pkg/nativeimgutil/nativeimgutil_test.go b/pkg/imgutil/nativeimgutil/nativeimgutil_test.go
similarity index 89%
rename from pkg/nativeimgutil/nativeimgutil_test.go
rename to pkg/imgutil/nativeimgutil/nativeimgutil_test.go
index 943a8a8c..466edc62 100644
--- a/pkg/nativeimgutil/nativeimgutil_test.go
+++ b/pkg/imgutil/nativeimgutil/nativeimgutil_test.go
@@ -15,8 +15,8 @@ import (
 
 func TestRoundUp(t *testing.T) {
 	tests := []struct {
-		Size    int
-		Rounded int
+		Size    int64
+		Rounded int64
 	}{
 		{0, 0},
 		{1, 512},
@@ -25,7 +25,7 @@ func TestRoundUp(t *testing.T) {
 		{123456789, 123457024},
 	}
 	for _, tc := range tests {
-		if RoundUp(tc.Size) != tc.Rounded {
+		if roundUp(tc.Size) != tc.Rounded {
 			t.Errorf("expected %d, got %d", tc.Rounded, tc.Size)
 		}
 	}
@@ -63,7 +63,7 @@ func TestConvertToRaw(t *testing.T) {
 	t.Run("qcow without backing file", func(t *testing.T) {
 		resultImage := filepath.Join(tmpDir, strings.ReplaceAll(strings.ReplaceAll(t.Name(), string(os.PathSeparator), "_"), "/", "_"))
 
-		err = ConvertToRaw(qcowImage.Name(), resultImage, nil, false)
+		err = convertToRaw(qcowImage.Name(), resultImage, nil, false)
 		assert.NilError(t, err)
 		assertFileEquals(t, rawImage.Name(), resultImage)
 	})
@@ -71,7 +71,7 @@ func TestConvertToRaw(t *testing.T) {
 	t.Run("qcow with backing file", func(t *testing.T) {
 		resultImage := filepath.Join(tmpDir, strings.ReplaceAll(strings.ReplaceAll(t.Name(), string(os.PathSeparator), "_"), "/", "_"))
 
-		err = ConvertToRaw(qcowImage.Name(), resultImage, nil, true)
+		err = convertToRaw(qcowImage.Name(), resultImage, nil, true)
 		assert.NilError(t, err)
 		assertFileEquals(t, rawImage.Name(), resultImage)
 	})
@@ -80,7 +80,7 @@ func TestConvertToRaw(t *testing.T) {
 		resultImage := filepath.Join(tmpDir, strings.ReplaceAll(strings.ReplaceAll(t.Name(), string(os.PathSeparator), "_"), "/", "_"))
 
 		size := int64(2_097_152) // 2mb
-		err = ConvertToRaw(qcowImage.Name(), resultImage, &size, false)
+		err = convertToRaw(qcowImage.Name(), resultImage, &size, false)
 		assert.NilError(t, err)
 		assertFileEquals(t, rawImageExtended.Name(), resultImage)
 	})
@@ -88,7 +88,7 @@ func TestConvertToRaw(t *testing.T) {
 	t.Run("raw", func(t *testing.T) {
 		resultImage := filepath.Join(tmpDir, strings.ReplaceAll(strings.ReplaceAll(t.Name(), string(os.PathSeparator), "_"), "/", "_"))
 
-		err = ConvertToRaw(rawImage.Name(), resultImage, nil, false)
+		err = convertToRaw(rawImage.Name(), resultImage, nil, false)
 		assert.NilError(t, err)
 		assertFileEquals(t, rawImage.Name(), resultImage)
 	})
diff --git a/pkg/qemu/imgutil/imgutil_test.go b/pkg/imgutil/qemuimgutil/qemuimgutil_test.go
similarity index 97%
rename from pkg/qemu/imgutil/imgutil_test.go
rename to pkg/imgutil/qemuimgutil/qemuimgutil_test.go
index bb630f48..b0f39d43 100644
--- a/pkg/qemu/imgutil/imgutil_test.go
+++ b/pkg/imgutil/qemuimgutil/qemuimgutil_test.go
@@ -1,7 +1,7 @@
 // SPDX-FileCopyrightText: Copyright The Lima Authors
 // SPDX-License-Identifier: Apache-2.0
 
-package imgutil
+package qemuimgutil
 
 import (
 	"testing"
@@ -52,7 +52,7 @@ func TestParseInfo(t *testing.T) {
     "dirty-flag": false
 }`
 
-		info, err := ParseInfo([]byte(s))
+		info, err := parseInfo([]byte(s))
 		assert.NilError(t, err)
 		assert.Equal(t, 1, len(info.Children))
 		assert.Check(t, info.FormatSpecific != nil)
@@ -104,7 +104,7 @@ func TestParseInfo(t *testing.T) {
     "backing-filename": "foo.qcow2",
     "dirty-flag": false
 }`
-			info, err := ParseInfo([]byte(s))
+			info, err := parseInfo([]byte(s))
 			assert.NilError(t, err)
 			assert.Equal(t, 1, len(info.Children))
 			assert.Equal(t, "foo.qcow2", info.BackingFilename)
@@ -202,7 +202,7 @@ func TestParseInfo(t *testing.T) {
     },
     "dirty-flag": false
 }`
-			info, err := ParseInfo([]byte(s))
+			info, err := parseInfo([]byte(s))
 			assert.NilError(t, err)
 			assert.Equal(t, 3, len(info.Children))
 			assert.Equal(t, "foo.vmdk", info.Filename)
diff --git a/pkg/limayaml/validate_test.go b/pkg/limayaml/validate_test.go
index 6d459417..859d8ea9 100644
--- a/pkg/limayaml/validate_test.go
+++ b/pkg/limayaml/validate_test.go
@@ -4,7 +4,6 @@
 package limayaml
 
 import (
-	"errors"
 	"testing"
 
 	"gotest.tools/v3/assert"
@@ -265,55 +264,3 @@ provision:
 		"field `provision[0].mode` must one of \"system\", \"user\", \"boot\", \"data\", \"dependency\", or \"ansible\"\n"+
 		"field `provision[1].path` must not be empty when mode is \"data\"")
 }
-
-func TestValidateAgainstLatestConfig(t *testing.T) {
-	tests := []struct {
-		name    string
-		yNew    string
-		yLatest string
-		wantErr error
-	}{
-		{
-			name:    "Valid disk size unchanged",
-			yNew:    `disk: 100GiB`,
-			yLatest: `disk: 100GiB`,
-		},
-		{
-			name:    "Valid disk size increased",
-			yNew:    `disk: 200GiB`,
-			yLatest: `disk: 100GiB`,
-		},
-		{
-			name:    "No disk field in both YAMLs",
-			yNew:    ``,
-			yLatest: ``,
-		},
-		{
-			name:    "No disk field in new YAMLs",
-			yNew:    ``,
-			yLatest: `disk: 100GiB`,
-		},
-		{
-			name:    "No disk field in latest YAMLs",
-			yNew:    `disk: 100GiB`,
-			yLatest: ``,
-		},
-		{
-			name:    "Disk size shrunk",
-			yNew:    `disk: 50GiB`,
-			yLatest: `disk: 100GiB`,
-			wantErr: errors.New("field `disk`: shrinking the disk (100GiB --> 50GiB) is not supported"),
-		},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			err := ValidateAgainstLatestConfig([]byte(tt.yNew), []byte(tt.yLatest))
-			if tt.wantErr == nil {
-				assert.NilError(t, err)
-			} else {
-				assert.Error(t, err, tt.wantErr.Error())
-			}
-		})
-	}
-}