Yocto

Yocto Emulation: Setting Up QEMU with TPM

As promised, it’s time for the QEMU follow-up. Last time we got Yocto’s runqemu command to launch u-boot, boot up a kernel, and mount a virtual drive with multiple partitions. Quite a lot of stuff. This time we are “just” going to add a TPM device to the virtual machine. As before, you can find the example meta-layer from Github. It contains the example snippets presented in this blog text, and should be ready to use.

Why is this virtualized TPM worth the effort? Well, if you have ever been in a painful situation where you’re working with TPMs and you’re writing some scripts or programs using them, you know that the development is not as straightforward as one would hope. The flows tend to be confusing, frustrating, and difficult. Using a virtual environment that’s easy to reset and that’s quite close to the actual hardware is a nice aid for developing and testing these types of applications.

In a nutshell, the idea is to run swtpm TPM emulator on the host machine, and then launch QEMU Arm device emulator that talks with the swtpm process. QEMU has an option for a TPM device that can be passed through to the guest device, so the process is fairly easy. With these systems in place, we can have a virtual TPM chip inside the virtual machine. *insert yo dawg meme here*

TPM Emulation With swtpm

Because I’m terrible at explaining things understandably, I’m going to ask my co-author ChatGPT to summarise in one paragraph what a TPM is:

Trusted Platform Module (TPM) is a hardware-based security feature integrated into computer systems to provide a secure foundation for various cryptographic functions and protect sensitive data. TPM securely stores cryptographic keys, certificates, and passwords, ensuring they remain inaccessible to unauthorized entities. It enables secure boot processes, integrity measurement, and secure storage of credentials, enhancing the overall security of computing devices by thwarting attacks such as tampering, unauthorized access, and data breaches.

I’m not sure if this is easier to understand than my ramblings, but I guess it makes the point clear. It’s a hardware chip that can be used to store and generate secrets. One extra thing worth knowing is that there are two notable versions of the TPM specification: 1.2 and 2.0. When I’m talking about TPM in this blog text, I mean TPM 2.0.

Since we’re using emulated hardware, we don’t have the “hardware” part in the system. Well, QEMU has a passthrough option for hardware TPMs, but for development purposes it’s easier to have an emulated TPM, something that swtpm can be used for. Installing swtpm is straightforward, as it can be found in most of the package repositories. For example, on Ubuntu, you can just run:

sudo apt install swtpm

Building swtpm is also an option. It has quite a few dependencies though, so you may want to consider just fetching the packages. Sometimes taking the easy route is allowed.

Whichever option you choose, once you’re ready you can run the following commands to set up the swtpm and launch the swtpm process:

mkdir /tmp/mytpm1
swtpm_setup --tpmstate /tmp/mytpm1 \
  --create-ek-cert \
  --create-platform-cert \
  --create-spk \
  --tpm2 \
  --overwrite
swtpm socket --tpmstate dir=/tmp/mytpm1 \
  --ctrl type=unixio,path=/tmp/mytpm1/swtpm-sock \
  --tpm2 \
  --log level=20

Once the process launches, it opens a Unix domain socket that listens to the incoming connections. It’s worth knowing that the process gets launched as a foreground job, and once a connected process exits swtpm exits as well. Next, we’re going to make QEMU talk with the swtpm daemon.

QEMU TPM

Fortunately, making QEMU communicate with TPM isn’t anything groundbreaking. There’s a whole page of documentation dedicated to this topic, so we’re just going to follow it. For Arm devices, we want to pass the following additional parameters to QEMU:

-chardev socket,id=chrtpm,path=/tmp/mytpm1/swtpm-sock \
-tpmdev emulator,id=tpm0,chardev=chrtpm \
-device tpm-spapr,tpmdev=tpm0 \

These parameters should result in the QEMU connecting to the swtpm, and using the emulated software TPM as a TPM in the emulated machine. Simple as.

One thing worth noting though. Since we’re adding a new device to the virtual machine, the device tree changes as well. Therefore, we need to dump the device tree again. This was discussed more in-depth in the first part of this emulation exercise, so I recommend reading that. In summary, you can dump the device tree with the following runqemu command:

BIOS=tmp/deploy/images/qemuarm-uboot/u-boot.bin \
runqemu \
core-image-base nographic wic.qcow2 \
qemuparams="-chardev socket,id=chrtpm,path=/tmp/mytpm1/swtpm-sock \
-tpmdev emulator,id=tpm0,chardev=chrtpm \
-device tpm-tis-device,tpmdev=tpm0 \
-machine dumpdtb=qemu.dtb"

Then, you need to move the dumped binary to a location where it can get installed to the boot partition as a part of the Yocto build. This was also discussed in the first blog text.

TPM2.0 Software Stack

Configuring Yocto

Now that we have the virtualized hardware in order, it’s time to get the software part sorted out. Yocto has a meta-layer that contains security features and programs. That layer is aptly named meta-security. To add the TPM-related stuff into the firmware image, add sub-layer meta-tpm to bblayers.conf. meta-tpm has dependencies to meta-openembedded sub-layers meta-oe and meta-python, so add those as well.

Once the layers are added, we still need to configure the build a bit. The following should be added to your distro.conf, or if you don’t have one, local.conf should suffice:

DISTRO_FEATURES:append = " tpm"

Configuring Linux Kernel

Next, to get the TPM device working together with Linux, we need to configure the kernel. First of all, the TPM feature needs to be enabled, and then the driver for our emulated chip needs to be added. If you were curious enough to decompile the QEMU device tree binary, you maybe noticed that the emulated TPM device is compatible with tcg,tpm-tis-mmio. Therefore, we don’t need a specific driver, the generic tpm-tis driver should do. The following two config lines both enable TPM and add the driver:

CONFIG_TCG_TPM=y
CONFIG_TCG_TIS=y

If you’re wondering what TCG means, it stands for Trusted Computing Group, the organization that has developed the TPM standard. TIS on the other hand stands for TPM Interface Specification. There are a lot of TLAs here that begin with the letter T, and we haven’t even seen all of them yet.

Well, here’s the yo dawg meme.

Configuring U-Boot

Configuring TPM support for U-Boot is quite simple. Actually, the U-Boot I built worked straight away with the defconfig. However, if you have issues with TPM in U-Boot, you should ensure that you have the following configuration items enabled:

# Enable TPM 2.0
CONFIG_TPM=y
CONFIG_TPM_V2=y
# Add MMIO interface for device
CONFIG_TPM2_MMIO=y
# Add TPM command
CONFIG_CMD_TPM=y
# This should be enabled automatically if
# CMD_TPM and TPM_V2 are enabled
CONFIG_CMD_TPM_V2=y

Installing tpm2-tools

In theory, we now should have completed the original goal of booting a Yocto image on an emulator that has a virtual TPM. However, there’s still nothing that uses the TPM. To add plenty of packages, tpm2-tools among them, we can add the following to the image configuration:

IMAGE_INSTALL:append = " \
    packagegroup-security-tpm2 \
    libtss2-tcti-device \
"

packagegroup-security-tpm2 contains the following packages:

tpm2-tools
trousers
tpm2-tss
libtss2
tpm2-abrmd
tpm2-pkcs11

For our testing purposes, we are mostly interested in tpm2-tools and tpm2-tss, and libtss2 that tpm2-tools requires. TSS here stands for TPM2 Software Stack. trousers is an older implementation of the stack, tpm2-abrmd (=access broker & resource manager daemon) didn’t work for me (and AFAIK using a kernel-managed device is preferred anyway), and PKCS#11 isn’t required for our simple example. libtss2-tcti-device is required to enable a TCTI (TPM Command Transmission Interface) for communication with Linux kernel TPM device files. These are the last acronyms, so now you can let out a sigh of relief.

Running QEMU

Now you can rebuild the image to compile a suitable kernel and user-space tools. Once the build finishes, you can use the following command to launch QEMU (ensure that swtpm is running):

BIOS=tmp/deploy/images/qemuarm-uboot/u-boot.bin \
runqemu \
core-image-base nographic wic.qcow2 \
qemuparams="-chardev socket,id=chrtpm,path=/tmp/mytpm1/swtpm-sock \
-tpmdev emulator,id=tpm0,chardev=chrtpm \
-device tpm-tis-device,tpmdev=tpm0"

Then, stop the booting process to drop into the U-Boot terminal. We wrote the boot script in the previous blog, but now we can add tpm2 commands to initialize and self-test the TPM. The first three commands of this complete boot script set-up and self-test the TPM:

# Initalize TPM
tpm2 init
tpm2 startup TPM2_SU_CLEAR
tpm2 self_test full
# Set boot arguments for the kernel
setenv bootargs root=/dev/vda2 console=ttyAMA0
# Load kernel image
setenv loadaddr 0x40200000
fatload virtio 0:1 ${loadaddr} zImage
# Load device tree binary
setenv loadaddr_dtb 0x49000000
fatload virtio 0:1 ${loadaddr_dtb} qemu.dtb
# Boot the kernel
bootz ${loadaddr} - ${loadaddr_dtb}

Now, once the machine boots up, you should see /dev/tpm0 and /dev/tpmrm0 devices present in the system. tpm0 is a direct access device, and tpmrm0 is a device using the kernel’s resource manager. The latter of these is the alternative to tpm2-abrmd, and we’re going to be using it for a demo.

TPM Demo

Before we proceed, I warn you that my knowledge of actual TPM usage is a bit shallow. So, the example presented here may not necessarily follow the best practices, but it should perform a simple task that should prove that the QEMU TPM works. We are going to create a key, store it in the TPM, sign a file and verify the signature. When you’ve got the device booted with the swtpm running in the background, you can start trying out these commands:

# Set environment variable for selecting TPM device
# instead of the abrmd.
export TPM2TOOLS_TCTI="device:/dev/tpmrm0"
# Create contexts
tpm2_createprimary -C e -c primary.ctx
tpm2_create -G rsa -u rsa.pub -r rsa.priv -C primary.ctx
# Load and store contexts
tpm2_load -C primary.ctx -u rsa.pub -r rsa.priv -c rsa.ctx
tpm2_evictcontrol -C o -c primary.ctx 0x81010002
tpm2_evictcontrol -C o -c rsa.ctx 0x81010003
# Remove generated files and create message
rm rsa.pub rsa.priv rsa.ctx primary.ctx
echo "my message" > message.dat
# Sign and verify signature with TPM handles
tpm2_sign -c 0x81010003 -g sha256 -o sig.rssa message.dat
tpm2_verifysignature -c 0x81010003 -g sha256 -s sig.rssa -m message.dat

