Tutorial: How to virtualize FreeBSD on ARM-based Macs

This post shows you how to automate virtualization of FreeBSD 14.1 on MacOS Sonoma with an open source solution that achieves high performance and is speed-wise near bare metal.

Install the virtualization engine

There are various options for virtualization on MacOS, like Parallels Desktop, VMWare Fusion, or others. The most versatile and open source offering is to use QEMU. It is an excellent solution that provides options for both emulation and virtualization. With its support for Hypervisor.framework you can run FreeBSD with native speed on the ARM architecture and thus easy test different setups.

The easiest way for installation is via MacPorts. So first go and download MacPorts for MacOS Sonoma via the official installer and perform the installation of the package. Check that everything worked as intented
by calling the ports command via Terminal and perform the installation of QEMU.

port install packer
port install qemu
port install socket_vmnet

Prepare the UEFI images

To successfully boot FreeBSD for ARM, you'll first need a suitable firmware. I couldn't make to boot an image on the default firmware that ships with QEMU. So I searched for other options. My approach was to download OVMF, a project designed to enable UEFI support on VMs. As the complexity of building the firmware from source is quite high, the most straightforward approach is to use a prebuilt version. This simplifies the process and ensures you have a reliable starting point for your virtual machine setup.

mkdir -p Firmware
(cd Firmware && curl -OL https://github.com/rust-osdev/ovmf-prebuilt/releases/download/edk2-stable202402-r1/edk2-stable202402-r1-bin.tar.xz
tar xzf edk2-stable202402-r1-bin.tar.xz)

The next step is copy the firmware to a writable image. While this step is optional, it provides the advantage of persisting any updates, making your virtual machine setup more flexible and adaptable.

dd if=Firmware/edk2-stable202402-r1-bin/aarch64/code.fd of=Firmware/code-freebsd-aarch64.img conv=notrunc
dd if=Firmware/edk2-stable202402-r1-bin/aarch64/vars.fd of=Firmware/vars-freebsd-aarch64.img conv=notrunc

Download FreeBSD

With the firmware prepared, you can create a packer configuration, which defines the settings and scripts necessary for building your machine images. This configuration file, typically written in JSON or HCL, allows you to specify the source template, provisioners, and builders. With this setup, you can automate the creation of a consistent and reliable machine for development and production environments.

mkdir Packer

Create the packer configuration in Packer/freebsd_amd64.pkr.hcl:

    packer {
      required_plugins {
        qemu = {
          version = "~> 1"
          source  = "github.com/hashicorp/qemu"
        }
      }
    }
    
    source "qemu" "freebsd" {
      iso_url           = "https://download.freebsd.org/releases/amd64/amd64/ISO-IMAGES/14.1/FreeBSD-14.1-RELEASE-amd64-disc1.iso"
      iso_checksum      = "sha256:5321791bd502c3714850e79743f5a1aedfeb26f37eeed7cb8eb3616d0aebf86b"
      efi_firmware_code = "../Firmware/code-freebsd-amd64.img"
      efi_firmware_vars = "../Firmware/vars-freebsd-amd64.img"
      output_directory  = "image_freebsd_amd64"
      qemu_binary       = "qemu-system-x86_64"
      disk_size         = "2G"
      format            = "raw"
      display           = "cocoa"
      ssh_username      = "root"
      ssh_password      = "packer"
      ssh_timeout       = "20m"
      vm_name           = "freebsd.img"
      net_device        = "virtio-net-pci"
      disk_interface    = "virtio"
      boot_wait         = "5s"
      http_directory    = "http_freebsd"
      shutdown_command  = "poweroff"
    
      qemuargs = [
        ["-machine", "q35,accel=tcg"],
        ["-cpu", "max"],
        ["-smp", "4"],
        ["-boot", "strict=off"],
        ["-device", "qemu-xhci"],
        ["-device", "usb-kbd"],
        ["-device", "usb-tablet"],
        ["-device", "intel-hda"],
        ["-device", "hda-duplex"]
      ]
    
      boot_command = [
        "<esc><wait>", 
        "boot -s<enter>", 
        "<wait15s>", 
        "/bin/sh<enter><wait>", 
        "mdmfs -s 100m md /tmp<enter><wait>", 
        "dhclient -l /tmp/dhclient.lease.vtnet0 vtnet0<enter><wait5>", 
        "fetch -o /tmp/installerconfig http://{{ .HTTPIP }}:{{ .HTTPPort }}/installerconfig<enter><wait5>",
        "bsdinstall script /tmp/installerconfig<enter>"
      ]
    }
    
    build {
      name = "build-freebsd"
      sources = [
        "source.qemu.freebsd"
      ]
      provisioner "shell" {
        execute_command = "chmod +x {{ .Path }}; env {{ .Vars }} {{ .Path }}"
        scripts = [
          "shell/freebsd-update.sh",
          "shell/freebsd-install.sh",
          "shell/freebsd-cleanup.sh"
        ]
      }
    
    }

Booting FreeBSD

With the firmware in place and the disk image prepared, we are now ready to boot. We are going to launch the virtual machine with an accelerator that will bring up your FreeBSD environment, allowing you to login directly via the terminal.

 #!/bin/zsh

socket_vmnet_client /var/run/socket_vmnet \
qemu-system-x86_64 \
  -boot strict=off \
  -machine q35 \
  -cpu max \
  -accel tcg \
  -smp 4 \
  -m 4096 \
  -drive file=Firmware/code-freebsd-amd64.img,format=raw,if=pflash,readonly=on \
  -drive file=Packer/image_freebsd_amd64/efivars.fd,format=raw,if=pflash \
  -device virtio-gpu-pci \
  -display default,show-cursor=on \
  -device qemu-xhci \
  -device usb-kbd \
  -device usb-tablet \
  -device intel-hda \
  -device hda-duplex \
  -drive id=main,if=none,file=Packer/image_freebsd_amd64/freebsd.img,format=raw,cache=writethrough \
  -device virtio-blk-pci,drive=main \
  -device virtio-net-pci,mac=52:54:00:12:34:53,netdev=net0 -netdev socket,id=net0,fd=3 \
  -nographic

You can find the source and relevant scripts on Github.