diff --git a/pkg/qemu/entitlementutil/entitlementutil.go b/pkg/qemu/entitlementutil/entitlementutil.go new file mode 100644 index 00000000000..15ecaa7f941 --- /dev/null +++ b/pkg/qemu/entitlementutil/entitlementutil.go @@ -0,0 +1,94 @@ +// Package entitlementutil provides a workaround for https://github.com/lima-vm/lima/issues/1742 +package entitlementutil + +import ( + "fmt" + "os" + "os/exec" + "strings" + + "github.com/AlecAivazis/survey/v2" + "github.com/mattn/go-isatty" + "github.com/sirupsen/logrus" +) + +// IsSigned returns an error if the binary is not signed, or the sign is invalid, +// or not associated with the "com.apple.security.hypervisor" entitlement. +func IsSigned(qExe string) error { + cmd := exec.Command("codesign", "--verify", qExe) + out, err := cmd.CombinedOutput() + logrus.WithError(err).Debugf("Executed %v: out=%q", cmd.Args, string(out)) + if err != nil { + return fmt.Errorf("failed to run %v: %w (out=%q)", cmd.Args, err, string(out)) + } + + cmd = exec.Command("codesign", "--display", "--entitlements", "-", "--xml", qExe) + out, err = cmd.CombinedOutput() + logrus.WithError(err).Debugf("Executed %v: out=%q", cmd.Args, string(out)) + if err != nil { + return fmt.Errorf("failed to run %v: %w (out=%q)", cmd.Args, err, string(out)) + } + if !strings.Contains(string(out), "com.apple.security.hypervisor") { + return fmt.Errorf("binary %q seems signed but lacking the \"com.apple.security.hypervisor\" entitlement", qExe) + } + return nil +} + +func Sign(qExe string) error { + ent, err := os.CreateTemp("", "lima-qemu-entitlements-*.xml") + if err != nil { + return fmt.Errorf("failed to create a temporary file for signing QEMU binary: %w", err) + } + entName := ent.Name() + defer os.RemoveAll(entName) + const entXML = ` + + + + com.apple.security.hypervisor + + +` + if _, err = ent.Write([]byte(entXML)); err != nil { + return fmt.Errorf("Failed to write to a temporary file %q for signing QEMU binary: %w", entName, err) + } + ent.Close() + signCmd := exec.Command("codesign", "--sign", "-", "--entitlements", entName, "--force", qExe) + out, err := signCmd.CombinedOutput() + logrus.WithError(err).Debugf("Executed %v: out=%q", signCmd.Args, string(out)) + if err != nil { + return fmt.Errorf("failed to run %v: %w (out=%q)", signCmd.Args, err, string(out)) + } + return nil +} + +// AskToSignIfNotSignedProperly asks to sign the QEMU binary with the "com.apple.security.hypervisor" entitlement. +// +// On Homebrew, QEMU binaries are usually already signed, but Homebrew's signing infrastructure is broken for Intel as of Augest 2023. +// https://github.com/lima-vm/lima/issues/1742 +func AskToSignIfNotSignedProperly(qExe string) { + if isSignedErr := IsSigned(qExe); isSignedErr != nil { + logrus.WithError(isSignedErr).Warnf("QEMU binary %q is not properly signed with the \"com.apple.security.hypervisor\" entitlement", qExe) + var ans bool + if isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()) { + prompt := &survey.Confirm{ + Message: fmt.Sprintf("Try to sign %q with the \"com.apple.security.hypervisor\" entitlement?", qExe), + Default: true, + } + if askErr := survey.AskOne(prompt, &ans); askErr != nil { + logrus.WithError(askErr).Warn("No answer was given") + } + } + if ans { + if signErr := Sign(qExe); signErr != nil { + logrus.WithError(signErr).Warnf("Failed to sign %q", qExe) + } else { + logrus.Infof("Successfully signed %q with the \"com.apple.security.hypervisor\" entitlement", qExe) + } + } else { + logrus.Warn("You have to sign the QEMU binary with the \"com.apple.security.hypervisor\" entitlement manually. See https://github.com/lima-vm/lima/issues/1742 .") + } + } else { + logrus.Infof("QEMU binary %q seems properly signed with the \"com.apple.security.hypervisor\" entitlement", qExe) + } +} diff --git a/pkg/qemu/qemu.go b/pkg/qemu/qemu.go index 00081f6d63f..e2585aa04cb 100644 --- a/pkg/qemu/qemu.go +++ b/pkg/qemu/qemu.go @@ -469,7 +469,7 @@ func qemuMachine(arch limayaml.Arch) string { func Cmdline(cfg Config) (string, []string, error) { y := cfg.LimaYAML - exe, args, err := getExe(*y.Arch) + exe, args, err := Exe(*y.Arch) if err != nil { return "", nil, err } @@ -490,7 +490,7 @@ func Cmdline(cfg Config) (string, []string, error) { } // Architecture - accel := getAccel(*y.Arch) + accel := Accel(*y.Arch) if !strings.Contains(string(features.AccelHelp), accel) { return "", nil, fmt.Errorf("accelerator %q is not supported by %s", accel, exe) } @@ -1003,7 +1003,7 @@ func qemuArch(arch limayaml.Arch) string { return arch } -func getExe(arch limayaml.Arch) (string, []string, error) { +func Exe(arch limayaml.Arch) (string, []string, error) { exeBase := "qemu-system-" + qemuArch(arch) var args []string envK := "QEMU_SYSTEM_" + strings.ToUpper(qemuArch(arch)) @@ -1024,7 +1024,7 @@ func getExe(arch limayaml.Arch) (string, []string, error) { return exe, args, nil } -func getAccel(arch limayaml.Arch) string { +func Accel(arch limayaml.Arch) string { if limayaml.IsNativeArch(arch) { switch runtime.GOOS { case "darwin": diff --git a/pkg/start/start.go b/pkg/start/start.go index acecfde3783..dcdffc382dc 100644 --- a/pkg/start/start.go +++ b/pkg/start/start.go @@ -9,11 +9,14 @@ import ( "os" "os/exec" "path/filepath" + "runtime" "text/template" "time" "github.com/lima-vm/lima/pkg/driver" "github.com/lima-vm/lima/pkg/driverutil" + "github.com/lima-vm/lima/pkg/qemu" + "github.com/lima-vm/lima/pkg/qemu/entitlementutil" "github.com/lima-vm/lima/pkg/downloader" "github.com/lima-vm/lima/pkg/fileutils" @@ -101,6 +104,18 @@ func Start(ctx context.Context, inst *store.Instance) error { haSockPath := filepath.Join(inst.Dir, filenames.HostAgentSock) + // Ask the user to sign the qemu binary with the "com.apple.security.hypervisor" if needed. + // Workaround for https://github.com/lima-vm/lima/issues/1742 + if runtime.GOOS == "darwin" && inst.VMType == limayaml.QEMU { + qExe, _, err := qemu.Exe(inst.Arch) + if err != nil { + return fmt.Errorf("failed to find the QEMU binary for the architecture %q: %w", inst.Arch, err) + } + if accel := qemu.Accel(inst.Arch); accel == "hvf" { + entitlementutil.AskToSignIfNotSignedProperly(qExe) + } + } + prepared, err := Prepare(ctx, inst) if err != nil { return err