If life goes your way, all the commands should succeed without issues and you can create and verify the signature using the handles in the TPM. Usually, things aren’t that simple. If you see errors related to abrmd, you may need to define the TCTI as the tpmrm0 device. The TPM2TOOLS_TCTI environment variable should do that. However, if that doesn’t work you can try adding -T "device:/dev/tpmrm0" to the tpm2_* commands, so for example the first command looks like this:

tpm2_createprimary -C e -c primary.ctx -T "device:/dev/tpmrm0"

When running the tpm2_* commands, you should see swtpm printing out plenty of information. This information includes requests and responses received and sent by the daemon. To make some sense of these hexadecimal dumps, you can use tpmstream tool.

That should wrap up my texts about QEMU, Yocto and TPM. Hopefully, these will help you set up a QEMU device that has a TPM in it. I also hope that in the long run this setup helps you to develop and debug secure Linux systems that utilize TPM properly. Perhaps I’ll write more about TPMs in the future, it was quite difficult to find understandable sources and examples utilizing its features. But maybe first I’d need to understand the TPMs a bit better myself.

Yocto Emulation: Setting Up QEMU with U-Boot

I’ve been thinking about the next topic for the Yocto Hardening blog series, and it’s starting to feel like the easy topics are running out. Adding and using non-root users, basic stuff. Running a tool to check kernel configuration, should be simple enough. Firewalls, even your grandma knows what a firewall is.

So, I started to look into things like encryption and secure boot, but turns out they are quite complicated topics. Also, they more or less require a TPM (Trusted Platform Module), and I don’t have a board with such a chip. And even if I did, it’d be more useful to have flexible hardware for future experiments. And for writing blog texts that can be easily followed along it’d be beneficial if that hardware would be easily available for everyone.

Hardware emulation sounds like a solution to all of these problems. Yocto provides a script for using QEMU (Quick EMUlator) in the form of runqemu wrapper. However, by default that script seems to just boot up the kernel and root file system using whatever method QEMU considers the best (depending on the architecture). Also, runqemu passes just the root file system partition as a single drive to the emulator. Emulating a device with a bootloader and a partitioned disk image is a bit tricky thing to do, but that’s exactly what we’re going to do in this text. In the next part we’re going to throw a TPM into the mix, but for now, let’s focus on the basics.

Configuring the Yocto Build

Before we start, I’ll say that you can find a meta-layer containing the code presented here from GitHub. So if you don’t want to copy-paste everything, you can clone the repo. It’ll contain some more features in the future but the basic functionality created in this blog text should be present in the commit cf4372a.

Machine Configuration

To start, we’re going to define some variables related to the image being built. To do that, we will define our machine configuration that is an extension of a qemuarm configuration:

require conf/machine/qemuarm.conf

# Use the same overrides as qemuarm machine
MACHINEOVERRIDES:append = ":qemuarm"

# Set the required entrypoint and loadaddress
# These are usually 00008000 for Arm machines
UBOOT_ENTRYPOINT =       "0x00008000"
UBOOT_LOADADDRESS =      "0x00008000"

# Set the imagetype
KERNEL_IMAGETYPE = "zImage"
# Set kernel loaddaddr, should match the one u-boot uses
KERNEL_EXTRA_ARGS += "LOADADDR=${UBOOT_ENTRYPOINT}"

# Add wic.qcow2 image that can be used by QEMU for drive image
IMAGE_FSTYPES:append = " wic.qcow2"

# Add wks file for image partition definition
WKS_FILE = "qemu-test.wks"

# List artifacts in deploy dir that we want to be in boot partition
IMAGE_BOOT_FILES = "zImage qemu.dtb"

# Ensure things get deployed before wic builder tries to access them
do_image_wic[depends] += " \
    u-boot:do_deploy \
    qemu-devicetree:do_deploy \
"

# Configure the rootfs drive options. Biggest difference to original is
# format=qcow2, in original the default format is raw
QB_ROOTFS_OPT = "-drive id=disk0,file=@ROOTFS@,if=none,format=qcow2 -device virtio-blk-device,drive=disk0"

Drive Image Configuration with WIC

Once that is done, we can write the wks file that’ll guide the process that creates the wic image. wic image can be considered as a drive image with partitions and such. Writing wks files is worth a blog text of its own, but here’s the wks file I’ve been using that creates a drive containing two partitions:

part /boot --source bootimg-partition --ondisk vda --fstype=vfat --label boot --active --align 1024
part / --source rootfs --use-uuid --ondisk vda --fstype=ext4 --label platform --align 1024

The first partition is a FAT boot partition where we will store the kernel and device tree so that the bootloader can load them. Second is the ext4 root file system, containing all the lovely binaries Yocto spends a long time building.

Device Tree

We have defined the machine and the image. The only thing that is still missing is the device tree. The device tree defines the hardware of the machine in a tree-like format and should be passed to the kernel by the bootloader. QEMU generates a device tree on-the-fly, based on the parameters passed to it. The generated device tree binary can be dumped by adding -machine dumpdtb=qemu.dtb to the QEMU command. With runqemu, you can use the following command to pass the parameter:

runqemu core-image-base nographic wic.qcow2 qemuparams="-machine dumpdtb=qemu.dtb"

However, here we have a circular dependency. The image depends on the qemu-devicetree recipe to deploy the qemu.dtb, but runqemu cannot be run without an image, so the image needs to built to dump the device tree. To sort this out, remove the qemu-devicetree dependency from the machine configuration, build once, and dump the device tree. Then re-enable the dependency.

After this, you can give the device tree binary to a recipe and deploy it from there. Or you could maybe decompile it to a source file, and then re-compile the source as a part of kernel build to do things “correctly”. I was lazy and just wrote a recipe that deploys the binary:

SUMMARY = "QEMU device tree binary"
DESCRIPTION = "Recipe deploying the generated QEMU device tree binary blob"
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302"

SRC_URI = "file://qemu.dtb"

inherit deploy

