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) |
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:
kasinstalled:pip install kasgitfor cloning repositories- A properly configured Yocto build host - If you haven’t set up a Yocto build environment before, follow either:
- Preparing the Yocto Project environment from scratch on Mender Hub, or
- Yocto Project Quick Build from the official Yocto Project documentation
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
kasYAML file that includes the base configuration and adds your overrides, or - Adding the settings directly to your
kas/qemuarm64.ymlfile (though this modifies the upstream configuration)
- Set a root password using the
extrausersclass:
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"
- Remove development features from
IMAGE_FEATURESto 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/vda2and/dev/vda3) - Trigger updates and verify rollback behavior
- Modify the
kasconfiguration 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.
