Testing Mender with QEMU ARM64

Iterating on Mender integration with physical ARM hardware means dealing with SD cards, serial cables, and recovery mechanisms when something goes wrong. QEMU provides a faster development cycle by running your Mender-enabled images in a virtual environment, letting you test update mechanisms, rollback behavior, and partition layouts without touching hardware.

This tutorial walks you through building and running a Mender-enabled ARM64 QEMU image using kas and the pre-configured setup from meta-mender-community. You’ll use the scarthgap branch and boot with modern UEFI firmware, matching the approach used in contemporary embedded systems.

Version notes

The tutorial has been verified on Ubuntu 22.04 as of 2025-11-14.

Yocto Project Tutorial applies Maintenance
scarthgap (5.0) :test_works: :test_works: LTS

This tutorial specifically targets the scarthgap branch of meta-mender-community. While the kas-based approach may work with other Yocto releases if corresponding configurations exist in meta-mender-community, only scarthgap has been verified for this tutorial.

Prerequisites

You’ll need:

If you’re new to kas, take a look at the kas tutorial first. This tutorial assumes you understand kas basics and focuses on the QEMU-specific configuration.

Build and Run

The meta-mender-community repository provides a ready-to-use kas configuration for ARM64 QEMU. Let’s get you to a running system:

First, clone the repository and enter the kas shell:

git clone -b scarthgap https://github.com/mendersoftware/meta-mender-community.git
cd meta-mender-community
kas shell kas/qemuarm64.yml

The kas shell command initializes the Yocto/OpenEmbedded build environment, which is necessary for both building and running QEMU. You’re now in a shell where the build configuration and environment variables are properly set up.

From within this kas shell, build the image:

bitbake core-image-minimal

After kas shell has carried out the setup in a reproducible manner, handling layer fetching, patch application, configuration, this actually runs the build. The build process takes time on the first run as it downloads sources and compiles from scratch. Grab a coffee.

Once the build completes, launch QEMU:

runqemu

You can also run QEMU without a graphical window using the nographic flag:

runqemu nographic

The runqemu script reads the qemuboot.conf generated during build and launches QEMU with the right parameters. You’ll see the EDK2 firmware initialize, grub load, and the kernel boot.

To exit QEMU, use Ctrl-A then X.

Note on login: By default, this image allows root login without a password, which is a “secure by default” behaviour.

For development purposes, and not suitable for production, the Yocto Project provides IMAGE_FEATURES to control this behavior. In the context of this build, add the following to the conf/local.conf file in your build directory:

EXTRA_IMAGE_FEATURES += "empty-root-password"

After a rebuild with bitbake core-image-minimal", you can log in as root` (no password by default).

For production images, you should set a root password using the extrausers class or disable root login entirely. See the “Security Considerations” section below for more details.

Performance note: You’re running an ARM64 guest on an x86-64 host without KVM acceleration. Boot and runtime will be noticeably slower than native execution or x86-on-x86 QEMU with KVM. This is the tradeoff for testing on an architecture that’s more representative of embedded ARM targets. The slower execution is rarely a blocker for testing Mender update mechanisms.

To exit the kas shell afterwards, type exit or press Ctrl-D.

On just building

If you only want to build the image without immediately running it, you can use kas build as a shortcut. This command combines the environment setup and build process in a single step:

kas build kas/qemuarm64.yml

This is equivalent to entering the kas shell and running bitbake core-image-minimal, but handles everything automatically. Use this approach when you’re building images for deployment or when you want to run QEMU separately later. You’ll still need to enter the kas shell afterwards to use runqemu.

Understanding the Configuration

Now that you have a working system, let’s look at what the kas/qemuarm64.yml file configured:

UEFI Boot Setup

The configuration enables UEFI boot through several settings:

EFI_PROVIDER = "grub-efi"
MACHINE_FEATURES += "efi"
MENDER_EFI_LOADER = "edk2-firmware"

This means your image boots through EDK2 UEFI firmware (the same open-source UEFI implementation used by many ARM boards), which then loads grub from the EFI partition, which finally boots the kernel. This matches modern embedded systems far better than legacy boot methods.

QEMU Runtime Configuration

Key QEMU settings include:

QB_MACHINE = "-machine virt,secure=on"
QB_MEM = "-m 2048"
QB_DEFAULT_FSTYPE = "uefiimg"
QB_DEFAULT_BIOS = "QEMU_EFI.fd"
QB_DEFAULT_KERNEL = "none"

The critical setting is QB_DEFAULT_KERNEL = "none". Traditionally, runqemu loads the kernel separately and passes it to QEMU with the -kernel option. With UEFI boot, the kernel lives inside the disk image and gets loaded by grub. Setting this to “none” tells runqemu to skip separate kernel loading.