do_deploy() {
    install -d ${DEPLOYDIR}
    install -m 0664 ${WORKDIR}/*.dtb ${DEPLOYDIR}
}

addtask do_deploy after do_compile before do_build

Once that is done, you should be able to build the image. I recommend checking out the meta-layer repo if you found this explanation confusing. I’m using core-image-base as the image recipe, but you should be able to use pretty much any image, assuming it doesn’t overwrite variables in machine configuration.

Setting up QEMU

Running runqemu

We should now have an image that contains everything needed to emulate a boot process: it has a bootloader, a kernel and a file system. We just need to get the runqemu to play along nicely. To start booting from the bootloader, we want to pass the bootloader as a BIOS for QEMU. Also, we need to load the wic.qcow2 file instead of the rootfs.ext4 as the drive source so that we have the boot partition present for the bootloader. All this can be achieved with the following command:

BIOS=tmp/deploy/images/qemuarm-uboot/u-boot.bin runqemu core-image-base nographic wic.qcow2

nographic isn’t mandatory if you’re running in an environment that has visual display capabilities. To this day I still don’t quite understand how the runqemu argument parsing works, even though I tried going through the script source. It simultaneously feels like it’s very picky about the order of the parameters, and that it doesn’t matter at all what you pass and at what position. But at least the command above works.

Booting the Kernel

If things go well, you should be greeted with the u-boot log. If you’re quick, spam any key to stop the boot, and if you’re not, spam Ctrl-C to stop bootloader’s desperate efforts of TFTP booting. I’m not 100% sure why the default boot script fails to load the kernel, I think the boot script doesn’t like the boot partition being a FAT partition on a virtio interface. To be honest, I would have been more surprised if the stock script would have worked out of the box. However, what works is the script below:

# Set boot arguments for the kernel
setenv bootargs root=/dev/vda2 console=ttyAMA0
# Load kernel image
setenv loadaddr 0x40200000
fatload virtio 0:1 ${loadaddr} zImage
# Load device tree binary
setenv loadaddr_dtb 0x49000000
fatload virtio 0:1 ${loadaddr_dtb} qemu.dtb
# Boot the kernel
bootz ${loadaddr} - ${loadaddr_dtb}

This script does exactly what the comments say: it loads the two artefacts from the boot partition and boots the board. We don’t have an init RAM disk, so we skip the second parameter of bootz. I also tried to create a FIT (firmware image tree) image with uImage to avoid having multiple boot files in the boot partition. Unfortunately, that didn’t quite work out. Loading the uImage got the device stuck with a nefarious "Starting kernel ..." message for some reason.

Back to the task at hand: if things went as they should have, the kernel should boot with the bootz, and eventually you should be dropped to the kernel login prompt. You can run mount command to see that the boot partition gets mounted, and cat /proc/cmdline to check that vda2 indeed was the root device that was used.

Closing Words And What’s Next

Congratulations! You got the first part of the QEMU set-up done. The second half with the TPM setup will follow soon. The example presented here could be improved in a few ways, like by adding a custom boot script for u-boot so that the user doesn’t have to input the script manually to boot the device, and by getting that darn FIT image working. But those will be classified as “future work” for now. Until next time!

The second part where the TPM gets enabled is out now!

Yocto Hardening: Kernel and GCC Configuration

Find all of the Yocto hardening texts from here!

Would you like to make your Yocto image a tiny bit harder to hack ‘n’ crack? Of course you would. This time we’re going to be doing two things to improve its security: hardening the Linux kernel, and setting the hardening flags for GCC. The motivation for these is quite obvious. Kernel is the privileged core of the system, so it better be as hardened as possible. GCC compilation flags on the other hand affect almost every C and C++ binary and library that gets compiled into the system. As you may know, over the years we’ve gotten quite a few of them, so it’s a good idea to use any help the compiler can provide with hardening them.

On the other hand, who wouldn’t like to live a bit dangerously?

Kernel Configuration Hardening

Linux kernel is the heart of the operating system and environment. As one can guess, configuring the kernel incorrectly or enabling everything in the kernel “just in case” will in the best situation lead to suboptimal performance and/or size, and in the worst case, it’ll provide unnecessary attack surfaces. However, optimizing the configuration manually for size, speed, or safety is a massive undertaking. According to Linux from Scratch, there are almost 12,000 configuration switches in the kernel, so going through all of them isn’t really an option.

Fortunately, there are automatic kernel configuration checkers that can help guide the way. Alexander Popov’s Kernel Hardening Checker is one such tool, focusing on the safety of the kernel. It combines a few different security recommendations into one checker. The project’s README contains the list of recommendations it uses as the guideline for a safe configuration. The README also contains plenty of other useful information, like how to run the checker. Who would have guessed! For the sake of example, let’s go through the usage here as well.

Obtaining and Analyzing Kernel Hardening Information

The kernel-hardening-checker doesn’t actually only check the kernel configuration that defines the build time hardening, but it also checks the command line and sysctl parameters for boot-time and runtime hardening as well. Here’s how you can obtain the info for each of the checks:

  • Kernel configuration: in Yocto, you can usually find this from ${STAGING_KERNEL_BUILDDIR}/.config, e.g. <build>/tmp/work-shared/<machine>/kernel-build-artifacts/.config
  • Command line parameters: run cat /proc/cmdline on the system to print the command line parameters
  • Sysctl parameters: run sysctl -a on the system to print the sysctl information

Once you’ve collected all the information you want to check, you can install and run the tool in a Python virtual environment like this:

python3 -m venv venv
source venv/bin/activate
pip install git+https://github.com/a13xp0p0v/kernel-hardening-checker
kernel-hardening-checker -c <path-to-config> -l <path-to-cmdline> -s <path-to-sysctl>

Note that you don’t have to perform all the checks if you don’t want to. The command will print out the report, most likely recommending plenty of fixes. Green text is better than red text. Note that not all of the recommendations necessarily apply to your system. However, at least disabling the unused features is usually a good idea because it reduces the attack surface and (possibly) optimizes the kernel.

To generate the config fragment that contains the recommended configuration, you can use the -g flag without the input files. As the README states, the configuration flags may have performance and/or size impacts on the kernel. This is listed as recommended reading about the performance impact.

GCC Hardening

Whether you like it or not, GCC is the default compiler in Yocto builds. Well, there exists meta-clang for building with clang, and as far as I know, the support is already in quite good shape, but that’s beside the point. Yocto has had hardening flags for GCC compilation for quite some time. To check these flags, you can run the following command:

bitbake <image-name> -e | grep ^SECURITY_CFLAGS=

How the security flags get applied to the actual build flags may vary between Yocto versions. In Kirkstone, SECURITY_CFLAGS gets added to TARGET_CC_ARCH variable, which gets set to HOST_CC_ARCH, which finally gets added to CC command. HOST_CC_ARCH gets also added to CXX and CPP commands, so SECURITY_CFLAGS apply also to C++ programs. bitbake -e is your friend when trying to figure out what gets set and where.

I don’t think any other meme can capture this feeling of madness

So, in addition to checking the SECURITY_CFLAGS, you most likely want to check the CC variable as well to see that the flags actually get added to the command that gets run:

# Note that the CC variable gets exported so grep is slightly different
bitbake <image-name> -e |grep "^export CC="

The flags are defined in security_flags.inc file in Poky (link goes to Kirkstone version of the file). It also shows how to make package-specific exceptions with pn-<package-name> override. The PIE (position-independent executables) flags are perhaps worth mentioning as they’re a bit special. The compiler is built to create position-independent executables by default (seen in GCCPIE variable), so PIE flags are empty and not part of the SECURITY_CFLAGS. Only if PIE flags are not wanted, they are explicitly disabled.

Extra Flags for GCC

Are the flags defined in security_flags.inc any good? Yes, they are, but they can also be expanded a bit. GCC will most likely get in early 2024 new -fhardened flag that sets some options not present in Yocto’s security flags:

-D_FORTIFY_SOURCE=3 (or =2 for older glibcs) 
-D_GLIBCXX_ASSERTIONS 
-ftrivial-auto-var-init=pattern 
-fPIE -pie -Wl,-z,relro,-z,now
-fstack-protector-strong
-fstack-clash-protection
-fcf-protection=full (x86 GNU/Linux only)

Lines 2, 3, and 6 are not present in the Yocto flags. Those could be added using a SECURITY_CFLAGS:append in a suitable place if so desired. I had some trouble with the trivial-auto-var-init flag though, seems like it is introduced in GCC version 12.1 while Yocto Kirkstone is still using 11 series. Most of the aforementioned flags are explained quite well in this Red Hat article. Considering there’s plenty of overlap with the SECURITY_CFLAGS and -fhardened, it may be that in future versions of Poky the security flags will contain just -fhardened (assuming that the flag actually gets implemented).

All in all, assuming you have a fairly modern version of Yocto, this GCC hardening chapter consists mostly of just checking that you have the SECURITY_CFLAGS present in CC variable and adding a few flags. Note once again that using the hardening flags has its own performance hit, so if you are writing something really time- or resource-critical you need to find a suitable balance with the hardening and optimization levels.

In Closing

While this was quite a short text, and the GCC hardening chapter mostly consisted of two grep commands and silly memes, the Linux kernel hardening work is something that actually takes a long time to complete and verify. At least my simple check for core-image-minimal with systemd enabled resulted in 136 recommendation fails and 110 passes. Fixing it would most likely take quite a bit longer than writing this text. Perhaps not all of the issues need to be fixed but deciding what’s an actual issue and what isn’t takes its own time as well. So good luck with that, and until next time!

As a reminder, if you liked this text and/or found it useful, you can find the whole Yocto hardening series from here.


The Movember blog series continues with this text! As usual, after reading this text I ask you to do something good. Good is a bit subjective, so you most likely know what’s something good that you can do. I’m going to eat a hamburger, change ventilation filters, and donate a bit to charity.

Yocto Hardening: Firewalls, Part 2: firewalld

Find all of the Yocto hardening texts from here!

People often ask me two things. The first question is “Why did you choose to write this firewall text in two parts?”. The answer to that is I actually started writing this over a year ago, the scope of the text swelled like crazy, and in the end, to get something published I chose to write the thing in two parts to have a better focus. The second question that I get asked is “Do you often have imaginary discussions with imaginary people in your head?”. To that, the answer is yes.

Comic about imaginary conversations
I prepare for daily stand-up by mentally going through conversations where I get deservedly fired because I didn’t close three tickets the previous day.

But without further ado, let’s continue where we left off in part 1. As promised, let’s take a look at another way of setting up and configuring a firewall: firewalld.

One Step Forward, Two Steps Back: Configuring Kernel (Again)

To get the firewalld running we need a few more kernel configuration items enabled. How many? Well, I spent a few days trying to figure out the minimal possible configuration to run some simple firewalld commands, all without success. firewalld is not too helpful with its error messages, as it basically says “something is missing” without really specifying what that special something is.

Meme about Linux kernel modules
I’ve always liked Lionel’s ‘stash, but I feel like he’s looking extra fine in this pic.

After banging my head on a wall for a few days I attempted to enable every single Netfilter and nftables configuration item (because firewalld uses nftables), but no success. After some further searching, I found this blog post that contained a kernel configuration that had worked for the author of the blog. I combined that with my desperate efforts, and the resulting config actually worked! In the end, my full configuration fragment looked like this:

As you can see, my shotgun approach to configuration wasn’t 100% accurate because there were few NF and NFT configuration items I missed. Is everything here absolutely necessary? Most likely not. There actually are some warnings about unused config options for the Linux kernel version 5.15, but I’m afraid to touch the configuration at this point.

I spent some more time optimizing this and trying to make the “minimal config”, just to realize that I’m actually minimizing something that can’t be really minimized. The “minimal config” really depends on what your actual firewall configuration contains. After this realization, I decided that it’s better to have too much than too little, and found an inner peace. You can think that the fragment provides a starting point from which you can start to remove useless-sounding options if you feel like it.

While we’re still talking about the build side of things, remember to add the kernel-modules to your IMAGE_INSTALL as instructed in part 1. Or if you chose to go through the RRECOMMENDS route, remember to add all 36 modules to the dependencies.

Meme about saving disk space by removing kernel modules
To be fair, I’ve been in a situation where a few KBs of root file system content have made the difference between a success and a catastrophic failure.

The Hard Firewall – firewalld

Now that I’ve spent three chapters basically apologizing for my Linux config, it’s time to get to the actual firewall stuff. The “hard” way of setting up a firewall consists of setting up firewalld. This is in my opinion suitable if you have multiple interfaces with changing configurations, possibly edited by a human user, meaning that things change (and usually in a more complex direction).

firewalld is a firewall management tool designed for Linux distributions, providing a dynamic interface for network security (another sentence from ChatGPT, thank you AI overlords). Or to put it into more understandable words, it’s a front-end for nftables providing a D-Bus interface for configuring the firewall. The nice thing about firewalld is that it operates in zones & services, allowing different network interfaces to operate in different network zones with different available services, all of which can be edited run-time using D-Bus, creating mind-boggling, evolving, and Lovecraftian configurations. This is the nice thing, so you can imagine what the not-so-nice things are.

Now that we are somewhat aware of what we’re getting into, we can add firewalld to IMAGE_INSTALL:

IMAGE_INSTALL:append = " firewalld"

One thing worth noting is that firewalld is not part of the Poky repository, but it comes as a part of meta-openembedded. Most likely this is not a problem, because every project I’ve worked on has meta-openembedded, but it’s worth knowing nevertheless.

Now that everything is in place we can start hammering the firewalld to the desired shape. To edit the configuration we can use the firewall-cmd shipped with firewalld package. This firewall-cmd (among some other firewall-* tools) uses the D-Bus interface to command the firewalld, which in turn commands nftables, which finally sets the Netfilter in the kernel. This picture illustrates it all nicely:

Graph showing firewalld internal structure
The picture originates from here:
https://firewalld.org/2018/07/nftables-backend

So, about the actual configuration process: each network interface can be assigned a zone, and the zones can enable a certain set of services. Here’s a short command reference for how to add a custom zone, add a custom service, add the new service to the new zone, and set the new zone to a network interface:

# List available zones
firewall-cmd --get-zones
# These zones are also listed in /usr/lib/firewalld/zones/
# and /etc/firewalld/zones/

# Get the default zone with the following command
# (This is usually public zone)
firewall-cmd --get-default-zone

# This'll return "no zone" if nothing is set,
# meaning that the default zone will be used
firewall-cmd --get-zone-of-interface=eth0

# Create a custom zone blocking everything
firewall-cmd --permanent --new-zone=test_zone
firewall-cmd --permanent --zone=test_zone --set-target=DROP
# If permanent option is not used, changes will be lost on reload.
# This applies to pretty much all of the commands

# Reload the firewalld to make the new zone visible
firewall-cmd --reload

#List services, and add a custom service
firewall-cmd --list-services
firewall-cmd --permanent --new-service=my-custom-service
firewall-cmd --permanent --service=my-custom-service --add-port=22/tcp
firewall-cmd --reload

# Add the service to the zone
firewall-cmd --permanent --zone=test_zone --add-service=my-custom-service

# Set zone
firewall-cmd --zone=home --change-interface=eth0
# Add --permanent flag to make the change, well, permanent

You can (and should) check between the commands how the ports appear to the outside world using nmap to get a bit better grasp on how the different commands actually work. The changes are not always as immediate and intuitive as one would hope. Good rule of thumb is that if the settings survive --reload, they’re set up properly.

How does this all work with the file-based configuration and Yocto? You could create the zone and service files for your system using the firewall-cmd, install them during build time, and change the default zone in the configuration file /etc/firewalld/firewalld.conf to have the rules active by default. This is similar to nftables approach, but is a bit easier to customize for example from a graphical user interface or scripts, and doesn’t require custom start-up scripts. An example of adding some zones and services, and a patch setting the default zone, can be found in the meta-firewall-examples repo created for this blog text.

Comparison and Closing Words

So, now we have two options for firewalling. Which one should be used? Well, I’d pick nftables, use it as long as possible, and move to firewalld if it would be required later on. This is suitable when the target is quite simple and lightweight, at least at the beginning of the lifespan of a device. On the other hand, if it’s already known from the get-go that the device is going to be a full-fledged configurable router, it’d be better to pick the firewalld right off the bat.

Meme about differences of nftables and firewalld

What about iptables and ufw then? Both have bitbake recipes and can definitely be installed, and I suppose they even work. iptables is a bit of a legacy software these days, so maybe avoid that one though. It should be possible to use iptables syntax with nftables backend if that’s what you want. ufw in Yocto uses iptables by default, but it should be possible to use nftables as the backend for ufw. So as usual with the open-source projects, everything is possible with a bit of effort. And as you can guess from the amount of weasel words in this chapter, I don’t really know these two that well, I just took a quick glance at their recipes.

Note that the strength of the firewall depends also on how protected the rulesets are. If the rules are in a plaintext file that everyone can write to, it’s safe to assume that everyone will do so at some point. So give a thought to who has the access and ability to edit the rules. Also, it may be worthwhile to consider some tampering detection for the rule files, but something like that is worth a text of its own.

In closing, I hope this helped you to set up your firewall. There are plenty of ways to set it up, this two-parter provides two options. It’s important to check with external tooling that the firewall actually works as it should and there are no unwanted surprises. Depending on the type of device you want to configure you can choose the simple way or the configurable way. I’d recommend sticking to the easy solutions if they’re possible. And finally, these are just my suggestions. I don’t claim to know anything, except that I know nothing, so you should check your system’s overall security with someone who’s an actual pro.

Yocto Hardening: Firewalls, Part 1: nftables

Find all of the Yocto hardening texts from here!

The eternal task of making the Yocto Linux build an impenetrable fortress continues. Next, we’ll look into setting up a firewall two different ways: the easy way with nftables, and in the part two the easily configurable way with firewalld. I must warn you beforehand that this text contains a bit of resentment towards kernel configuration. I also used ChatGPT as a tool for writing a part of this text, but I’ll inform you of the section I asked it to write.

So yeah, firewall. The system keeping the barbarians out of our Linux empire. Also, one of the most bad-ass names for a program, at least if taken literally. Reality is a bit less exciting, turns out “firewall” is some technical construction term. In the magical world of computers, a firewall is a piece of software used to allow or deny network traffic coming in and out of a machine. It’s mighty useful for improving the overall security of the system to be able to prevent unwanted remote connections and such.

Step 0 – Configuring Kernel

The first step of installing and configuring a firewall on a Yocto image is ensuring that the kernel is configured correctly. What, do you think you can just start configuring the firewall and blocking your nefarious enemies just like that? Pfft, get out with that casual attitude (please don’t).

To get some basic traffic filtering with nftables done, we need to ensure that the Netfilter is enabled in the kernel, as well as the Netfilter support for nftables, and that some nftables modules are installed as well. It’s all a bit confusing, but to summarize: Netfilter is the kernel framework for packet filtering and nftables is the front-end for Netfilter that can be used from user space. nftables can then be built with or without modules to extend its functionality. The following kernel configuration fragment should do the trick:

Getting this config fragment working was where things got problematic for me. I thought that I’d just slap =y to everything and be done with it, no need to install and load modules. Well, the configuration process of the kernel consists of merging the defconfig, kernel features, and configuration fragments, and as it turns out, the kernel feature enabling the Netfilter overrode some parts of my config fragment, changing =y to =m. For some reason bitbake didn’t give a warning about this. I’m quite sure it used to do.

But where is this mythical Netfilter kernel feature located? The fragment above enables only nftables related things, how to get the Netfilter working? Well, Kirkstone release (3.1.27) of Poky has this line that enables the feature by default in the kernel, and the related configuration fragment looks like this. The multiple configs lead to funny situations where you try to remember the difference between CONFIG_NF_CONNTRACK and CONFIG_NFT_CT and where they are defined and to what value.

Ensure that you’re also installing the kernel modules to the image by adding the following line somewhere in the image configuration:

IMAGE_INSTALL:append = " kernel-modules"

No use enabling the modules if they’re not installed, and by default nothing installs them, so they won’t get added to the final image. At least to the qemu64 core-image-minimal image they won’t be installed by default, guess if I found that out the hard way as well. If you need more fine-grained control over the modules that get installed, you can add them as a runtime recommendation to some package:

RRECOMMENDS:${PN} += "kernel-module-nf-tables kernel-module-nf-conntrack kernel-module-nft-ct ...

Most likely nftables package is the best place for this. However, I’d advise against this approach if possible, because there will be a lot of modules later on. If you choose to go down this route, the naming format of the modules is fortunately quite self-explanatory. CONFIG_NF_TABLES being built as a module generates kernel-module-nf-tables package, CONFIG_NFT_CT results in kernel-module-nft-ct being built etc, so this should be easily scriptable (although painful to maintain).

The Easy Firewall – nftables

The easy way of getting some firewalling done involves installing nftables, adding some rules to it, and loading them as a part of the start-up. This is suitable if you have a few interfaces with a simple configuration that doesn’t change often, preferably ever.

The actual first step of getting the firewall up and running is adding the nftables package to the image if you don’t have it already installed. This can be easily checked by running nft command. If it fails, add the package to the image configuration:

IMAGE_INSTALL:append = " nftables"

If you see the following types of errors when running the nft command it means that your kernel configuration is missing either nftables support (the first error) or some nftables module (the second error):

root@qemux86-64:~# nft
../../nftables-1.0.2/src/mnl.c:60: Unable to initialize Netlink socket: Protocol not supported
root@qemux86-64:~# 
root@qemux86-64:~# nft -f /tmp/test.conf
/tmp/test2:32:9-16: Error: Could not process rule: No such file or directory
        ct state vmap { established : accept, related : accept, invalid : drop } 
        ^^^^^^^^
root@qemux86-64:~# 

Quite often the firewall in Yocto is empty by default. The active rules of the firewall can be checked by running nft list ruleset command to print the current ruleset. If nothing is output, no rules are present. If something gets output, there are some rules present. Here’s a short reference of commands on how to set some basic rules and list them on the command line.

# List ruleset
nft list ruleset

# Add a table named filter for IP traffic 
nft add table inet filter

# Add an input chain that drops traffic by default
nft add chain inet filter input \{ type filter hook input priority 0 \; policy drop \}

# Add rule to allow traffic to port 22
nft add rule inet filter input tcp dport \{ 22 \} accept

# List tables and rules in a table
nft list tables
nft list table inet firewall

In addition to listing the tables and rulesets from inside the device, you should also try a black-box approach. Running nmap command against your device is a good way to check how the outside world sees your device. In this example, I have added the Dropbear SSH server to the image. Because the server is listening on port 22 and there are no rules defined in the firewall, the port is seen as open:

esa@ubuntu:~$ nmap 192.168.7.2
Starting Nmap 7.80 ( https://nmap.org ) at 2023-07-22 16:14 UTC
Nmap scan report for 192.168.7.2
Host is up (0.0024s latency).
Not shown: 999 closed ports
PORT   STATE SERVICE
22/tcp open  ssh
Nmap done: 1 IP address (1 host up) scanned in 0.06 seconds

The command line commands are useful for editing the configuration run-time, but for the rest of the text we’re going to focus on a file-based configuration that is set during the Yocto build. The configurations presented here are mostly based on “a simple ruleset for a server” in nftables wiki. It contains some useful extra things not presented here, like a rule for the loopback interface, differentiating between ipv4 and ipv6 traffic and logging, and I recommend checking it out.

Usually, a good way to start creating a firewall ruleset is to block everything by default, and then start poking holes as needed. To block everything, you can write a configuration file with the following content:

flush ruleset

table inet firewall {
    chain inbound {
        type filter hook input priority 0; policy drop;
    }

    chain forward {
        type filter hook forward priority 0; policy drop;
    }

    chain output {
        type filter hook output priority 0; policy drop;
    }
}

This config creates one rule table named firewall for inet traffic. The table contains three chains: inbound, forward and output. You can load the ruleset with nft -f <config_file> command. Once you’ve blocked all traffic, you can give the nmap command another go. As you can see, blocking is really effective. nmap actually considers the device to be non-existent at this point:

esa@ubuntu:~$ nmap 192.168.7.2
Starting Nmap 7.80 ( https://nmap.org ) at 2023-07-22 16:18 UTC
Note: Host seems down. If it is really up, but blocking our ping probes, try -Pn
Nmap done: 1 IP address (0 hosts up) scanned in 3.03 seconds

Trying to ping from the device itself to the outside world also fails because the output is blocked. In theory, allowing outgoing traffic is not the worst idea, usually many sample configurations actually allow it. But if you know the traffic that’s going to go out and the ports that will be used there’s in my opinion no sense allowing all of the unnecessary traffic. That’s just an attitude that will lead to botnets. But if you want/need to allow all outgoing traffic, you can just drop the output chain from the config. By default everything will be accepted.

After nothing goes in or out, it’s time to start allowing some traffic. For example, if you want to allow SSH traffic from the barbarians to port 22 (generally a Bad Idea, but for the sake of an example), you can use the following ruleset:

flush ruleset

table inet firewall {
    chain inbound {
        type filter hook input priority 0; policy drop;

        # Allow SSH on port TCP/22 for IPv4 and IPv6.
        tcp dport { 22 } accept
    }

    chain forward {
        type filter hook forward priority 0; policy drop;
    }

    chain output {
        type filter hook output priority 0; policy drop;

        # Allow established and related traffic
        ct state vmap { established : accept, related : accept, invalid : drop } 
    }
}

If you want to define multiple allowed destination ports, separate them using commas, like { 22, 80, 443 }. If you choose not to block outgoing connections, you obviously don’t need the output chain presented here.

On the other hand, if there’s a process in the Yocto image that wants to contact the outside world, you could consider adding a ruleset like below:

flush ruleset

table inet firewall {
    chain inbound {
        type filter hook input priority 0; policy drop;

        # Allow established and related traffic
        ct state vmap { established : accept, related : accept, invalid : drop } 
    }

    chain forward {
        type filter hook forward priority 0; policy drop;
    }

    chain output {
        type filter hook output priority 0; policy drop;

        # You can check the ephemeral port range of kernel 
        # from /proc/sys/net/ipv4/ip_local_port_range
        tcp sport 32768-60999 accept
    }
}

Note how we need to use a port range for the output. Outgoing connections usually use a port from an ephemeral range for their connection, meaning that you may not know beforehand the exact port that needs to be allowed. Having fixed ports for outgoing traffic makes the device more susceptible to port scanning and prevents multiple connections from the same service because the port is already used, but if you choose to create a service that uses a static source port, you can use a single port number in the rule.

You can combine the rules in the chains as required by your system and services. If you want to enable the ping command (for ipv4), you can add the following piece of configuration to the input chain (note that this is just a single line of configuration, not a full config):

icmp type echo-request limit rate 5/second accept

In the end, creating the firewall ruleset is fairly simple: block everything and allow only necessary things. The difficulty really comes from keeping the rules up-to-date when something changes. However, when developing an embedded device with Yocto you generally should have a well-defined list of allowed ports & traffic (as opposed to a general-purpose computer used by a living human where all sorts of traffic and configs may come and go at the user’s whim). If that’s not the case, the part 2 where we work with firewalld may be useful to you.

So, now we know how to write the perfect configuration file. But how to add this to a Yocto build? You can create a bbappend file for nftables into your own meta-layer. This append then installs the configuration file, because nftables does not install any configuration by default. An example of how to add a configuration to nftables can be found in meta-firewall-examples repository I made for this blog post.

You should also note that nftables package doesn’t contain any mechanism to activate the rules during the start-up. Therefore you should append the nftables recipe so that it adds an init.d script or a service file that activates the rules from the configuration file before the networking is started. You can find an example of this as well from the same meta-firewall-examples repository.

Note that if you do run-time edits to the configuration with nft command, the changes will be lost when the device is reset. To ensure that the changes are kept, you can run the following command to overwrite the default config with the new, enhanced configuration:

nft list ruleset > /path/to/your/configuration-file

And one more thing: if you face some No such file or directory issues when trying more exotic configurations you are most likely missing some kernel configurations. In part 2, we’ll be enabling pretty much every feature of the Netfilter, so that’ll (most likely) be helpful. On that cliffhanger, we eagerly await the next chapter in this ever-evolving journey of discovery and innovation (I asked Chat-GPT to write that sentence).

Yocto Hardening: Finding & Fixing CVEs

Find all of the Yocto hardening texts from here!

Last time when we were talking about Yocto hardening we focused on setting up extra users to prevent unauthorized root access and to avoid situations where the users would have too open permission sets. Getting these right is a good low-hanging fruit, but there are plenty of other things to consider for hardening the system. One of the most important things to do is to eliminate (or at least minimize) the security vulnerabilities weakening the overall system defenses.

What are CVEs?

Despite what one may be inclined to think, open-source code is not always perfect. It may contain bugs, which is a bit annoying, but it may also contain vulnerabilities, which is potentially a bit dangerous. These can lead to denial of service attacks, data leaks, or even unauthorized accesses & system takeovers. Not good. Very bad. The usual stuff.

Panic Omg GIF - Find & Share on GIPHY

CVE (Common Vulnerabilities and Exposures) is a system maintained by The United States National Cybersecurity FFRDC and operated by Mitre Corporation. The system contains information about, well, vulnerabilities and exposures. “CVE” as a noun can also mean vulnerability or exposure. Or at least I often use the acronym in with that meaning. Checking your Yocto system against this database of vulnerabilities can help to detect security issues lurking in the open-source code components.

Checking vulnerabilities in Yocto

Every CVE doesn’t necessarily lead to unauthorised root access, and there may be vulnerabilities that don’t have a CVE identifier. However, scanning through the CVEs in your system should provide a fairly good image of how vulnerable your system is against possible CVE exploits. At least at the time of the scan, there are constantly new issues popping up which means you have to keep doing it often. Also it’s good to keep in mind that this kind of a scan does not check for other kind of weaknesses in the system. But at least performing the CVE check in the Yocto system is fairly simple. Something is simple in Yocto, what?

Mood What GIF by NBC - Find & Share on GIPHY
My whole worldview is being shattered.

In all simplicity, just add this line to local.conf to check your packages & images for common vulnerabilities.

INHERIT += "cve-check"
# I had some issues with slow download speeds. If you get weird timeouts
# when fetching issue database, try adding this line as well:
CVE_SOCKET_TIMEOUT = "180"

Now when you run the build you’ll most likely end up with more warnings than before. Yocto’s CVE checker prints a warning to the build output for every vulnerability it detects, and depending on the complexity and age of your system your terminal may end up quite yellow. It’s more useful to inspect the CVE reports than to try and remember the build output as it scrolls by. These reports can be found in <build-folder>/tmp/deploy/cve folder, and we’ll inspect them a bit later. It’s worth knowing that this CVE checker may not be perfect, meaning that there may be some vulnerabilities lurking around that it does not detect. But it’s at least better than going through all the source code eyeballing it.

Analysing the results

Ok, now you’ve proved to yourself that your system has holes that need to be plugged. How to get over this uneasy feeling of constant doom looming over your system? The first thing is listing out all the CVE IDs and checking their status & severity to get a bit more stressed. Or relaxed, depends a bit on how well your system is maintained.

Sponge Bob Reaction GIF - Find & Share on GIPHY
Let’s be real, this is most likely going to be the most accurate reaction.

Further information for the CVEs can be found on MITRE’s CVE site, and some more complementary information can be found on NIST’s (National Institute of Standards and Technology) NVD (National Vulnerability Database). Plenty of acronyms. NVD is a system containing the same CVE IDs as MITRE’s site, but with severity scores and usually some extra information. Both are definitely useful, but for practical purposes NVD tends to be more useful. MITRE’s site can contain some new info not yet present in NVD, but NVD contains the information in a more easily digestible format.

Let’s take CVE-2023-22451 as an example. CVE listing for the vulnerability shows a short summary, affected versions, and a bunch of links. NVD entry has the same information, severity score, modification date, and a bit more information about the links so you won’t have to guess which link is the patch and which is some advisory (this can be useful on bigger issues where there are a dozen of links). For what it’s worth, Yocto’s CVE checker uses NVD’s database.

Now that we’ve gone through some background information, we can investigate the results of the CVE check. The aforementioned <build-folder>/tmp/deploy/cve contains two summary files: cve-summary and cve-summary.json. These contain all the detected issues, both patched and unpatched, and the format of the files isn’t all that intuitive for getting an actual summary, so I wrote a simple Python-tool for summarising the results. The design is very human:

# Replace the file path with your path
python3 ./cve-parser.py -f build/tmp/log/cve/cve-summary.json

Very easy to use. By default the parser will output the unpatched CVEs, and from each CVE it will print the package name, CVE ID, CVSS scores, and the link to the NVD site for more info. There is more information per issue available, the results can be sorted, and the ignored & patched issues can also be printed if needed. To get the complete list of options, run:

python3 ./cve-parser.py -h

I’m not going to document the whole program here in case I decide to change it at some point. If your boss asks you to get the total amount of unpatched CVEs in the system, you can run the command below to get “extra info” from the parser. Currently, “extra info” just contains the number of displayed issues, but if you’ve got ideas for it feel free to let me know:

# -u display unpatched issues (default behavior, but added just in case)
# -e displays extra info
python3 ./cve-parser.py -u -e -f build/tmp/log/cve/cve-summary.json

If your boss asks how many of them actually need to be fixed, use this to sort the CVEs by severity score and count how many have a score higher than 8.3

# -u display unpatched issues
# -ss2 sort by CVSSv2 score
python3 ./cve-parser.py -u -ss2 -f build/tmp/log/cve/cve-summary.json

Just kidding, please fix them all.

Fry Reaction GIF - Find & Share on GIPHY

Fixing the CVEs

After you’ve justified your fears of getting hacked by realizing that your system has seven CVEs with a CVSS score higher than 9.5 (three of them in the kernel) and a few dozen more with just a slightly lower priority, it’s time to ask the big question: what can I do? What can I possibly do? This didn’t help with the feeling of constant doom at all.

The first step is going through the list of unpatched issues. There tend to be quite a few false positives that are not applicable to the current system. For example, a brand new Poky build reports an unpatched CVE-2017-6264 that’s been around six years already. This is a vulnerability in the NVIDIA GPU driver, and it’s applicable to Android products. Most certainly it’s a false positive in your Yocto system, but since the bad code is present in the source code, it’s reported as unpatched. You can ignore these false positives by adding the line below somewhere to your build configuration (local.conf works, but it’s maybe not the best place for it):

# The format is CVE_CHECK_IGNORE="<cve-id>", e.g.
CVE_CHECK_IGNORE="CVE-2017-6264"
# In the older Yocto versions this was called CVE_CHECK_WHITELIST
# so if ignore doesn't work, try the old whitelist variable

After ignoring the false positives fixing the rest of the CVE issues is easy, in theory. Just keep the Yocto at its newest (or relatively new LTS) version. This should keep the packages fresh. Sounds simple, but unfortunately this process tends to cause a big headache with incompatibilities and such. However, that’s the best advice I can give.

If there is a need to update the recipes even further than officially supported, or if you want to update a single recipe, copy the recipe in question to your own meta-layer, update the recipe version in the filename and fix SRCREV to match. When hacking around like this remember to hope that nothing falls apart in the system even more than during a regular Yocto version update.

Sometimes, updating the packages is not an option. Perhaps updating the package breaks the build, a newer version of the library isn’t compatible with your application, or there is some other Perfectly Good Reason you’re not allowed to do that. That’s when it’s time to do some patching to make the confusing build system even more spaghettified.

Noodles Spaghetti GIF - Find & Share on GIPHY

Basically, this approach consists of heading to the NVD entry for the CVE, checking if there’s a patch for the vulnerability, and if there is, porting the patch to the Yocto build. For example, the aforementioned CVE-2023-22451 mentions this commit as its patch in the NVD entry. Copy the contents of the commit to a patch file, create bbappend file for the recipe and add the patch to the recipe with that bbappend.

If there’s no patch you can wait. Or if you’re like a kid on Christmas and can’t wait, you can try digging the package’s git repositories for the corrective commit before it’s been released. It ain’t fun, and rarely it’s a productive use of time, but every now and then some fixes can be found like this before they are made official.

That’s a short explanation of how you can check and fix the CVE vulnerabilities in your system. The theory of the process is fairly simple. However, it tends to get a bit more complicated in the actual world, especially when trying to update older legacy systems where the stale, non-updated packages contain more patches than original code. But I guess that’s the problem with real life, it tends to mess up good theories.

You can find the next part about Yocto hardening here. It’s about firewalls.

How to build Yocto with Apple Silicon

If you’re like me, you have more money than brains, and not too much of either really. This can lead to situations where you end up buying a MacBook without actually checking if it supports Yocto builds, the one thing you inexplicably like to waste your little free time on. As it turns out after getting the nearly 2K€ laptop, builds on Mac are not supported. But don’t worry, where there is a problem, there usually is a convoluted solution.

Rube Goldberg Win GIF by Millions - Find & Share on GIPHY

As one might expect, the answer is virtualization. Specifically Docker virtualization this time. Writing this kind of instructional text is becoming my second nature, earlier I wrote how to build Yocto on Windows/WSL, and now how to build the damn thing on Mac. One day I’ll get an actual Linux computer suitable for this kind of development work, but not today. I’m out of money at the moment. Anyways, to the actual guide. I’ll warn you beforehand that my knowledge of Docker is quite superficial, but I’ll try to explain things as well as I can.

Installing Docker

When doing Docker builds the first step obviously is to install Docker. Get it from here and come back when you’re done.

Gonna Go Homer Simpson GIF - Find & Share on GIPHY
Don’t worry, they definitely are.

Getting correct images

Now, the actual tricky part: getting the Docker images suitable for building Yocto. Docker images are used to create the containers that are the virtualized environment we can do the builds in. Yocto Development Tasks Manual mentions CROPS (CROss PlatformS) Docker images that can be used for building in non-Linux hosts. CROPS itself also has this nice Mac guide that almost works.

Well, I guess the manual works for the older amd64 MacBooks because the pre-built Docker images are only available in amd64 format. Since Apple Silicon is arm64, Docker gives a nice warning that the emulated image may or may not work, and spoiler alert, it does not. Attempting to run bitbake after following the guide gives the following error:

OSError: Cannot initialize new instance of inotify, Errno=Function not implemented (ENOSYS)

However, I recommend keeping that CROPS guide nearby because we are going to follow it once we get the suitable images built. The first step of the journey is pulling the Docker image source repositories for the two images mentioned in the CROPS Mac guide:

% git clone https://github.com/crops/samba.git
% git clone https://github.com/crops/poky-container.git

The first one is responsible for creating a samba file server to provide the contents of the build container to the Mac file system via a Docker volume, and the second one is the actual builder image. Building the samba image is simple enough:

% cd samba
% docker build . -t crops/samba:selfbuilt
% cd ..

The poky-container image is a bit trickier. It attempts to fetch an amd64 operating system image provided by CROPS as its base layer. Instead of using that image, we need to find something else that’s more suitable for us and arm64.

Season 3 Episode 20 GIF by SpongeBob SquarePants - Find & Share on GIPHY
Not much.

Docker Hub is a site containing Docker images for a variety of uses. Among many others, it also contains the CROPS amd64 images that we are not able to use. By searching for “Yocto”, and limiting the results to arm64 architecture we can find quite a few results. However, none of these (at least at the time of writing) fit into the CROPS workflow or are up to date, meaning that they won’t work with the newer versions of Yocto. Therefore we need to build the operating system base image by ourselves.

The Dockerfiles used to build the images are open source, but unfortunately, the scripts in them use GNU versions of some commands, meaning that the scripts won’t work with the BSD commands Mac has. But fear not, I made a fork for building the base images on Mac! It works at least for the Ubuntu 22.04 image, I was a bit lazy and didn’t check the dozen other options. To build the Ubuntu base image, run the following commands:

git clone git@github.com:ejaaskel/yocto-dockerfiles.git
cd yocto-dockerfiles
export REPO=ejaaskel/yocto
export DISTRO_TO_BUILD=ubuntu-22.04
./build_container
cd ..

This will create two images: ejaaskel/yocto:ubuntu-22.04-base and ejaaskel/yocto:ubuntu-22.04-builder. Now we just need to go to the CROPS poky-container Dockerfile and replace their base image with the one we just built. After that, we should be able to build the poky-container as an arm64 image and get to actually building Yocto.

% cd poky-container
## Open your favourite text editor (it better be nvim),
## and replace these lines in Dockerfile:
ARG BASE_DISTRO=SPECIFY_ME
FROM crops/yocto:$BASE_DISTRO-base
## With these lines to specify the distro version and 
## use the Docker image we built:
ARG BASE_DISTRO=ubuntu-22.04
FROM ejaaskel/yocto:$BASE_DISTRO-base
% docker build . -t crops/poky:selfbuilt
% cd ..

Now we’re again on track to follow that CROPS guide. We just have to suffix every mention of the CROPS images with :selfbuilt so that we’ll use the built images and not pull images from Docker Hub. So whenever there’s a mention of crops/samba in the guide, write crops/samba:selfbuilt, and when there’s a mention of crops/poky, use crops/poky:selfbuilt instead.

Building Yocto

If you followed the instructions in the CROPS guide you should end up with an Ubuntu terminal in /workdir and a Finder window that’s connected to the same location in the Docker container. After that, it’s just a matter of following Yocto’s Quick Build manual or building something else that’s actually interesting. Once the build finishes, you can access the files in Finder as you normally do, or if you prefer to use the command line, docker cp should do the trick as well:

# Use docker ps to get crops/poky:selfbuilt container id
% docker ps
% docker cp <container-id>:workdir/<whatever-you-want-to-copy> .

By the way, if you stick to docker cp for moving stuff around, you don’t actually need to start the samba container. Saves a bit of effort.

Fight Club Work GIF - Find & Share on GIPHY
of a copy

If you happen to need a root shell in the container for some reason, like installing packages, you can open it like this:

# Use docker ps to get crops/poky:selfbuilt container id
% docker ps
% docker exec -it --user=root <container-id> bash

If (and when) you run out of disk space, you can increase the virtual disk size from the Docker Desktop program. Select the cog from the upper right corner to enter settings, navigate to Resources->Advanced and from there you can increase the virtual disk limit. I’m quite sure there is a way to do this in the command line as well, but hopefully this isn’t a task that I’ll have to do so often that I’d need to figure it out.

Can you see how artistic those red circles are? All thanks to the new MacBook.

That’s all for this time! Hopefully, this helped you to get started with Yocto builds on Apple Silicon a bit quicker. Fortunately, this wasn’t really anything groundbreaking, mostly it was just a matter of combining a few different information sources and a bit of script editing. The biggest sidestep from the CROPS instructions was building the Docker images instead of using the ready-built ones. But for someone who’s new to Mac & Docker, it may take a surprisingly long time to figure this out. At least it did for me. Writing this also helped a bit with my buyer’s remorse. Until next time!

Yocto hardening: Non-root users, sudo configuration & disabling root

Find all of the Yocto hardening texts from here!

Cybersecurity. The never-ending race between you trying to secure your precious IoT device and some propeller head who’s finding the wildest exploits and destroying your system just in the time between breakfast and lunch (although true hackers work during night, not in the mornings). Or perhaps you forgot to update your WordPress plugins, and now your fantastic development blog is hacked. Or perhaps you’ve just been postponing that security update on your Android phone for six months. We all have some experience with cybersecurity.

Usually, there are some simple things to do to prevent the worst catastrophes. Update software, make sure the run-time environment is sane & secure, and encrypt the secrets to mention a few. In this new blog series, I try to explain how to make your Yocto systems more secure by adding some common-sense security to the system. All of these ideas are familiar from Linux desktop & server environments, but I’m going to show how to apply them to Yocto builds. So, unfortunately, I’m not actually going to cover how to keep a fantastic WordPress site secure.

Well okay, no need to be upset. Just press the red “update plugins” button every now and then. And if you’re self hosting, I wish luck to your distro upgrades.

As usual with any security advice, it’s good to be slightly skeptical when reading it. This means that some rando writing blog posts on their blog may not be a 100% trustworthy or up-to-date source on everything. Also, those randos will take no responsibility for the results of their advice. However, their advice may provide useful guidance and general tips that you can apply to your system’s security hardening plan. You do have a security plan, right?

So, the first topic of this Yocto hardening exercise is users. As you may have heard, running and doing everything as the root user is generally considered a Bad Idea. Having services running under the root user unnecessarily can result in root user executing arbitrary code using the most imaginative methods. Or, if the only user available for logging in is the root user you’re not only giving away root permissions to malicious users, but to incompetent users as well.

This text will assume intermediate knowledge of Yocto, meaning that I won’t explain every step in depth along the way. Hopefully, you know what an append-file and local.conf are. The code and config snippets were originally written for Yocto Kirkstone checked out “at some point after 4.0.3 release”, and later re-tested with version 4.0.15.

Before we get started with the actual hardening, there’s one preliminary task to do. Ensure that you don’t have DEBUG_TWEAKS in either IMAGE_FEATURES or EXTRA_IMAGE_FEATURES. Not only is it unsafe as it allows root login without a password, and a bunch of other debug stuff, but it also makes some examples shown here behave in unexpected ways.

Creating non-root users

Here is a code snippet that creates a service user for the system. This user can be logged in with, and they have sudo capabilities (if sudo-package is installed). Insert this code either into a image recipe or a configuration file:

# If adding to configuration file, use INHERIT += "extrausers" instead.
inherit extrausers

IMAGE_INSTALL:append = " sudo"

# This password is generated with `openssl passwd -6 password`, 
# where -6 stands for SHA-512 hashing alorithgm
# The resulting string is in format $<ALGORITHM_ID>$<SALT>$<PASSWORD_HASH>,
# the dollar signs have been escaped
# This'll allow user to login with the least secure password there is, "password" (without quotes)
PASSWD = "\$6\$vRcGS0O8nEeug1zJ\$YnRLFm/w1y/JtgGOQRTfm57c1.QVSZfbJEHzzLUAFmwcf6N72tDQ7xlsmhEF.3JdVL9iz75DVnmmtxVnNIFvp0"

# This creates a user with name serviceuser and UID 1200. 
# The password is stored in the aforementioned PASSWD variable
# and home-folder is /home/serviceuser, and the login-shell is set as sh.
# Finally, this user is added to the sudo-group.
EXTRA_USERS_PARAMS:append = "\
    useradd -u 1200 -d /home/serviceuser -s /bin/sh -p '${PASSWD}' serviceuser; \
    usermod -a -G sudo serviceuser; \
    "

The extrausers class allows creating additional users to the image. This is done simply by defining the desired commands to create & configure users in the EXTRA_USERS_PARAMS variable. As a side note, it’s good to have static UIDs for the users as this will make the builds more reproducible.

In a perfect world, every custom service you create for your system would run under a non-root user. When writing a recipe that creates a daemon or other kind of service, you can use the snippet below to add a new user in a non-image recipe:

inherit useradd
USERADD_PACKAGES = "${PN}"
GROUPADD_PARAM:${PN} = "--system systemuser"
# This creates a non-root user that cannot be logged in as
USERADD_PARAM:${PN} = "--system -s /sbin/nologin -g systemuser systemuser"

As you can see, adding a new user in a non-image recipe is slightly different than in the image recipe. This time we’re only giving parameters to useradd and groupadd commands. After adding this, you should be able to start your daemon as the systemuser in a startup script. If you need root permissions for some functionality in your service but don’t want the service to be run as root, I’d recommend reading into capabilities. It’s worth noting that when adding users in a service recipe like this, the additions are done per-package basis, not per-recipe basis. This means that you can create and add new users in a quite flexible manner.

The flexibility tends to come with some complications though…

Editing sudoers configuration

By default, when adding the sudo-package to the build it doesn’t do much. It doesn’t actually allow anything special to be done by the users, even if they are in the sudoers group. That’s why we need to edit the sudo-configuration. There are two ways of doing this, either by editing the default sudoers file or by adding drop-in sudoers.d configurations. First, editing the sudoers file (which is the worse method in my opinion).

There are two ways of doing this, and I’m not 100% sure which one is less bad, so I’ll leave it to you to decide. The first option is adding a ROOTFS_POSTPROCESS_COMMAND to the image recipe:

enable_sudo_group() {
    # This magic looking sed will uncomment the following line from sudoers:
    #   %sudo   ALL=(ALL:ALL) ALL
    sed -i 's/^#\s*\(%sudo\s*ALL=(ALL:ALL)\s*ALL\)/\1/'  ${IMAGE_ROOTFS}/etc/sudoers
}

ROOTFS_POSTPROCESS_COMMAND += "enable_sudo_group;"

The second option is creating a bbappend file for the sudo recipe, and adding something like this there:

do_install:append() {
    # Effectively the same magic sed command
    sed -i 's/^#\s*\(%sudo\s*ALL=(ALL:ALL)\s*ALL\)/\1/'  ${D}/${sysconfdir}/sudoers
}

Both do the same thing, and both are hacky. The correct choice really depends on if you want to edit the sudoers file in a different way for each image, or if you want the sudo configuration to depend on something else. You can also of course supply your own sudoers file instead of sed-editing it, and that can be a lot more maintainable way of doing this if you have more than just one change.

However, a more flexible way of configuring sudo is with the sudoers.d drop-in files. It also involves a lot fewer cryptic sed commands. Let’s assume that you have a recipe that creates a new user lsuser (not to be confused with a loser), and you want that user to have sudo rights just for ls-command (considering rights given to this user, they may actually be a loser). For this purpose you need to create a bbappend for sudo, and add something like this snippet to create a drop-in configuration:

do_install:append() {
    echo "lsuser ALL= /bin/ls " > ${D}${sysconfdir}/sudoers.d/lsuser
}

FILES_${PN} += " ${sysconfdir}/sudoers.d/lsuser"

Again, if your drop-in configuration is more complex than one line, you can provide it as a configuration file through SRC_URI and install that in your image instead of echoing configuration lines. I recommend reading sudo-configuration documentation for more info on how to precisely set the permissions because let’s be honest, these two examples aren’t the shining examples of sudo configuration.

The downside of this approach is that you need to add the drop-in configuration in a sudo bbappend because /etc/sudoers.d will conflict when creating the root filesystem if you add the drop-in in another recipe. This means that when you’re creating a user in a non-sudo recipe and add the drop-in conf in the sudo recipe you’ll have the user creation & configuration handled in two different places, which is perfect for forgetting to maintain things.

Disabling root-login

Disabling root login is useful for a multitude of reasons.

  1. You usually want to make sure that no user has the full command set available
  2. You also want that the users’ privileged actions to end up logged in auth.log when they use sudo
  3. In general, it’s “a bit of a risk” to allow users to log in as root

Disabling the root user can be achieved in multiple ways, and I’m going to cover three of them here: fully locking the root account, removing the login shell, and disabling the SSH login. In theory, locking the root account should be enough, assuming non-root users cannot unlock it, but I’ll present all the methods as they may be useful for different situations.

First, disabling the root account. This is a method I found from this StackOverflow answer, kudos there. The simple snippet below pasted in the image recipe should lock and expire the root account:

inherit extrausers
EXTRA_USERS_PARAMS:append = " usermod -L -e 1 root; "

Then, removing the login shell. Or more like setting the login shell to nologin. This isn’t strictly required after locking the root account but can be achieved by creating a bbappend shown below to base-passwd recipe:

do_install:append() {
    # What this magic sed does:
    # "In the line beginning with root
    # replace /bin/sh with /sbin/nologin
    # in file passwd.master"
    # (passwd file gets generated from this template during install)
    sed -i '/^root/ s/\/bin\/sh/\/sbin\/nologin/' ${D}${datadir}/base-passwd/passwd.master
    # sry for the picket fence

    # I think it's peak sed that I need a four line
    # comment to explain a one-liner
}

And finally, disabling the SSH login. You should always disable the root login over remote networks. If you need, you can just disable the SSH login for root and still allow other service users to log in as root. This is still a better option than doing nothing, as this will prevent logging in as root remotely and attackers would need to know two passwords to gain root access (one for the service user, and the other one for the root user).

Disabling the root login varies a bit between the SSH servers. Dropbear disables root login by default with -w parameter in DROPBEAR_EXTRA_ARGS variable located in the /etc/defaults/dropbear file (note that if you have debug-tweaks enabled, the file actually contains -B, allowing root-login). You can overwrite the file with your own dropbear.default file during the build if you want to add more options.

Similarly, OpenSSH-server disables root logins with passwords if debug-tweaks is removed from the IMAGE_FEATURES (and allows them if it’s present). This is achieved by sed-editing SSH configuration files. If you want to see how this is exactly done, check ssh_allow_root_login function in meta/classes/rootfs-postcommands.bbclass (part of poky).

However, it’s worth noting that this default behaviour doesn’t prevent public key authentication. If you want to disable that as well, you can add this function as a rootfs post-process task to the image recipe. And if needed, it could of course be modified to work in a bbappend as well.

# The function here searches sshd_config and sshd_config_readonly files for a
# commented line containing PermitRootLogin, and replaces it with "PermitRootLogin
# no" to prevent any sort of root login.

disable_rootlogin() {
    for config in sshd_config sshd_config_readonly; do
        if [ -e ${IMAGE_ROOTFS}${sysconfdir}/ssh/$config ]; then
            sed -i 's/^[#[:space:]]*PermitRootLogin.*/PermitRootLogin no/' ${IMAGE_ROOTFS}${sysconfdir}/ssh/$config
        fi
    done
}

