Repository: lima-vm/lima

Issue:
cidata: identify vmtype-specific boot scripts
https://github.com/lima-vm/lima/tree/master/pkg/cidata/cidata.TEMPLATE.d/boot

VMtype-specific boot scripts ([02-wsl2-setup.sh](https://github.com/lima-vm/lima/blob/master/pkg/cidata/cidata.TEMPLATE.d/boot/02-wsl2-setup.sh), [05-rosetta-volume.sh](https://github.com/lima-vm/lima/blob/master/pkg/cidata/cidata.TEMPLATE.d/boot/05-rosetta-volume.sh), and others?) should be consolidated to the same step.

These scripts should be eventually decoupled from the core of Lima, and should be provided by VM drivers


Discussion:
@afbjorklund: I think the wsl2-setup / lima-init can be decoupled separately, and does not have to be provided by the VM drivers.

Otherwise, it will duplicated in each driver: WSL2, Apple Containers, Kata Containers - everything without `cloud-init`

But it does need a different feature check (than checking vmType):

* https://github.com/lima-vm/lima/pull/3861

```
pkg/cidata/cidata.go:   if args.VMType == limayaml.WSL2 {
```

```
pkg/cidata/cidata.TEMPLATE.d/boot/02-wsl2-setup.sh:[ "$LIMA_CIDATA_VMTYPE" = "wsl2" ] || exit 0
```
@jandubois: Maybe drivers need a mechanism to specify/override `LIMA_CIDATA_*` variables, so they can configure builtin boot scripts (maybe @unsuman has implemented this; I haven't checked the code).@unsuman: No, actually, I have only implemented a function in the driver package(`BootScripts()`) and moved the driver-specific boot scripts under `driver/boot`(only vz for now).
[See this.](https://github.com/lima-vm/lima/pull/3901/files#diff-6a911aa7574099e18369e9477b214a031077350dedc30e60b933ad0f61e022c2R381-R392) Do you think the approach is any good?


> LIMA_CIDATA_* variables

Can you please elaborate and set some context on what it does so that I can try to add it to the recent PR?

Solution patch:
diff --git a/Makefile b/Makefile
index 90af0241..8c99b56b 100644
--- a/Makefile
+++ b/Makefile
@@ -43,16 +43,38 @@ PACKAGE := github.com/lima-vm/lima/v2
 VERSION := $(shell git describe --match 'v[0-9]*' --dirty='.m' --always --tags)
 VERSION_TRIMMED := $(VERSION:v%=%)
 
+# `DEBUG` flag to build binaries with debug information for use by `dlv exec`.
+# This implies KEEP_DWARF=1 and KEEP_SYMBOLS=1.
+DEBUG ?=
+GO_BUILD_GCFLAGS ?=
+KEEP_DWARF ?=
 KEEP_SYMBOLS ?=
+ifeq ($(DEBUG),1)
+	# Disable optimizations and inlining to make debugging easier.
+	GO_BUILD_GCFLAGS = -gcflags="all=-N -l"
+	# Keep the symbol table
+	KEEP_DWARF = 1
+	# Enable DWARF generation
+	KEEP_SYMBOLS = 1
+endif
+
+GO_BUILD_LDFLAGS_W := true
+ifeq ($(KEEP_DWARF),1)
+	GO_BUILD_LDFLAGS_W = false
+endif
+
 GO_BUILD_LDFLAGS_S := true
 ifeq ($(KEEP_SYMBOLS),1)
 	GO_BUILD_LDFLAGS_S = false
 endif
-GO_BUILD_LDFLAGS := -ldflags="-s=$(GO_BUILD_LDFLAGS_S) -w -X $(PACKAGE)/pkg/version.Version=$(VERSION)"
+# `-s`: Strip the symbol table according to the KEEP_SYMBOLS config
+# `-w`: Disable DWARF generation according to the KEEP_DWARF config
+# `-X`: Embed version information.
+GO_BUILD_LDFLAGS := -ldflags="-s=$(GO_BUILD_LDFLAGS_S) -w=$(GO_BUILD_LDFLAGS_W) -X $(PACKAGE)/pkg/version.Version=$(VERSION)"
 # `go -version -m` returns -tags with comma-separated list, because space-separated list is deprecated in go1.13.
 # converting to comma-separated list is useful for comparing with the output of `go version -m`.
 GO_BUILD_FLAG_TAGS := $(addprefix -tags=,$(shell echo "$(GO_BUILDTAGS)"|tr " " "\n"|paste -sd "," -))
-GO_BUILD := $(GO) build $(GO_BUILD_LDFLAGS) $(GO_BUILD_FLAG_TAGS)
+GO_BUILD := $(strip $(GO) build $(GO_BUILD_GCFLAGS) $(GO_BUILD_LDFLAGS) $(GO_BUILD_FLAG_TAGS))
 
 ################################################################################
 # Features
@@ -78,7 +100,9 @@ help-variables:
 	@echo  '# Variables that can be overridden.'
 	@echo
 	@echo  '- PREFIX       (directory)  : Installation prefix (default: /usr/local)'
+	@echo  '- KEEP_DWARF   (1 or 0)     : Whether to keep DWARF information (default: 0)'
 	@echo  '- KEEP_SYMBOLS (1 or 0)     : Whether to keep symbols (default: 0)'
+	@echo  '- DEBUG        (1 or 0)     : Whether to build with debug information (default: 0)'
 
 .PHONY: help-targets
 help-targets:
@@ -177,7 +201,7 @@ dependencies_for_cmd = go.mod $(call find_files_excluding_dir_and_test, ./cmd/$(
 # $(1): target binary
 extract_build_vars = $(shell \
 	($(GO) version -m $(1) 2>&- || echo $(1):) | \
-	awk 'FNR==1{print "GOVERSION="$$2}$$2~/^(CGO|GO|-ldflags|-tags).*=.+$$/{sub("^.*"$$2,$$2); print $$0}' \
+	awk 'FNR==1{print "GOVERSION="$$2}$$2~/^(CGO|GO|-gcflags|-ldflags|-tags).*=.+$$/{sub("^.*"$$2,$$2); print $$0}' \
 )
 
 # a list of keys from the GO build variables to be used for calling `go env`.
@@ -192,7 +216,7 @@ go_build_vars = $(shell \
 	$(ENVS_$(1)) $(GO) env $(2) | \
 	awk '/ /{print "\""$$0"\""; next}{print}' | \
 	for k in $(2); do read -r v && echo "$$k=$${v}"; done \
-) $(GO_BUILD_LDFLAGS) $(GO_BUILD_FLAG_TAGS)
+) $(GO_BUILD_GCFLAGS) $(GO_BUILD_LDFLAGS) $(GO_BUILD_FLAG_TAGS)
 
 # returns the difference between $(1) and $(2).
 diff = $(filter-out $(2),$(1))$(filter-out $(1),$(2))
@@ -356,16 +380,20 @@ MKDIR_TARGETS += _output/share/lima
 ################################################################################
 # _output/share/lima/templates
 TEMPLATES = $(addprefix _output/share/lima/templates/,$(filter-out experimental,$(notdir $(wildcard templates/*))))
+TEMPLATE_DEFAULTS = ${addprefix _output/share/lima/templates/_default/,$(notdir $(wildcard templates/_default/*))}
+TEMPLATE_IMAGES = $(addprefix _output/share/lima/templates/_images/,$(notdir $(wildcard templates/_images/*)))
 TEMPLATE_EXPERIMENTALS = $(addprefix _output/share/lima/templates/experimental/,$(notdir $(wildcard templates/experimental/*)))
 
 .PHONY: default_template templates template_experimentals
 default_template: _output/share/lima/templates/default.yaml
-templates: $(TEMPLATES)
+templates: $(TEMPLATES) $(TEMPLATE_DEFAULTS) $(TEMPLATE_IMAGES)
 template_experimentals: $(TEMPLATE_EXPERIMENTALS)
 
 $(TEMPLATES): | _output/share/lima/templates
+$(TEMPLATE_DEFAULTS): | _output/share/lima/templates/_default
+$(TEMPLATE_IMAGES): | _output/share/lima/templates/_images
 $(TEMPLATE_EXPERIMENTALS): | _output/share/lima/templates/experimental
-MKDIR_TARGETS += _output/share/lima/templates _output/share/lima/templates/experimental
+MKDIR_TARGETS += _output/share/lima/templates _output/share/lima/templates/_default _output/share/lima/templates/_images _output/share/lima/templates/experimental
 
 _output/share/lima/templates/%: templates/%
 	cp -aL $< $@
@@ -495,7 +523,11 @@ uninstall:
 		"$(DEST)/bin/kubectl.lima" \
 		"$(DEST)/share/man/man1/lima.1" \
 		"$(DEST)/share/man/man1/limactl"*".1" \
-		"$(DEST)/share/lima" "$(DEST)/share/doc/lima"
+		"$(DEST)/share/lima" \
+		"$(DEST)/share/doc/lima" \
+		"$(DEST)/libexec/lima/lima-driver-qemu$(exe)" \
+		"$(DEST)/libexec/lima/lima-driver-vz$(exe)" \
+		"$(DEST)/libexec/lima/lima-driver-wsl2$(exe)"
 	if [ "$$(readlink "$(DEST)/bin/nerdctl")" = "nerdctl.lima" ]; then rm "$(DEST)/bin/nerdctl"; fi
 	if [ "$$(readlink "$(DEST)/bin/apptainer")" = "apptainer.lima" ]; then rm "$(DEST)/bin/apptainer"; fi
 
diff --git a/cmd/limactl/copy.go b/cmd/limactl/copy.go
index 9d7a0055..8f3ac550 100644
--- a/cmd/limactl/copy.go
+++ b/cmd/limactl/copy.go
@@ -4,6 +4,7 @@
 package main
 
 import (
+	"context"
 	"errors"
 	"fmt"
 	"os"
@@ -26,11 +27,45 @@ const copyHelp = `Copy files between host and guest
 
 Prefix guest filenames with the instance name and a colon.
 
-Example: limactl copy default:/etc/os-release .
+Backends:
+  auto   - Automatically selects the best available backend (rsync preferred, falls back to scp)
+  rsync  - Uses rsync for faster transfers with resume capability (requires rsync on both host and guest)
+  scp    - Uses scp for reliable transfers (always available)
+
+Examples:
+  # Copy file from guest to host (auto backend)
+  limactl copy default:/etc/os-release .
+
+  # Copy file from host to guest with verbose output
+  limactl copy -v myfile.txt default:/tmp/
+
+  # Copy directory recursively using rsync backend
+  limactl copy --backend=rsync -r ./mydir default:/tmp/
+
+  # Copy using scp backend specifically
+  limactl copy --backend=scp default:/var/log/app.log ./logs/
+
+  # Copy multiple files
+  limactl copy file1.txt file2.txt default:/tmp/
 
 Not to be confused with 'limactl clone'.
 `
 
+type copyTool string
+
+const (
+	rsync copyTool = "rsync"
+	scp   copyTool = "scp"
+	auto  copyTool = "auto"
+)
+
+type copyPath struct {
+	instanceName string
+	path         string
+	isRemote     bool
+	instance     *limatype.Instance
+}
+
 func newCopyCommand() *cobra.Command {
 	copyCommand := &cobra.Command{
 		Use:     "copy SOURCE ... TARGET",
@@ -44,6 +79,7 @@ func newCopyCommand() *cobra.Command {
 
 	copyCommand.Flags().BoolP("recursive", "r", false, "Copy directories recursively")
 	copyCommand.Flags().BoolP("verbose", "v", false, "Enable verbose output")
+	copyCommand.Flags().String("backend", "auto", "Copy backend (scp|rsync|auto)")
 
 	return copyCommand
 }
@@ -60,13 +96,6 @@ func copyAction(cmd *cobra.Command, args []string) error {
 		return err
 	}
 
-	arg0, err := exec.LookPath("scp")
-	if err != nil {
-		return err
-	}
-	instances := make(map[string]*limatype.Instance)
-	scpFlags := []string{}
-	scpArgs := []string{}
 	debug, err := cmd.Flags().GetBool("debug")
 	if err != nil {
 		return err
@@ -76,62 +105,215 @@ func copyAction(cmd *cobra.Command, args []string) error {
 		verbose = true
 	}
 
-	if verbose {
-		scpFlags = append(scpFlags, "-v")
-	} else {
-		scpFlags = append(scpFlags, "-q")
+	copyPaths, err := parseCopyArgs(ctx, args)
+	if err != nil {
+		return err
 	}
 
-	if recursive {
-		scpFlags = append(scpFlags, "-r")
+	backend, err := cmd.Flags().GetString("backend")
+	if err != nil {
+		return err
+	}
+
+	cpTool, toolPath, err := selectCopyTool(ctx, copyPaths, backend)
+	if err != nil {
+		return err
+	}
+
+	logrus.Debugf("using copy tool %q", toolPath)
+
+	var copyCmd *exec.Cmd
+	switch cpTool {
+	case scp:
+		copyCmd, err = scpCommand(ctx, toolPath, copyPaths, verbose, recursive)
+	case rsync:
+		copyCmd, err = rsyncCommand(ctx, toolPath, copyPaths, verbose, recursive)
+	default:
+		err = fmt.Errorf("invalid copy tool %q", cpTool)
 	}
-	// this assumes that ssh and scp come from the same place, but scp has no -V
-	sshExe, err := sshutil.NewSSHExe()
 	if err != nil {
 		return err
 	}
-	legacySSH := sshutil.DetectOpenSSHVersion(ctx, sshExe).LessThan(*semver.New("8.0.0"))
+
+	copyCmd.Stdin = cmd.InOrStdin()
+	copyCmd.Stdout = cmd.OutOrStdout()
+	copyCmd.Stderr = cmd.ErrOrStderr()
+	logrus.Debugf("executing %v (may take a long time)", copyCmd)
+
+	// TODO: use syscall.Exec directly (results in losing tty?)
+	return copyCmd.Run()
+}
+
+func parseCopyArgs(ctx context.Context, args []string) ([]*copyPath, error) {
+	var copyPaths []*copyPath
+
 	for _, arg := range args {
+		cp := &copyPath{}
+
 		if runtime.GOOS == "windows" {
 			if filepath.IsAbs(arg) {
+				var err error
 				arg, err = ioutilx.WindowsSubsystemPath(ctx, arg)
 				if err != nil {
-					return err
+					return nil, err
 				}
 			} else {
 				arg = filepath.ToSlash(arg)
 			}
 		}
-		path := strings.Split(arg, ":")
-		switch len(path) {
+
+		parts := strings.SplitN(arg, ":", 2)
+		switch len(parts) {
 		case 1:
-			scpArgs = append(scpArgs, arg)
+			cp.path = arg
+			cp.isRemote = false
 		case 2:
-			instName := path[0]
-			inst, err := store.Inspect(ctx, instName)
+			cp.instanceName = parts[0]
+			cp.path = parts[1]
+			cp.isRemote = true
+
+			inst, err := store.Inspect(ctx, cp.instanceName)
 			if err != nil {
 				if errors.Is(err, os.ErrNotExist) {
-					return fmt.Errorf("instance %q does not exist, run `limactl create %s` to create a new instance", instName, instName)
+					return nil, fmt.Errorf("instance %q does not exist, run `limactl create %s` to create a new instance", cp.instanceName, cp.instanceName)
 				}
-				return err
+				return nil, err
 			}
 			if inst.Status == limatype.StatusStopped {
-				return fmt.Errorf("instance %q is stopped, run `limactl start %s` to start the instance", instName, instName)
+				return nil, fmt.Errorf("instance %q is stopped, run `limactl start %s` to start the instance", cp.instanceName, cp.instanceName)
 			}
+			cp.instance = inst
+		default:
+			return nil, fmt.Errorf("path %q contains multiple colons", arg)
+		}
+
+		copyPaths = append(copyPaths, cp)
+	}
+
+	return copyPaths, nil
+}
+
+func selectCopyTool(ctx context.Context, copyPaths []*copyPath, backend string) (copyTool, string, error) {
+	switch copyTool(backend) {
+	case scp:
+		scpPath, err := exec.LookPath("scp")
+		if err != nil {
+			return "", "", fmt.Errorf("scp not found on host: %w", err)
+		}
+		return scp, scpPath, nil
+	case rsync:
+		rsyncPath, err := exec.LookPath("rsync")
+		if err != nil {
+			return "", "", fmt.Errorf("rsync not found on host: %w", err)
+		}
+		if !rsyncAvailableOnGuests(ctx, copyPaths) {
+			return "", "", errors.New("rsync not available on guest(s)")
+		}
+		return rsync, rsyncPath, nil
+	case auto:
+		if rsyncPath, err := exec.LookPath("rsync"); err == nil {
+			if rsyncAvailableOnGuests(ctx, copyPaths) {
+				return rsync, rsyncPath, nil
+			}
+			logrus.Debugf("rsync not available on guest(s), falling back to scp")
+		} else {
+			logrus.Debugf("rsync not found on host, falling back to scp: %v", err)
+		}
+
+		scpPath, err := exec.LookPath("scp")
+		if err != nil {
+			return "", "", fmt.Errorf("neither rsync nor scp found on host: %w", err)
+		}
+		return scp, scpPath, nil
+	default:
+		return "", "", fmt.Errorf("invalid backend %q, must be one of: scp, rsync, auto", backend)
+	}
+}
+
+func rsyncAvailableOnGuests(ctx context.Context, copyPaths []*copyPath) bool {
+	instances := make(map[string]*limatype.Instance)
+
+	for _, cp := range copyPaths {
+		if cp.isRemote {
+			instances[cp.instanceName] = cp.instance
+		}
+	}
+
+	for instName, inst := range instances {
+		if !checkRsyncOnGuest(ctx, inst) {
+			logrus.Debugf("rsync not available on instance %q", instName)
+			return false
+		}
+	}
+
+	return true
+}
+
+func checkRsyncOnGuest(ctx context.Context, inst *limatype.Instance) bool {
+	sshExe, err := sshutil.NewSSHExe()
+	if err != nil {
+		logrus.Debugf("failed to create SSH executable: %v", err)
+		return false
+	}
+	sshOpts, err := sshutil.SSHOpts(ctx, sshExe, inst.Dir, *inst.Config.User.Name, false, false, false, false)
+	if err != nil {
+		logrus.Debugf("failed to get SSH options for rsync check: %v", err)
+		return false
+	}
+
+	sshArgs := sshutil.SSHArgsFromOpts(sshOpts)
+	checkCmd := exec.CommandContext(ctx, "ssh")
+	checkCmd.Args = append(checkCmd.Args, sshArgs...)
+	checkCmd.Args = append(checkCmd.Args,
+		"-p", fmt.Sprintf("%d", inst.SSHLocalPort),
+		fmt.Sprintf("%s@127.0.0.1", *inst.Config.User.Name),
+		"command -v rsync >/dev/null 2>&1",
+	)
+
+	err = checkCmd.Run()
+	return err == nil
+}
+
+func scpCommand(ctx context.Context, command string, copyPaths []*copyPath, verbose, recursive bool) (*exec.Cmd, error) {
+	instances := make(map[string]*limatype.Instance)
+	scpFlags := []string{}
+	scpArgs := []string{}
+
+	if verbose {
+		scpFlags = append(scpFlags, "-v")
+	} else {
+		scpFlags = append(scpFlags, "-q")
+	}
+
+	if recursive {
+		scpFlags = append(scpFlags, "-r")
+	}
+
+	// this assumes that ssh and scp come from the same place, but scp has no -V
+	sshExeForVersion, err := sshutil.NewSSHExe()
+	if err != nil {
+		return nil, err
+	}
+	legacySSH := sshutil.DetectOpenSSHVersion(ctx, sshExeForVersion).LessThan(*semver.New("8.0.0"))
+
+	for _, cp := range copyPaths {
+		if cp.isRemote {
 			if legacySSH {
-				scpFlags = append(scpFlags, "-P", fmt.Sprintf("%d", inst.SSHLocalPort))
-				scpArgs = append(scpArgs, fmt.Sprintf("%s@127.0.0.1:%s", *inst.Config.User.Name, path[1]))
+				scpFlags = append(scpFlags, "-P", fmt.Sprintf("%d", cp.instance.SSHLocalPort))
+				scpArgs = append(scpArgs, fmt.Sprintf("%s@127.0.0.1:%s", *cp.instance.Config.User.Name, cp.path))
 			} else {
-				scpArgs = append(scpArgs, fmt.Sprintf("scp://%s@127.0.0.1:%d/%s", *inst.Config.User.Name, inst.SSHLocalPort, path[1]))
+				scpArgs = append(scpArgs, fmt.Sprintf("scp://%s@127.0.0.1:%d/%s", *cp.instance.Config.User.Name, cp.instance.SSHLocalPort, cp.path))
 			}
-			instances[instName] = inst
-		default:
-			return fmt.Errorf("path %q contains multiple colons", arg)
+			instances[cp.instanceName] = cp.instance
+		} else {
+			scpArgs = append(scpArgs, cp.path)
 		}
 	}
+
 	if legacySSH && len(instances) > 1 {
-		return errors.New("more than one (instance) host is involved in this command, this is only supported for openSSH v8.0 or higher")
+		return nil, errors.New("more than one (instance) host is involved in this command, this is only supported for openSSH v8.0 or higher")
 	}
+
 	scpFlags = append(scpFlags, "-3", "--")
 	scpArgs = append(scpFlags, scpArgs...)
 
@@ -143,32 +325,78 @@ func copyAction(cmd *cobra.Command, args []string) error {
 		for _, inst := range instances {
 			sshExe, err := sshutil.NewSSHExe()
 			if err != nil {
-				return err
+				return nil, err
 			}
 			sshOpts, err = sshutil.SSHOpts(ctx, sshExe, inst.Dir, *inst.Config.User.Name, false, false, false, false)
 			if err != nil {
-				return err
+				return nil, err
 			}
 		}
 	} else {
 		// Copying among multiple hosts; we can't pass in host-specific options.
 		sshExe, err := sshutil.NewSSHExe()
 		if err != nil {
-			return err
+			return nil, err
 		}
 		sshOpts, err = sshutil.CommonOpts(ctx, sshExe, false)
 		if err != nil {
-			return err
+			return nil, err
 		}
 	}
 	sshArgs := sshutil.SSHArgsFromOpts(sshOpts)
 
-	sshCmd := exec.CommandContext(ctx, arg0, append(sshArgs, scpArgs...)...)
-	sshCmd.Stdin = cmd.InOrStdin()
-	sshCmd.Stdout = cmd.OutOrStdout()
-	sshCmd.Stderr = cmd.ErrOrStderr()
-	logrus.Debugf("executing scp (may take a long time): %+v", sshCmd.Args)
+	return exec.CommandContext(ctx, command, append(sshArgs, scpArgs...)...), nil
+}
 
-	// TODO: use syscall.Exec directly (results in losing tty?)
-	return sshCmd.Run()
+func rsyncCommand(ctx context.Context, command string, copyPaths []*copyPath, verbose, recursive bool) (*exec.Cmd, error) {
+	rsyncFlags := []string{"-a"}
+
+	if verbose {
+		rsyncFlags = append(rsyncFlags, "-v", "--progress")
+	} else {
+		rsyncFlags = append(rsyncFlags, "-q")
+	}
+
+	if recursive {
+		rsyncFlags = append(rsyncFlags, "-r")
+	}
+
+	rsyncArgs := make([]string, 0, len(rsyncFlags)+len(copyPaths))
+	rsyncArgs = append(rsyncArgs, rsyncFlags...)
+
+	var sshCmd string
+	var remoteInstance *limatype.Instance
+
+	for _, cp := range copyPaths {
+		if cp.isRemote {
+			if remoteInstance == nil {
+				remoteInstance = cp.instance
+				sshExe, err := sshutil.NewSSHExe()
+				if err != nil {
+					return nil, err
+				}
+				sshOpts, err := sshutil.SSHOpts(ctx, sshExe, cp.instance.Dir, *cp.instance.Config.User.Name, false, false, false, false)
+				if err != nil {
+					return nil, err
+				}
+
+				sshArgs := sshutil.SSHArgsFromOpts(sshOpts)
+				sshCmd = fmt.Sprintf("ssh -p %d %s", cp.instance.SSHLocalPort, strings.Join(sshArgs, " "))
+			}
+		}
+	}
+
+	if sshCmd != "" {
+		rsyncArgs = append(rsyncArgs, "-e", sshCmd)
+	}
+
+	for _, cp := range copyPaths {
+		if cp.isRemote {
+			rsyncArgs = append(rsyncArgs, fmt.Sprintf("%s@127.0.0.1:%s", *cp.instance.Config.User.Name, cp.path))
+		} else {
+			rsyncArgs = append(rsyncArgs, cp.path)
+		}
+	}
+
+	return exec.CommandContext(ctx, command, rsyncArgs...), nil
 }
diff --git a/cmd/limactl/editflags/editflags.go b/cmd/limactl/editflags/editflags.go
index 6b8d21d6..7047a79c 100644
--- a/cmd/limactl/editflags/editflags.go
+++ b/cmd/limactl/editflags/editflags.go
@@ -8,6 +8,7 @@ import (
 	"fmt"
 	"math/bits"
 	"runtime"
+	"slices"
 	"strconv"
 	"strings"
 
@@ -179,7 +180,7 @@ func buildMountListExpression(ss []string) (string, error) {
 		if err != nil {
 			return "", err
 		}
-		expr += fmt.Sprintf(`{"location": %q, "writable": %v}`, loc, writable)
+		expr += fmt.Sprintf(`{"location": %q, "mountPoint": %q, "writable": %v}`, loc, loc, writable)
 		if i < len(ss)-1 {
 			expr += ","
 		}
@@ -225,6 +226,7 @@ func YQExpressions(flags *flag.FlagSet, newInstance bool) ([]string, error) {
 			"mount",
 			func(_ *flag.Flag) (string, error) {
 				ss, err := flags.GetStringSlice("mount")
+				slices.Reverse(ss)
 				if err != nil {
 					return "", err
 				}
@@ -232,7 +234,8 @@ func YQExpressions(flags *flag.FlagSet, newInstance bool) ([]string, error) {
 				if err != nil {
 					return "", err
 				}
-				expr := `.mounts += ` + mountListExpr + ` | .mounts |= unique_by(.location)`
+				// mount options take precedence over template settings
+				expr := fmt.Sprintf(".mounts = %s + .mounts", mountListExpr)
 				mountOnly, err := flags.GetStringSlice("mount-only")
 				if err != nil {
 					return "", err
diff --git a/cmd/limactl/hostagent.go b/cmd/limactl/hostagent.go
index d0a363f7..0ad654ed 100644
--- a/cmd/limactl/hostagent.go
+++ b/cmd/limactl/hostagent.go
@@ -20,6 +20,7 @@ import (
 
 	"github.com/lima-vm/lima/v2/pkg/hostagent"
 	"github.com/lima-vm/lima/v2/pkg/hostagent/api/server"
+	"github.com/lima-vm/lima/v2/pkg/store"
 )
 
 func newHostagentCommand() *cobra.Command {
@@ -46,8 +47,10 @@ func hostagentAction(cmd *cobra.Command, args []string) error {
 		return err
 	}
 	if pidfile != "" {
-		if _, err := os.Stat(pidfile); !errors.Is(err, os.ErrNotExist) {
-			return fmt.Errorf("pidfile %q already exists", pidfile)
+		if existingPID, err := store.ReadPIDFile(pidfile); existingPID != 0 {
+			return fmt.Errorf("another hostagent may already be running with pid %d (pidfile %q)", existingPID, pidfile)
+		} else if err != nil {
+			return fmt.Errorf("failed to determine if another hostagent is running: %w", err)
 		}
 		if err := os.WriteFile(pidfile, []byte(strconv.Itoa(os.Getpid())+"\n"), 0o644); err != nil {
 			return err
diff --git a/cmd/limactl/usernet.go b/cmd/limactl/usernet.go
index 37777de6..9c11ce39 100644
--- a/cmd/limactl/usernet.go
+++ b/cmd/limactl/usernet.go
@@ -7,7 +7,9 @@ import (
 	"errors"
 	"fmt"
 	"os"
+	"os/signal"
 	"strconv"
+	"syscall"
 
 	"github.com/spf13/cobra"
 
@@ -77,10 +79,13 @@ func usernetAction(cmd *cobra.Command, _ []string) error {
 	os.RemoveAll(qemuSocket)
 	os.RemoveAll(fdSocket)
 
+	ctx, cancel := signal.NotifyContext(cmd.Context(), os.Interrupt, syscall.SIGTERM)
+	defer cancel()
+
 	// Environment Variables
 	// LIMA_USERNET_RESOLVE_IP_ADDRESS_TIMEOUT: Specifies the timeout duration for resolving IP addresses in minutes. Default is 2 minutes.
 
-	return usernet.StartGVisorNetstack(cmd.Context(), &usernet.GVisorNetstackOpts{
+	return usernet.StartGVisorNetstack(ctx, &usernet.GVisorNetstackOpts{
 		MTU:           mtu,
 		Endpoint:      endpoint,
 		QemuSocket:    qemuSocket,
diff --git a/hack/test-templates.sh b/hack/test-templates.sh
index 7faa5f2a..3322a780 100755
--- a/hack/test-templates.sh
+++ b/hack/test-templates.sh
@@ -260,6 +260,57 @@ if [ "$got" != "$expected" ]; then
 	exit 1
 fi
 
+INFO "Testing limactl copy command with scp backend"
+tmpfile_scp="$tmpdir/lima-hostname-scp"
+rm -f "$tmpfile_scp"
+tmpfile_scp_host=$tmpfile_scp
+if [ "${OS_HOST}" = "Msys" ]; then
+	tmpfile_scp_host="$(cygpath -w "$tmpfile_scp")"
+fi
+limactl cp --backend=scp "$NAME":/etc/hostname "$tmpfile_scp_host"
+expected="$(limactl shell "$NAME" cat /etc/hostname)"
+got="$(cat "$tmpfile_scp")"
+INFO "/etc/hostname (scp): expected=${expected}, got=${got}"
+if [ "$got" != "$expected" ]; then
+	ERROR "copy command with scp backend did not fetch the file"
+	exit 1
+fi
+
+if command -v rsync >/dev/null && limactl shell "$NAME" command -v rsync >/dev/null 2>&1; then
+	INFO "Testing limactl copy command with rsync backend"
+	tmpfile_rsync="$tmpdir/lima-hostname-rsync"
+	rm -f "$tmpfile_rsync"
+	tmpfile_rsync_host=$tmpfile_rsync
+	if [ "${OS_HOST}" = "Msys" ]; then
+		tmpfile_rsync_host="$(cygpath -w "$tmpfile_rsync")"
+	fi
+	limactl cp --backend=rsync "$NAME":/etc/hostname "$tmpfile_rsync_host"
+	expected="$(limactl shell "$NAME" cat /etc/hostname)"
+	got="$(cat "$tmpfile_rsync")"
+	INFO "/etc/hostname (rsync): expected=${expected}, got=${got}"
+	if [ "$got" != "$expected" ]; then
+		ERROR "copy command with rsync backend did not fetch the file"
+		exit 1
+	fi
+
+	INFO "Testing limactl copy command with rsync backend (verbose, recursive)"
+	testdir="$tmpdir/test-rsync-dir"
+	mkdir -p "$testdir"
+	echo "test content" >"$testdir/testfile.txt"
+	limactl cp --backend=rsync -r -v "$testdir" "$NAME":/tmp/
+	if ! limactl shell "$NAME" test -f /tmp/test-rsync-dir/testfile.txt; then
+		ERROR "rsync recursive copy failed"
+		exit 1
+	fi
+	rsync_content="$(limactl shell "$NAME" cat /tmp/test-rsync-dir/testfile.txt)"
+	if [ "$rsync_content" != "test content" ]; then
+		ERROR "rsync file content mismatch"
+		exit 1
+	fi
+else
+	INFO "Skipping rsync backend test (rsync not available on host or guest)"
+fi
+
 INFO "Testing limactl command with escaped characters"
 limactl shell "$NAME" bash -c "$(echo -e '\n\techo foo\n\techo bar')"
 
diff --git a/pkg/cidata/cidata.TEMPLATE.d/boot/30-install-packages.sh b/pkg/cidata/cidata.TEMPLATE.d/boot/30-install-packages.sh
index e32a6992..a274b0a0 100644
--- a/pkg/cidata/cidata.TEMPLATE.d/boot/30-install-packages.sh
+++ b/pkg/cidata/cidata.TEMPLATE.d/boot/30-install-packages.sh
@@ -53,6 +53,9 @@ if head -c 4 "$(command -v apt-get)" | grep -qP '\x7fELF' >/dev/null 2>&1; then
 	if [ "${LIMA_CIDATA_CONTAINERD_USER}" = 1 ] && ! command -v newuidmap >/dev/null 2>&1; then
 		pkgs="${pkgs} uidmap fuse3 dbus-user-session"
 	fi
+	if ! command -v rsync >/dev/null 2>&1; then
+		pkgs="${pkgs} rsync"
+	fi
 	if [ -n "${pkgs}" ]; then
 		DEBIAN_FRONTEND=noninteractive
 		export DEBIAN_FRONTEND
@@ -83,6 +86,9 @@ elif command -v dnf >/dev/null 2>&1; then
 			pkgs="${pkgs} fuse3"
 		fi
 	fi
+	if ! command -v rsync >/dev/null 2>&1; then
+		pkgs="${pkgs} rsync"
+	fi
 	if [ -n "${pkgs}" ] || [ -n "${extrapkgs}" ]; then
 		dnf_install_flags="-y --setopt=install_weak_deps=False"
 		epel_install_flags=""
@@ -148,6 +154,9 @@ elif command -v yum >/dev/null 2>&1; then
 			pkgs="${pkgs} fuse3"
 		fi
 	fi
+	if ! command -v rsync >/dev/null 2>&1; then
+		pkgs="${pkgs} rsync"
+	fi
 	if [ -n "${pkgs}" ]; then
 		# shellcheck disable=SC2086
 		yum install ${yum_install_flags} ${pkgs}
@@ -160,6 +169,9 @@ elif command -v pacman >/dev/null 2>&1; then
 			pkgs="${pkgs} sshfs"
 		fi
 	fi
+	if ! command -v rsync >/dev/null 2>&1; then
+		pkgs="${pkgs} rsync"
+	fi
 	# other dependencies are preinstalled on Arch Linux
 	if [ -n "${pkgs}" ]; then
 		# shellcheck disable=SC2086
@@ -178,6 +190,9 @@ elif command -v zypper >/dev/null 2>&1; then
 	if [ "${LIMA_CIDATA_CONTAINERD_USER}" = 1 ] && ! command -v mount.fuse3 >/dev/null 2>&1; then
 		pkgs="${pkgs} fuse3"
 	fi
+	if ! command -v rsync >/dev/null 2>&1; then
+		pkgs="${pkgs} rsync"
+	fi
 	if [ -n "${pkgs}" ]; then
 		# shellcheck disable=SC2086
 		zypper --non-interactive install -y --no-recommends ${pkgs}
@@ -192,6 +207,9 @@ elif command -v apk >/dev/null 2>&1; then
 	if [ "${INSTALL_IPTABLES}" = 1 ] && ! command -v iptables >/dev/null 2>&1; then
 		pkgs="${pkgs} iptables"
 	fi
+	if ! command -v rsync >/dev/null 2>&1; then
+		pkgs="${pkgs} rsync"
+	fi
 	if [ -n "${pkgs}" ]; then
 		apk update
 		# shellcheck disable=SC2086
diff --git a/pkg/cidata/cidata.TEMPLATE.d/lima.env b/pkg/cidata/cidata.TEMPLATE.d/lima.env
index e08129b5..8cd9453c 100644
--- a/pkg/cidata/cidata.TEMPLATE.d/lima.env
+++ b/pkg/cidata/cidata.TEMPLATE.d/lima.env
@@ -65,3 +65,8 @@ LIMA_CIDATA_PLAIN=1
 {{- else}}
 LIMA_CIDATA_PLAIN=
 {{- end}}
+{{- if .NoCloudInit}}
+LIMA_CIDATA_NO_CLOUD_INIT=1
+{{- else}}
+LIMA_CIDATA_NO_CLOUD_INIT=
+{{- end}}
diff --git a/pkg/cidata/cidata.go b/pkg/cidata/cidata.go
index 4a38c0c2..ad024acb 100644
--- a/pkg/cidata/cidata.go
+++ b/pkg/cidata/cidata.go
@@ -118,7 +118,7 @@ func setupEnv(instConfigEnv map[string]string, propagateProxyEnv bool, slirpGate
 	return env, nil
 }
 
-func templateArgs(ctx context.Context, bootScripts bool, instDir, name string, instConfig *limatype.LimaYAML, udpDNSLocalPort, tcpDNSLocalPort, vsockPort int, virtioPort string) (*TemplateArgs, error) {
+func templateArgs(ctx context.Context, bootScripts bool, instDir, name string, instConfig *limatype.LimaYAML, udpDNSLocalPort, tcpDNSLocalPort, vsockPort int, virtioPort string, noCloudInit bool) (*TemplateArgs, error) {
 	if err := limayaml.Validate(instConfig, false); err != nil {
 		return nil, err
 	}
@@ -138,12 +138,13 @@ func templateArgs(ctx context.Context, bootScripts bool, instDir, name string, i
 		Containerd:         Containerd{System: *instConfig.Containerd.System, User: *instConfig.Containerd.User, Archive: archive},
 		SlirpNICName:       networks.SlirpNICName,
 
-		VMType:     *instConfig.VMType,
-		VSockPort:  vsockPort,
-		VirtioPort: virtioPort,
-		Plain:      *instConfig.Plain,
-		TimeZone:   *instConfig.TimeZone,
-		Param:      instConfig.Param,
+		VMType:      *instConfig.VMType,
+		VSockPort:   vsockPort,
+		VirtioPort:  virtioPort,
+		Plain:       *instConfig.Plain,
+		TimeZone:    *instConfig.TimeZone,
+		NoCloudInit: noCloudInit,
+		Param:       instConfig.Param,
 	}
 
 	if instConfig.VMOpts.VZ.Rosetta.Enabled != nil {
@@ -350,7 +351,7 @@ func templateArgs(ctx context.Context, bootScripts bool, instDir, name string, i
 }
 
 func GenerateCloudConfig(ctx context.Context, instDir, name string, instConfig *limatype.LimaYAML) error {
-	args, err := templateArgs(ctx, false, instDir, name, instConfig, 0, 0, 0, "")
+	args, err := templateArgs(ctx, false, instDir, name, instConfig, 0, 0, 0, "", false)
 	if err != nil {
 		return err
 	}
@@ -372,8 +373,8 @@ func GenerateCloudConfig(ctx context.Context, instDir, name string, instConfig *
 	return os.WriteFile(filepath.Join(instDir, filenames.CloudConfig), config, 0o444)
 }
 
-func GenerateISO9660(ctx context.Context, drv driver.Driver, instDir, name string, instConfig *limatype.LimaYAML, udpDNSLocalPort, tcpDNSLocalPort int, guestAgentBinary, nerdctlArchive string, vsockPort int, virtioPort string) error {
-	args, err := templateArgs(ctx, true, instDir, name, instConfig, udpDNSLocalPort, tcpDNSLocalPort, vsockPort, virtioPort)
+func GenerateISO9660(ctx context.Context, drv driver.Driver, instDir, name string, instConfig *limatype.LimaYAML, udpDNSLocalPort, tcpDNSLocalPort int, guestAgentBinary, nerdctlArchive string, vsockPort int, virtioPort string, noCloudInit bool) error {
+	args, err := templateArgs(ctx, true, instDir, name, instConfig, udpDNSLocalPort, tcpDNSLocalPort, vsockPort, virtioPort, noCloudInit)
 	if err != nil {
 		return err
 	}
@@ -466,7 +467,7 @@ func GenerateISO9660(ctx context.Context, drv driver.Driver, instDir, name strin
 		})
 	}
 
-	if args.VMType == limatype.WSL2 {
+	if noCloudInit {
 		layout = append(layout, iso9660util.Entry{
 			Path:   "ssh_authorized_keys",
 			Reader: strings.NewReader(strings.Join(args.SSHPubKeys, "\n")),
diff --git a/pkg/cidata/template.go b/pkg/cidata/template.go
index 3d886e7a..84dfafce 100644
--- a/pkg/cidata/template.go
+++ b/pkg/cidata/template.go
@@ -114,6 +114,7 @@ type TemplateArgs struct {
 	VirtioPort                      string
 	Plain                           bool
 	TimeZone                        string
+	NoCloudInit                     bool
 }
 
 func ValidateTemplateArgs(args *TemplateArgs) error {
diff --git a/pkg/driver/driver.go b/pkg/driver/driver.go
index 94c192f0..fc3a1afd 100644
--- a/pkg/driver/driver.go
+++ b/pkg/driver/driver.go
@@ -106,4 +106,5 @@ type DriverFeatures struct {
 	CanRunGUI            bool `json:"canRunGui,omitempty"`
 	DynamicSSHAddress    bool `json:"dynamicSSHAddress"`
 	SkipSocketForwarding bool `json:"skipSocketForwarding"`
+	NoCloudInit          bool `json:"noCloudInit"`
 }
diff --git a/pkg/driver/qemu/qemu.go b/pkg/driver/qemu/qemu.go
index 858dad02..1af03ae0 100644
--- a/pkg/driver/qemu/qemu.go
+++ b/pkg/driver/qemu/qemu.go
@@ -935,7 +935,7 @@ func Cmdline(ctx context.Context, cfg Config) (exe string, args []string, err er
 				options += fmt.Sprintf(",path=%s", f.Location)
 				options += fmt.Sprintf(",security_model=%s", *f.NineP.SecurityModel)
 				if !*f.Writable {
-					options += ",readonly"
+					options += ",readonly=on"
 				}
 				args = append(args, "-virtfs", options)
 			case limatype.VIRTIOFS:
diff --git a/pkg/driver/wsl2/boot/02-wsl2-setup.sh b/pkg/driver/wsl2/boot/02-no-cloud-init-setup.sh
similarity index 85%
rename from pkg/driver/wsl2/boot/02-wsl2-setup.sh
rename to pkg/driver/wsl2/boot/02-no-cloud-init-setup.sh
index cb563965..ebfe351c 100755
--- a/pkg/driver/wsl2/boot/02-wsl2-setup.sh
+++ b/pkg/driver/wsl2/boot/02-no-cloud-init-setup.sh
@@ -4,8 +4,8 @@
 # SPDX-License-Identifier: Apache-2.0
 
 # This script replaces the cloud-init functionality of creating a user and setting its SSH keys
-# when using a WSL2 VM.
-[ "$LIMA_CIDATA_VMTYPE" = "wsl2" ] || exit 0
+# when cloud-init is not available
+[ "$LIMA_CIDATA_NO_CLOUD_INIT" = "1" ] || exit 0
 
 # create user
 # shellcheck disable=SC2153
@@ -22,4 +22,4 @@ chmod 600 "${LIMA_CIDATA_HOME}"/.ssh/authorized_keys
 echo "${LIMA_CIDATA_USER} ALL=(ALL) NOPASSWD:ALL" | tee -a /etc/sudoers.d/99_lima_sudoers
 
 # symlink CIDATA to the hardcoded path for requirement checks (TODO: make this not hardcoded)
-ln -sfFn "${LIMA_CIDATA_MNT}" /mnt/lima-cidata
+[ "$LIMA_CIDATA_MNT" = "/mnt/lima-cidata" ] || ln -sfFn "${LIMA_CIDATA_MNT}" /mnt/lima-cidata
diff --git a/pkg/driver/wsl2/wsl_driver_windows.go b/pkg/driver/wsl2/wsl_driver_windows.go
index 17886de8..ae8252b1 100644
--- a/pkg/driver/wsl2/wsl_driver_windows.go
+++ b/pkg/driver/wsl2/wsl_driver_windows.go
@@ -310,6 +310,7 @@ func (l *LimaWslDriver) Info() driver.Info {
 	info.Features = driver.DriverFeatures{
 		DynamicSSHAddress:    true,
 		SkipSocketForwarding: true,
+		NoCloudInit:          true,
 		CanRunGUI:            l.canRunGUI(),
 	}
 	return info
diff --git a/pkg/hostagent/hostagent.go b/pkg/hostagent/hostagent.go
index 3affdf6b..41738e2d 100644
--- a/pkg/hostagent/hostagent.go
+++ b/pkg/hostagent/hostagent.go
@@ -162,11 +162,12 @@ func New(ctx context.Context, instName string, stdout io.Writer, signalCh chan o
 
 	vSockPort := limaDriver.Info().VsockPort
 	virtioPort := limaDriver.Info().VirtioPort
+	noCloudInit := limaDriver.Info().Features.NoCloudInit
 
 	if err := cidata.GenerateCloudConfig(ctx, inst.Dir, instName, inst.Config); err != nil {
 		return nil, err
 	}
-	if err := cidata.GenerateISO9660(ctx, limaDriver, inst.Dir, instName, inst.Config, udpDNSLocalPort, tcpDNSLocalPort, o.guestAgentBinary, o.nerdctlArchive, vSockPort, virtioPort); err != nil {
+	if err := cidata.GenerateISO9660(ctx, limaDriver, inst.Dir, instName, inst.Config, udpDNSLocalPort, tcpDNSLocalPort, o.guestAgentBinary, o.nerdctlArchive, vSockPort, virtioPort, noCloudInit); err != nil {
 		return nil, err
 	}
 
diff --git a/pkg/networks/usernet/gvproxy.go b/pkg/networks/usernet/gvproxy.go
index 4df2c62d..363ac996 100644
--- a/pkg/networks/usernet/gvproxy.go
+++ b/pkg/networks/usernet/gvproxy.go
@@ -6,6 +6,7 @@ package usernet
 import (
 	"bufio"
 	"context"
+	"errors"
 	"fmt"
 	"net"
 	"net/http"
@@ -128,9 +129,16 @@ func listenQEMU(ctx context.Context, vn *virtualnetwork.VirtualNetwork) error {
 
 	go func() {
 		defer listener.Close()
+		<-ctx.Done()
+	}()
+
+	go func() {
 		for {
 			conn, err := listener.Accept()
 			if err != nil {
+				if errors.Is(err, net.ErrClosed) {
+					return
+				}
 				logrus.Error("QEMU accept failed", err)
 			}
 
@@ -162,10 +170,18 @@ func listenFD(ctx context.Context, vn *virtualnetwork.VirtualNetwork) error {
 
 	go func() {
 		defer listener.Close()
+		<-ctx.Done()
+	}()
+
+	go func() {
 		for {
 			conn, err := listener.Accept()
 			if err != nil {
+				if errors.Is(err, net.ErrClosed) {
+					return
+				}
 				logrus.Error("FD accept failed", err)
+				continue // since conn is nil
 			}
 
 			files, err := fd.Get(conn.(*net.UnixConn), 1, []string{"client"})
@@ -202,20 +218,20 @@ func listenFD(ctx context.Context, vn *virtualnetwork.VirtualNetwork) error {
 }
 
 func httpServe(ctx context.Context, g *errgroup.Group, ln net.Listener, mux http.Handler) {
+	s := &http.Server{
+		Handler:      mux,
+		ReadTimeout:  10 * time.Second,
+		WriteTimeout: 10 * time.Second,
+	}
 	g.Go(func() error {
 		<-ctx.Done()
-		return ln.Close()
+		return s.Close()
 	})
 	g.Go(func() error {
-		s := &http.Server{
-			Handler:      mux,
-			ReadTimeout:  10 * time.Second,
-			WriteTimeout: 10 * time.Second,
-		}
 		err := s.Serve(ln)
 		if err != nil {
-			if err != http.ErrServerClosed {
-				return err
+			if err == http.ErrServerClosed {
+				return nil
 			}
 			return err
 		}
diff --git a/pkg/networks/usernet/udpfileconn.go b/pkg/networks/usernet/udpfileconn.go
index ef04b841..ea8696c0 100644
--- a/pkg/networks/usernet/udpfileconn.go
+++ b/pkg/networks/usernet/udpfileconn.go
@@ -16,8 +16,7 @@ type UDPFileConn struct {
 func (conn *UDPFileConn) Read(b []byte) (n int, err error) {
 	// Check if the connection has been closed
 	if err := conn.SetReadDeadline(time.Time{}); err != nil {
-		var opErr *net.OpError
-		if errors.As(err, &opErr) && opErr.Err.Error() == "use of closed network connection" {
+		if errors.Is(err, net.ErrClosed) {
 			return 0, errors.New("UDPFileConn connection closed")
 		}
 	}



Test patch:
diff --git a/cmd/limactl/editflags/editflags_test.go b/cmd/limactl/editflags/editflags_test.go
index ae9e178e..c580bb0d 100644
--- a/cmd/limactl/editflags/editflags_test.go
+++ b/cmd/limactl/editflags/editflags_test.go
@@ -181,13 +181,13 @@ func TestYQExpressions(t *testing.T) {
 			name:        "mount",
 			args:        []string{"--mount", "/foo", "--mount", "./bar:w"},
 			newInstance: false,
-			expected:    []string{`.mounts += [{"location": "` + expand("/foo") + `", "writable": false},{"location": "` + expand("./bar") + `", "writable": true}] | .mounts |= unique_by(.location)`},
+			expected:    []string{`.mounts = [{"location": "` + expand("./bar") + `", "mountPoint": "` + expand("./bar") + `", "writable": true},{"location": "` + expand("/foo") + `", "mountPoint": "` + expand("/foo") + `", "writable": false}] + .mounts`},
 		},
 		{
 			name:        "mount-only",
 			args:        []string{"--mount-only", "/foo", "--mount-only", "/bar:w"},
 			newInstance: false,
-			expected:    []string{`.mounts = [{"location": "` + expand("/foo") + `", "writable": false},{"location": "` + expand("/bar") + `", "writable": true}]`},
+			expected:    []string{`.mounts = [{"location": "` + expand("/foo") + `", "mountPoint": "` + expand("/foo") + `", "writable": false},{"location": "` + expand("/bar") + `", "mountPoint": "` + expand("/bar") + `", "writable": true}]`},
 		},
 		{
 			name:        "mixture of mount and mount-only",