This required a small patch to openembedded-core’s qemuboot.bbclass, which kas applies automatically. The patch simply teaches qemuboot to handle QB_DEFAULT_KERNEL = "none" without trying to resolve it as a file path. More details in Appendix B if you’re curious about the technical implementation.

Security Considerations

The default configuration provides passwordless root access for development convenience. This is controlled through Yocto’s IMAGE_FEATURES/EXTRA_IMAGE_FEATURES variable.

Understanding IMAGE_FEATURES for Root Access

Yocto provides several image features that control root login behavior:

  • empty-root-password: Allows root login with an empty password. This doesn’t set the password itself; rather, it disables the mechanism that forces a non-empty root password.
  • allow-empty-password: Allows SSH servers (Dropbear and OpenSSH) to accept logins from accounts with empty passwords.
  • allow-root-login: Allows SSH servers to accept root logins.

Important: Prior to Yocto 5.2 (walnascar), these features were bundled in a debug-tweaks feature. Starting with Yocto 5.2, debug-tweaks was removed, and you must specify individual features explicitly. Since this tutorial targets scarthgap (5.0), debug-tweaks may still be available in some configurations, but using the explicit features is recommended for forward compatibility.

For Production Images

When building images for production deployment, you should configure security settings appropriately. The following examples show kas configuration overrides - YAML snippets that extend the base kas/qemuarm64.yml configuration. For more details on how kas configuration layering works, see the kas tutorial mentioned in the prerequisites.

You can apply these overrides by:

  • Creating a custom kas YAML file that includes the base configuration and adds your overrides, or
  • Adding the settings directly to your kas/qemuarm64.yml file (though this modifies the upstream configuration)
  1. Set a root password using the extrausers class:
local_conf_header:
  extrausers: |
    INHERIT += "extrausers"
    EXTRA_USERS_PARAMS = "usermod -P 'your-hashed-password' root;"

Generate a hashed password with: openssl passwd -6 "your-password"

  1. Remove development features from IMAGE_FEATURES to ensure no passwordless access is enabled.

For this tutorial’s development and testing purposes, the default passwordless root access simplifies the workflow.

Next Steps

Now that you have a working QEMU environment:

  • Test Mender artifact installation with mender install
  • Experiment with the dual partition layout (/dev/vda2 and /dev/vda3)
  • Trigger updates and verify rollback behavior
  • Modify the kas configuration to include your own layers

If you want to connect your QEMU instance to a Mender server for full OTA testing, see Appendix A.

That’s it! You now have a reproducible ARM64 QEMU environment for Mender development.


Appendix A: Mender Server Integration (Optional)

To test OTA updates from a Mender server, you’ll need to configure server settings before building.

Create a kas overlay file kas/qemuarm64-server.yml:

header:
  version: 14
  includes:
  - qemuarm64.yml

local_conf_header:
  mender-server: |
    MENDER_SERVER_URL = "https://hosted.mender.io"
    MENDER_TENANT_TOKEN = "your-tenant-token-here"

Replace your-tenant-token-here with your actual tenant token from hosted.mender.io or your self-hosted server.

Build with the overlay:

kas build kas/qemuarm64-server.yml

After booting, the Mender client will connect to your server. Authorize the device in the Mender UI, then you can deploy updates through the standard Mender workflow.

Security note: Don’t commit files containing real tenant tokens to public repositories. For production workflows, consider using environment variable substitution or kas’s secret management features.

Appendix B: The qemuboot Patch Explained

The kas configuration applies a patch to openembedded-core that enables QB_DEFAULT_KERNEL = "none". Here’s why it’s needed:

The qemuboot.bbclass generates a qemuboot.conf file that runqemu reads to determine how to launch QEMU. For the kernel setting, it traditionally does:

kernel_link = os.path.join(d.getVar('DEPLOY_DIR_IMAGE'), d.getVar('QB_DEFAULT_KERNEL'))
kernel = os.path.realpath(kernel_link)

This works fine when QB_DEFAULT_KERNEL points to a kernel file like “bzImage” or “zImage”. But when set to “none” (signaling UEFI boot where the kernel is in the disk image), this code tries to resolve a path to a file literally named “none”, which fails.

The patch adds a simple check:

qb_default_kernel = d.getVar('QB_DEFAULT_KERNEL')
if qb_default_kernel == "none":
  kernel = "none"
else:
  kernel_link = os.path.join(d.getVar('DEPLOY_DIR_IMAGE'), qb_default_kernel)
  kernel = os.path.realpath(kernel_link)

Now “none” passes through cleanly, runqemu sees it, and knows to skip the -kernel option to QEMU, allowing the UEFI firmware and grub to handle kernel loading instead.

The patch is located at patches/openembedded-core/0001-qemuboot-handle-QB_DEFAULT_KERNEL-none-properly.patch in meta-mender-community.