ROOTFS_POSTPROCESS_COMMAND += "disable_rootlogin;"

The thing to note about disabling root login is that too lenient sudo- or filesystem-permissions for non-root users can make the whole thing useless. For example, if a service user has sudo access to passwd command they can easily unlock the root account. If a service user has write permissions to /etc they can set the root’s login shell and edit SSH configuration. And finally, disabling root login like shown in this text does nothing to prevent shell escapes made possible by incorrectly set sudo-permissions.

Might as well roll out the red carpet

That’s it for this time! Hopefully, these tips helped you to achieve the minimum security in your Yocto images. Do these code snippets make your system unhackable? Not even close. Do they make it slightly more inconvenient? I hope so (but no guarantees for that either). But at least now you can show your Yocto image to an independent professional security consultant and say that you tried before they tear the whole system to pieces.

You can find the second part of the Yocto hardening series here. It’s about fixing CVEs.

Yocto and WSL, part 3: WSL vs. VMWare

Read the previous parts of “Yocto and WSL” series:
Yocto? On WSL2? Easier than you think!
Yocto and WSL, part 2: The Graphic Boogaloo

The ultimate showdown of the ultimate destiny!

All the good things are trilogies. Star Wars original trilogy, Nolan’s Batman trilogy, and The Hobbit trilogy. And now, Yocto & WSL trilogy. As per usual with the trilogies, the last part may be a bit of a letdown for the hardcore fans. This time we’re not doing really anything technical or exciting. Instead, we’re comparing some numbers while trying to decide if this whole exercise was worth it or not.

To judge the usefulness of WSL in the Yocto build context were doing a comparison between WSL machine and a “regular” virtual machine to see which one is actually better for building Yocto (if you’re forced to use Windows). Better means faster in this case. For this purpose, I created a VMWare virtual machine that matched my WSL machine’s specs: 4 cores, 8GB, and enough disk space to accommodate the build area. For the operating system I chose Ubuntu Server 20.04.4, and as a reference build target I used the simplest core-image-minimal recipe with both machines.

But wait, there’s more!

As a bit of an extra, I’ll be adding a comparison to a cloud build server as well for the same price! For this purpose, I prepared a similar 4-core 8GB build server on Hetzner. This is a virtual machine as well, but it’s interesting to have more things to compare to. Especially because the cloud instances provide an option to scale up in case they aren’t powerful enough. Also, like WSL and VMWare, they can be used as an alternative for a Linux build machine. Without further ado, let’s get into crunching numbers.

First, to actually make a worthwhile comparison between different build times, I’m going to separate the process of downloading the sources from the actual build itself. I did three runs of downloads because “third time’s the charm”. VMWare was using a NAT network connection here. The separation was done because the time it takes to download the sources depends on the load on the source code servers, how much my neighbor happens to be using the internet at the given moment, and the alignment of the stars. Or what do you think of these results:

MeanMedianRun 1Run 2Run 3
WSL44m 4s52m 1s52m 1s59m 29s20m 42s
VMWare46m 20s45m 5s37m 28s56m 29s45m 5s
Cloud build35m 18s33m 40s33m 40s41m 49s30m 26s

There were two packages that became bottlenecks in these tests: cross-localedef-native and linux-yocto. Especially the former. Other packages were downloaded in a few minutes but cross-localedef-native took almost always at least half an hour to download. In the WSL run #3 cross-localedef-native took only about 15 minutes to download, and instantly the whole download process was a lot quicker. In general, I would still boldly claim that Hetzner build is potentially the fastest on average. Or at least it would make sense, as the Internet pipes in their data centers are most likely wider than mine at my home.

That good ol’ YouTube player brings a nostalgic tear to my eye

Then, the actual build times. For these, I actually did four runs, because “third time’s the charm and one more for a good measure” (this blog text isn’t quite as scientific as I make it out to be). As mentioned earlier, I built the core-image-minimal for all these runs. These builds were done without downloading the sources in an attempt to get the build times to be a bit more stable. Here’s the table:

MeanMedianRun 1Run 2Run 3Run 4
WSL109m 15s106m 48s106m 11s121m 26s107m 26s103m 37s
VMWare125m 51s125m 51s126m 33s126m 32s125m 10s125m 9s
Cloud build106m 30s105m 35s111m 10s105m 29s105m 3s105m 40s

As perhaps expected, the virtual machine was constantly the slowest option. What however is slightly surprising is that the WSL was almost as fast as the cloud build server with the same resources. Or actually, with better resources as the build server had an SSD disk while WSL was running on an old HDD I bought from a friend of mine for 30 euros some years ago.

But yeah, in the light of this evidence I’d say that if you’ve got some dollars to spare, getting a cloud build server is the best option for building Yocto (if you can’t get a native Linux build machine that is). It’s not only the fastest option, but it’s also scalable, so when you’re building something else than the “trivial” core-image-minimal you can easily increase your build performance. I’ll admit that the time difference isn’t big in this comparison, but once you build more complex images the differences in the build times will become more apparent.

The cloud server used in this comparison costs about 15€ a month. If you want to give the cloud build a go, you can use this super-neat affiliate link to sign up and get 20€ worth of free credits. And maybe give me 10€ in the process if you happen to spend some actual money on the platform.

Shooting Easy Money GIF by iHeartRadio San Francisco - Find & Share on GIPHY
How well I handle my money every time the paycheck comes in

However, if you don’t have enough dollars for about two avocado toasts or three lattes every month, WSL is a better alternative than VMWare for building Yocto. Virtualbox seemed to be even worse than VMWare as it didn’t even want to build anything for me. It just spat out constant disk I/O errors that I simply couldn’t get fixed. Different caching options, physical disk drives, or even guest operating systems didn’t fix that.

But this is not all! You thought that you don’t have to read any more of these tables, didn’t you? I did some generic performance tests to get a bit better understanding of the different build machines, so I’ll add their results here as a bit of a bonus. The first one is the network speed test. I used the Ookla’s Speedtest command line tool for these. The values are averages of “about a dozen” runs.

LatencyDownloadUpload
WSL3.76ms104.22 Mb/s59.85 Mb/s
VMWare3.94ms104.13 Mb/s59.88 Mb/s
Cloud build0ms9266 Mb/s9220 Mb/s

There’s a bit of a difference in the latency between WSL and virtual machine, but the differences in the download and upload speeds are marginal. Cloud build is something entirely different than the other two, as expected.

Next is the hard drive speed, both reading and writing. For this, I used dd command-line tool. For the write test I used this command:

dd if=/dev/zero of=test bs=1G count=4 oflag=dsync

For the read test I used this command:

dd if=./test of=/dev/null

For the un-cached timings I cleared the caches with this command between the runs:

echo 3 | sudo tee /proc/sys/vm/drop_caches

For the cached timings I didn’t. Here are the results:

WriteWrite (cached)ReadRead (cached)
WSL91.6 MB/s93.7 MB/s91.9 MB/s628 MB/s
VMWare50.6 MB/s107.5 MB/s338 MB/s559 MB/s
Cloud build1.2 GB/s1.3 GB/s603 MB/s1.2 GB/s

This is where the cloud machine’s SSD seems to shine, even if it didn’t do so well when doing the actual build work. The differences between WSL and VMWare on the other hand are a bit varied. What these results seem to suggest is that the uncached write performance is quite important for Yocto build performance. However, results also seem to be showing that the mass storage performance isn’t quite as important for the Yocto builds.

Finally, the CPU speed test. This was done with sysbench, using the command sysbench cpu --threads=4 run. Here I compared the CPU events per second:

CPU events per second
WSL5359.31
VMWare5284.58
Cloud build14582.81

All things considered, it’s slightly surprising how small the cloud build’s margin of victory when comparing the build times actually was. Especially considering how much more performant the CPU is according to the raw numbers. The most important thing actually seemed to be the number of cores, because a three-core cloud machine would already be slower for Yocto builds than a four-core VMWare virtual machine. So please remember this when you’re setting up your Hetzner cloud instance after you’ve registered through this link (I promise this affiliate pushing won’t become a thing).

That’s all for now. This is also most likely the last text about Yocto builds with WSL for time being. Next time it’s either something different that’s not related to Yocto at all, or then it’s something similar that is related to Yocto but not WSL.

tl;dr: WSL is faster than a virtual machine, but I recommend using cloud servers if possible

Writer of this blog post is wondering how he ever managed to finish his thesis

Yocto and WSL, part 2: The Graphic Boogaloo

Read the other parts of “Yocto and WSL” series:
Yocto? On WSL2? Easier than you think!
Yocto and WSL, part 3: WSL vs. VMWare

So, in the previous part, we got the Yocto built with WSL and tested it out with a text-based terminal interface. This time we’re going to improve things a bit by getting the graphics working! Like every engineer always says, “it’s really nice to have this fancy graphical interface with buttons that have rounded corners instead of an efficient, text-based interface”.

(After publishing this text it was brought to my attention that WSL on Windows 11 actually supports GUI applications natively. I’m not sure how this works with QEMU but I’d recommend trying that out before going through all the hassle mentioned in this text. If you’re still stuck on Windows 10 like me these steps are at least currently applicable)

The process of getting graphics up and running in the WSL consists of three steps:

  1. Make a hole into the Windows firewall (starts promisingly) so that WSL can communicate with an X server
  2. Launch an X server with the access control disabled (this sounds great as well)
  3. Set the environment variables in WSL

The instructions I used to achieve this can be found in this great blog post. However, for the sake of documentation and out of the fear of broken hyperlinks, I’ll go through the steps here as well.

The hole in the firewall needs to be made because the networking in WSL2 is implemented in a way where the traffic is considered to be coming from an external source (as opposed to WSL1). The firewall rule can be set using the following steps:

  1. Search “Windows Defender Firewall with Advanced Security” from the Start Menu
  2. Click “Inbound Rules” on the left-hand side menu, and then click “New Rule…” on the right-hand side menu
  3. Select rule type to be “Port”, set the port to 6000 and give it a good name. The rest of the settings should be good by default (TCP protocol & allowing all kinds of access)
  4. Right click on the newly created rule in the rule list, select Properties and from there select Scope-tab. The limited scope prevents unwanted entities from exploiting the poor firewall’s new rule. Check the image below for Scope-settings:
Basically, we’re limiting the connections just to private IP-addresses

And just like that, the firewall should allow the traffic from WSL2 to the X server. The X server that we haven’t even installed yet. I have been using VcXsrv X server, and the next step is downloading and installing it. After VcXsrv has been installed we can continue.

As briefly mentioned earlier, the access control needs to be disabled when launching the server to make things go smoothly. Just use -ac flag when launching the X server from the command line to disable access control. The full command to launch VcXsrv from a Windows terminal is:

vcxsrv.exe :0 -multiwindow -wgl -ac -silent-dup-error 

The first parameter sets the display number to zero, the second one allows multiple windows, the third one doesn’t seem to be necessary after a quick test but I’ll still leave it here because it was in the original post, the fourth parameter is the important one that disables the access control and the last one prevents errors if launching multiple VcXsrv-servers. After getting the server running we’re almost ready to get into the exciting world of graphic content. Open up a WSL session, and export the following two variables (in WSL terminal):

export DISPLAY=$(awk '/nameserver / {print $2; exit}' /etc/resolv.conf 2>/dev/null):0
export LIBGL_ALWAYS_INDIRECT=1

The first variable sets the display we want to use (basically WSL nameserver address and display 0) and the second variable should prevent skipping X server when calling draw commands. I’m not 100% what that means, but it sounds important so it’s better to set it.

And now, finally, we’re ready to check out the graphical interface! If you’ve closed your WSL session in the past two months it took me to write this second blog post, first head to your poky-folder and run source oe-init-build-env again. Then you should be able to run:

runqemu
#look, no no-graphic option this time, wowzers!
Wait, I was promised graphics, what is this? I want to click icons and drag-n-drop stuff around.

Well, the thing is that the core-image-minimal image we built in the previous blog post doesn’t have a graphical interface built. While technically this new window is a graphical, emulated window, it’s not a graphical user interface. To get an image with these capabilities, we need to build core-image-sato instead (sorry).

So yeah, navigate to your build-folder and run bitbake core-image-sato. Even though the build reuses some of the artifacts from the previous build this will take some time, so be prepared to find something else to do for a while. After the build has been completed, you should be able to run runqemu again, and see something like this:

Awww yisssss

The apparent problem is the quite significant latency introduced by QEMU, and running QEMU on top of the WSL doesn’t do the situation any favors. Technically you can run the image also on a Windows native QEMU, but I didn’t see any noticeable difference in the GUI performance. My method of analysis was not the most scientific one though: I just pressed a button and tried to internally analyze how frustrated I got before something happened in the GUI. With both WSL QEMU and Windows native QEMU I got “quite frustrated”, so I’d call it a tie.

I didn’t do any actual benchmarking on if there’s a difference in the computing performance, perhaps in some upcoming blog post I could write a bit about that. It seems like the Windows native QEMU lacks some options (I couldn’t get the networking working (more like notworking amirite)), so it may be better to stick to WSL QEMU.

That’s all for this time. Hopefully, you learned something from this post, I at least did. Once again big shout-out to Skeptric-blog, plenty of other interesting texts in there as well.

Part 3 is now available here!

PS: I disabled the comments on this blog. It seems like the only ones writing those were freaky spambots. If you’ve got something to ask or say you can reach me on LinkedIn.