SWUpdate for Cubieboard QEMU

SWUpdate for Cubieboard QEMU

Software update is an important part of design of modern embedded systems. It allows faster time to market, since fixes, security or stability updates or even new features can be pushed to the devices after they have been deployed.

The update mechanism usually depends on the internet connectivity, but if device is physically accessible it can also be done using some local interface, like USB or ethernet connection.

When it comes to the Embedded Linux, there are different software update solutions which integrate well with Yocto

All of the mentioned solutions provide similar functionalities and require similar steps when integrating into an existing build process: bootloader integration, special memory partitioning, a way to perform fallback in case of a failed update.

In this post the steps for integrating SWUpdate with Yocto in order to enable software update will be described.

All of the changes that will be described, plus a few more which are required for this to work, are available in the scarthgap branch of meta-mistra repository in Github.

The following items will be covered in this post

Software update basics

The design of software update mechanism for an embedded system should take several items into account:

  • robustness: the upgrade process should be immune to sudden power-outages and should always have a way to return to functional state, even in a case of a failed or incomplete update
  • minimal downtime: during the upgrade process the system should provide designed functionality ideally without interruptions
  • security: there should be a way to check if the update file is not corrupted and that it comes from a trusted source

Of course, there are the usual factors which affect the embedded system design (cost, time, quality). Therefore, as with everything else, a form of compromise must be made when making the software update design.

From a high-level view, the robustness and length of downtime can be affected by the way the system memory is organized, whereas the security topic requires extra steps, both during the creation of the update file and during the upgrade process itself.

System memory partitioning

The premise is that the root filesystem partition cannot be updated while the system is running from it, so some other way has to be found to perform the update.

The two standard ways of partitioning system memory when talking about software update are the single (or recovery) and the double (or A/B) partitioning.

Recovery partitioning

In the single partitioning scheme, besides the root filesystem partition, there is also a recovery partition. The recovery partition holds only the minimal root filesystem, enough to perform the update.

When the system update is initiated, the system reboots into the recovery partition. Once in recovery, the update of the root filesystem partition is performed since the system is not running from it anymore. After the update, the system reboots into the regular root filesystem partition.

If the upgrade is interrupted or if the new version of the system does not work as expected, the system will reboot again into the recovery partition, so the robustness of the upgrade is maintained. It is then up to the recovery partition to handle that situation, either by flashing again “the golden image” (if it is available), or by connecting to the update server to obtain a new update file, or by enabling some physical interface (like USB) to perform the update manually. Of course, the rollback can also be available if there is room to store previous update images before starting an update with a new one, or to store compressed copy of the old root filesystem.

Looking at the downtime, it is not insignificant, since the system will not be running in normal operating mode across two reboots, and during the time it takes to perform the update while in recovery mode.

A/B partitioning

In the A/B partitioning there are two partitions that are used for holding copies of the root filesystem (A and B partitions). One partition is active, meaning the system has booted from it and it is used as current root filesystem partition. The other partition is inactive and not used during the current run.

When the update is started, the inactive partition is updated from the current active partition. Once update process is done, the reboot is performed and active and inactive partitions are switched, so the system will boot from the “other” partition, which is the active one now.

That way, in case of an update failure, there is always a partition to roll back to, since the partition that is inactive will hold the previous installation. It is just the matter of rebooting and changing the bootloader configuration to select the correct partition for mounting root filesystem.

This scheme is much better compared to the recovery scheme with respect to the downtime, since the system is not operational only during the reboot from current active partition to the new active partition. However, it requires more storage memory than the recovery-partition approach, since two root partitions are used.

More details about different partitioning approaches can be found in the SWupdate documentation.

In this example the A/B partitioning is used.

Read-only root filesystem

The robustness can be further improved by using a read-only access to the root filesystem.

Having the root filesystem read-only prevents all changes to the root filesystem, both unwanted (malicious or caused by mistake), but also improves the lifetime of the product since the underlying flash storage is not worn out.

However, having the whole root filesystem as read-only can impair the functionality of some services which need writable access. In order to mitigate this, different methods can be used to have parts of the filesystem writable by using overlayfs or by bind-mounting a writable directory over a certain path in the filesystem. Some of the examples for Yocto are the

  • volatile-binds: allows providing list of paths that should have writable access; these paths will be mounted as tmpfs overlay if possible, otherwise a “copy-bind” mount will be performed
  • overlayfs: allows specifying the paths that should be overlay mounted and backing storage for the overlay mount; also has separate class for /etc directory

There are other options, like overlayroot, which can make a writable overlay over the whole root filesystem, but that one requires initramfs to be able to execute chroot before system is initialized.

Besides the regular userspace services, the customer applications might need to store persistent information, so it is always good to have a persistent data storage. That is usually a separate partition which is mounted as read-write.

In this example the /data partition is used for rw data storage, and volatile-binds are already bundled and can be extended.

SWUpdate introduction

SWUpdate is a software update mechanism for Linux systems, targeted at embedded systems. It supports different channels for providing the update file:

  • directly from local filesystem
  • local web interface (static web page that provides upload functionality)
  • remote download, using integrated download client
  • remote download, initiated by the remote server

The update file is a cpio archive, with sw-description file in the beginning, followed by different parts of the update binaries or images, as well as shell or lua scripts. Inside the sw-description different partitions can be specified, as well as bootloader commands or environment updates which need to be triggered during the update process.

SWUpdate also supports different methods for signing the update file and its contents, as well as encryption of the update file.

SWUpdate configuration

SWUpdate is highly customizable. The configuration is done in two steps, through the config file (same as U-Boot or Linux kernel, menuconfig can be used) and the swupdate.cfg file.

The config file specifies the features that will be compiled into the swupdate binary, which it will support at runtime. That means that the specific instance can be configured to support only local web interface download, or to support update file signing using the RSA key, but not to encrypt it.

The swupdate.cfg file specifies the device configuration for the interfaces that are supported, like hardware ID, or port to use for the remote connection.

Since the default SWupdate configuration has local update and update via local web server already enabled, the additional changes to the configuration are only to remove lua support (regular bash script will be used for pre/post hooks), enable systemd and few other minor changes.

# CONFIG_MTD is not set
# CONFIG_LUA is not set
CONFIG_SYSTEMD=y
CONFIG_ARCHIVE=y
CONFIG_LOCALE=y
CONFIG_BOOTLOADERHANDLER=y
CONFIG_UPDATE_STATE_CHOICE_BOOTLOADER=y

The remote update will not be considered in this blog post, since it also requires configuration of the hawkbit instance.

The swupdate.cfg file holds only the configuration for the signing key and the local web server configuration

globals :
{
  verbose = true;
  loglevel = 5;
  syslog = true;
  public-key-file = "/lib/swupdate/mistra-test.cert.pem";
};

webserver :
{
  document_root = "/www";
  userid = 0;
  groupid = 0;
};

Bootloader configuration

As with other software update mechanisms, the bootloader integration is the most important one.

The bootloader is supposed to handle the information about the active partition and to handle the rollback conditions (this will be covered in Rollback mechanism).

Since root partition information is passed as kernel boot argument, the bootloader should handle the partition selection during the update process. The information about the active partition is stored in the bootloader environment.

if test -z "$rootpart"; then
  setenv rootpart 2
  saveenv
fi

Rollback mechanism

The rollback mechanism allows to recover the system in the case of a failed update. A failed update in this context would be an update after which the system cannot boot, either due to kernel or root filesystem not being copied properly, or due to some misconfiguration.

A failed update could also be if the applications that implement the system functionality are not behaving as they should, but in those cases a different approach to rollback should be taken, where a certain “monitoring” application can detect and decide that something is wrong.

In order to handle situations where system keeps rebooting after a failed update, the U-Boot bootloader has built-in support for boot counters. The boot counter can be enabled using the following config items (for Yocto it is enough to add the .cfg file with these lines to the u-boot recipe)

CONFIG_BOOTCOUNT_LIMIT=y
CONFIG_BOOTCOUNT_ENV=y
CONFIG_BOOTCOUNT_BOOTLIMIT=3

The boot counter starts from zero and is incremented every time the system boots if the variable upgrade_available is set to 1. This way, the counter will not be changed when performing a regular reboot.

If the counter value reaches the maximum configured, then the U-Boot will run the altbootcmd, which can be configured to perform the switching of the active partition.

if test -z "$altbootcmd"; then
  setenv altbootcmd 'echo Rollback upgrade
    if test ${rootpart} = 2; then setenv rootpart 3; else setenv rootpart 2; fi
    setenv upgrade_available 0; setenv bootcount 0; saveenv
    boot'
  saveenv
fi

Since U-Boot environment will be holding the information about the boot counters, as well as the information about whether an upgrade is available, the userspace applications should also have a way to access those variables. The access to bootcounters is needed to be able to reset them on a successful system start, and upgrade in progress information should be set once the upgrade is initiated.

In order to provide the access, the libubootenv is used. It provides access to the fw_printenv and fw_setenv applications, which can be used to read and write the U-Boot variables.

The only thing that is required for the libubootenv to work is to configure where the environment is located, using the fw_env.config file. In the case of Allwinner-A10, the environment is kept in the boot partition (FAT32 formatted) by default, so the contents of the fw_env.config file are

/boot/uboot.env  0x0000          0x10000

Upgrade done service

Since the upgrade_available variable is used to allow incrementing the boot counter when starting the system, it needs to be handled properly from the userspace.

When the upgrade is performed, the variable needs to be set to 1. That should be specified in the sw-description so it will be handled by the swupdate.

Once upgrade is done, the userspace has to confirm that the upgrade has been a success, and the best way is to set the upgrade_available to zero. This can be done in a script that runs at startup, but also can be a separate application which monitors the system behavior after an upgrade.

An example of a systemd service which clears upgrade_available with delay of 30 seconds from the start is presented and used in this example (adapted from RAUC)

[Unit]
Description=Service to mark the upgrade as success
After=boot-complete.target
Requires=boot-complete.target
DefaultDependencies=no

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=sleep 30 && fw_setenv upgrade_available 0

[Install]
WantedBy=multi-user.target

Signing the update file

The integrity of an update is an important aspect to consider.

In order to maintain integrity, the system should know that the update file comes from a trusted source. One way of doing it is having a trusted source sign the update file with a private key, and then provide the public certificate to the system. This way, the system can perform a runtime check if the certificate matches the key that was used for the signature.

The SWupdate manual has instructions on creating the CMS key and certificate.

openssl req -x509 -newkey rsa:4096 -nodes -keyout mistra-test.key.pem \
    -out mistra-test.cert.pem -subj "/O=SWUpdate /CN=target"

After the certificate is available, the SWupdate configuration needs to be updated to use the CMS key and to generate the sw-description.sig file.

CONFIG_HASH_VERIFY=y
CONFIG_SIGNED_IMAGES=y
CONFIG_SIGALG_CMS=y
CONFIG_CMS_IGNORE_EXPIRED_CERTIFICATE=y
CONFIG_CMS_IGNORE_CERTIFICATE_PURPOSE=y

The key needs to be integrated into the build process, and the certificate should be copied to the root filesystem.

In this example, it was easy to put the key and the certificate in the meta-mistra layer, and update the swupdate recipe to use them from that location. However, in a real build system, the private key should be kept in a secure location and obtained temporarily, only to facilitate the signing of the update file.

Testing

Several tests are interesting when it comes to testing software update.

The first and the most important is that it is working :) For that test, both the upgrade via command line and upgrade via local web server interface can be used.

Another thing that is very important is to verify that our signature checking is working. We can make a new temporary certificate, copy it to the image and try and upgrade, which should fail since the certificate will not match the key.

In the end, for testing robustness, we can verify that the boot counters are working as expected, and that if we do not clear the upgrade_available variable from userspace and keep rebooting the system, that the altbootcmd will be executed.

Building initial image

The original mistra-image is not suitable for the A/B partition upgrade, since it has only one root filesystem partition. Therefore, a new image, mistra-swupdate is created, with the corresponding wks file

part u-boot --source rawcopy --sourceparams="file=${SPL_BINARY}" --ondisk mmcblk0 --no-table --align 8
part /boot --source bootimg-partition --ondisk mmcblk0 --fstype=vfat --label boot --active --align 2048 --fixed-size ${SUNXI_BOOT_SPACE}
part /     --source rootfs --ondisk mmcblk0 --fstype=ext4 --label root_A --align 2048 --size 256M
part /     --source rootfs --ondisk mmcblk0 --fstype=ext4 --label root_B --align 2048 --size 256M
part --size 1G --ondisk mmcblk0 --fstype=ext4 --label data --align 2048

The wks file defines a boot partition, two root filesystem partitions, and a data partition, intended to be used a persistent storage between upgrades.

The fstab is also updated to mount the /boot and /data partitions on every start

# We need to mount the first partition to access the U-Boot environment
/dev/mmcblk0p1       /boot                auto       defaults,sync         0  0
# Mount persistent data directory
/dev/mmcblk0p4       /data                auto       defaults,sync         0  0

The mistra-swupdate image recipe is presented here

require recipes-extended/images/mistra-image.bb

CORE_IMAGE_EXTRA_INSTALL += " \
    libubootenv-bin \
    swupdate \
    swupdate-www \
    kernel-image \
    kernel-devicetree \
"

IMAGE_FSTYPES = "ext4.gz wic.gz"
IMAGE_BOOT_FILES = "boot.scr"
IMAGE_FEATURES += "read-only-rootfs"

create_data_dir() {
   mkdir -p ${IMAGE_ROOTFS}/data
}

IMAGE_PREPROCESS_COMMAND += "create_data_dir;"

WKS_FILES = "mistra-swupdate.wks.in"

It is built using

. ./setup-environment build_fb
MACHINE=cubieboard-ng DISTRO=mistra-framebuffer bitbake mistra-swupdate

After the build is done, the compressed wic image will be in tmp/deploy/images/cubieboard-ng/mistra-swupdate-cubieboard-ng.rootfs.wic.gz. The image is converted to sd.img

qemu-img create sd.img 2G
gunzip -c tmp/deploy/images/cubieboard-ng/mistra-swupdate-cubieboard-ng.rootfs.wic.gz | dd of=sd.img bs=1M iflag=fullblock oflag=direct conv=fsync
qemu-img resize sd.img 2G

and run inside QEMU using the following commands

qemu-system-arm -machine cubieboard \
                -m 1G \
                -drive file=sd.img,format=raw,if=sd \
                -serial mon:stdio \
                -nographic \
                -net nic \
                -net tap,ifname=qemu-tap0,script=no,downscript=no
U-Boot SPL 2024.01-g (Jan 08 2024 - 15:37:48 +0000)
DRAM: 1024 MiB
CPU: 1008000000Hz, AXI/AHB/APB: 3/2/2
Trying to boot from MMC1


U-Boot 2024.01-g (Jan 08 2024 - 15:37:48 +0000) Allwinner Technology

CPU:   Allwinner A10 (SUN4I)
Model: Cubietech Cubieboard
DRAM:  1 GiB
Core:  74 devices, 24 uclasses, devicetree: separate
WDT:   Not starting watchdog@1c20c90
MMC:   mmc@1c0f000: 0
Loading Environment from FAT... Unable to read "uboot.env" from mmc0:1...
Unknown monitor
Unknown monitor
In:    serial,usbkbd
Out:   serial,vidconsole
Err:   serial,vidconsole
Net:
Error: ethernet@1c0b000 No valid MAC address found.
No ethernet found.

starting USB...
Bus usb@1c14000: USB EHCI 0.00
Bus usb@1c14400: USB OHCI 0.0
Bus usb@1c1c000: USB EHCI 0.00
Bus usb@1c1c400: USB OHCI 0.0
scanning bus usb@1c14000 for devices... 1 USB Device(s) found
scanning bus usb@1c14400 for devices... 1 USB Device(s) found
scanning bus usb@1c1c000 for devices... 1 USB Device(s) found
scanning bus usb@1c1c400 for devices... 1 USB Device(s) found
       scanning usb for storage devices... 0 Storage Device(s) found
Hit any key to stop autoboot:  0

...

Mistra FrameBuffer 4.0 cubieboard-ng ttyS0

cubieboard-ng login:

The SD card partition can be validated using fdisk

fdisk -l /dev/mmcblk0
Disk /dev/mmcblk0: 2048 MB, 2147483648 bytes, 4194304 sectors
32768 cylinders, 4 heads, 32 sectors/track
Units: sectors of 1 * 512 = 512 bytes

Device       Boot StartCHS    EndCHS        StartLBA     EndLBA    Sectors  Size Id Type
/dev/mmcblk0p1 *  32,0,1      671,3,32          4096      86015      81920 40.0M  c Win95 FAT32 (LBA)
/dev/mmcblk0p2    672,0,1     1023,3,32        86016     767589     681574  332M 83 Linux
/dev/mmcblk0p3    1023,3,32   1023,3,32       770048    1451621     681574  332M 83 Linux
/dev/mmcblk0p4    1023,3,32   1023,3,32      1454080    3551231    2097152 1024M 83 Linux

The new U-Boot variables can be checked using the fw_printenv command

fw_printenv bootcount rootpart upgrade_available
bootcount=1
rootpart=2
upgrade_available=

The upgrade_available is empty since the upgrade has not been started.

The swupdate service will be running, which also means that it is accessible from the local web server interface at “192.168.123.1:8080”

systemctl status swupdate
* swupdate.service - SWUpdate daemon
     Loaded: loaded (/usr/lib/systemd/system/swupdate.service; enabled; preset: enabled)
     Active: active (running) since Mon 2024-07-15 22:14:27 UTC; 6min ago
TriggeredBy: * swupdate.socket
       Docs: https://github.com/sbabic/swupdate
             https://sbabic.github.io/swupdate
   Main PID: 198 (swupdate)
        CPU: 4.339s
     CGroup: /system.slice/swupdate.service
             |-198 /usr/bin/swupdate -H cubieboard-ng 1.0 -e stable copy2 -p "'reboot'" -f /etc/swupdate.cfg -w "-r /www -p 8080"
             `-220 /usr/bin/swupdate -H cubieboard-ng 1.0 -e stable copy2 -p "'reboot'" -f /etc/swupdate.cfg -w "-r /www -p 8080"

Installing update

The update-image recipe is used to build the update image. It contains the rootfs defined by the mistra-swupdate image, and bundles the sw-description

DESCRIPTION = "Example image demonstrating how to build SWUpdate compound image"

LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302"

inherit swupdate

SWUPDATE_SIGNING="CMS"
SWUPDATE_CMS_CERT="${TOPDIR}/../sources/meta-mistra/certs/mistra-test.cert.pem"
SWUPDATE_CMS_KEY="${TOPDIR}/../sources/meta-mistra/certs/mistra-test.key.pem"

SRC_URI = "\
    file://shellscript.sh \
    file://sw-description \
"

# images to build before building swupdate image
IMAGE_DEPENDS = "mistra-swupdate"

# images and files that will be included in the .swu image
SWUPDATE_IMAGES = "mistra-swupdate-cubieboard-ng.rootfs"

SWUPDATE_IMAGES_FSTYPES[mistra-swupdate-cubieboard-ng.rootfs] = ".ext4.gz"

It is built using the following command

MACHINE=cubieboard-ng DISTRO=mistra-framebuffer bitbake update-image

After the build is done, the resulting swu file can be found in tmp/deploy/images/cubieboard-ng/update-image-cubieboard-ng.rootfs.swu.

Using the command line

In order to perform an update, the file needs to be transferred to the board. Since ssh is not configured, we will copy the file to the /data partition before starting the QEMU.

The partition can be mounted using the same steps as in QEMU board emulation part 2

sudo kpartx -av ./sd.img
sudo mkdir -p /run/mount/data
sudo mount /dev/mapper/loop0p4 /run/mount/data

After the partition is mounted, the file can be copied and partition unmounted

sudo cp tmp/deploy/images/cubieboard-ng/update-image-cubieboard-ng.rootfs.swu /run/mount/data
sudo umount /run/mount/data
sudo kpartx -d ./sd.img

The QEMU can be started, and after the system has booted, the upgrade process is run using

swupdate -H cubieboard-ng:1.0 -e stable,copy2 -i update-image-cubieboard-ng.rootfs.swu
SWUpdate v2024.05.2

Licensed under GPLv2. See source distribution for detailed copyright notices.

...
[INFO ] : SWUPDATE started :  Software Update started !
[TRACE] : SWUPDATE running :  [network_initializer] : Software update started
[TRACE] : SWUPDATE running :  [extract_file_to_tmp] : Found file
[TRACE] : SWUPDATE running :  [extract_file_to_tmp] :   filename sw-description
[TRACE] : SWUPDATE running :  [extract_file_to_tmp] :   size 1904
[TRACE] : SWUPDATE running :  [extract_file_to_tmp] : Found file
[TRACE] : SWUPDATE running :  [extract_file_to_tmp] :   filename sw-description.sig
[TRACE] : SWUPDATE running :  [extract_file_to_tmp] :   size 2161
[TRACE] : SWUPDATE running :  [swupdate_verify_file] : Verified OK
[DEBUG] : SWUPDATE running :  [parse_cfg] : Parsing config file /tmp/sw-description
[TRACE] : SWUPDATE running :  [get_common_fields] : Version 0.1.0
[TRACE] : SWUPDATE running :  [get_common_fields] : reboot_required 1
[TRACE] : SWUPDATE running :  [parse_hw_compatibility] : Accepted Hw Revision : 1.0
[TRACE] : SWUPDATE running :  [_parse_images] : Found compressed Image: mistra-swupdate-cubieboard-ng.rootfs.ext4.gz in device : /dev/mmcblk0p3 for handler raw
[TRACE] : SWUPDATE running :  [_parse_scripts] : Found Script: shellscript.sh
[TRACE] : SWUPDATE running :  [_parse_bootloader] : Bootloader var: upgrade_available = 1
[TRACE] : SWUPDATE running :  [_parse_bootloader] : Bootloader var: rootpart = 3
[TRACE] : SWUPDATE running :  [check_hw_compatibility] : Hardware cubieboard-ng Revision: 1.0
[TRACE] : SWUPDATE running :  [check_hw_compatibility] : Hardware compatibility verified
[DEBUG] : SWUPDATE running :  [preupdatecmd] : Running Pre-update command
[TRACE] : SWUPDATE running :  [extract_files] : Found file
[TRACE] : SWUPDATE running :  [extract_files] :         filename shellscript.sh
[TRACE] : SWUPDATE running :  [extract_files] :         size 394 required
[TRACE] : SWUPDATE running :  [extract_files] : Found file
[TRACE] : SWUPDATE running :  [extract_files] :         filename mistra-swupdate-cubieboard-ng.rootfs.ext4.gz
[TRACE] : SWUPDATE running :  [extract_files] :         size 23926605 required
[TRACE] : SWUPDATE running :  [extract_padding] : Expecting up to 512 padding bytes at end-of-file
[TRACE] : SWUPDATE running :  [network_initializer] : Valid image found: copying to FLASH
[INFO ] : SWUPDATE running :  Installation in progress
[TRACE] : SWUPDATE running :  [read_lines_notify] : call do_preinst
[TRACE] : SWUPDATE running :  [read_lines_notify] : do_preinst
[TRACE] : SWUPDATE running :  [__run_cmd] : /tmp/scripts/shellscript.sh preinst  command returned 0
[TRACE] : SWUPDATE running :  [install_single_image] : Found installer for stream mistra-swupdate-cubieboard-ng.rootfs.ext4.gz raw
[TRACE] : SWUPDATE running :  [read_lines_notify] : call do_postinst
[TRACE] : SWUPDATE running :  [read_lines_notify] : do_postinst
[TRACE] : SWUPDATE running :  [__run_cmd] : /tmp/scripts/shellscript.sh postinst  command returned 0
[INFO ] : SWUPDATE successful ! SWUPDATE successful !
[INFO ] : SWUPDATE running :  [endupdate] : SWUpdate was successful !
[DEBUG] : SWUPDATE running :  [postupdate] : Running Post-update command

The -e stable,copy2 is required, and it changes based on the current partition from which the system is running. In this case, the system is running from /dev/mmcblk0p2 (copy1 partition), so the target is set to /dev/mmcblk0p3 (copy2)

Once the upgrade is complete, the system can be restarted using

reboot

, and we can check that the system is now running from the other partition

fw_printenv bootcount rootpart upgrade_available
bootcount=1
rootpart=3
upgrade_available=1

Of course, if the same command is run again after 30 seconds, it will just say that the upgrade_available is equal to zero

fw_printenv bootcount rootpart upgrade_available
bootcount=1
rootpart=3
upgrade_available=0

Using the local web server interface

If the local web server interface is used, we only need to provide the path to the correct update file, and it will do everything for us

During the update the progress bar is visible, and again after the upgrade we can check the U-Boot environment variables, to confirm that the upgrade is done.

Test with wrong certificate

In order to perform the test with the wrong certificate, a new certificate can be created following the steps from above.

Once the certificate is created, it can be copied to the root filesystem partition at /usr/lib/swupdate/mistra-test.cert.pem, and upgrade process can be started.

The output of SWupdate in that case is

Jul 20 23:26:09 cubieboard-ng swupdate.sh[198]: [TRACE] : SWUPDATE running :  [network_thread] : Incoming network request: processing...
Jul 20 23:26:09 cubieboard-ng swupdate.sh[198]: [INFO ] : SWUPDATE started :  Software Update started !
Jul 20 23:26:09 cubieboard-ng swupdate.sh[198]: [TRACE] : SWUPDATE running :  [network_initializer] : Software update started
Jul 20 23:26:09 cubieboard-ng swupdate.sh[198]: [TRACE] : SWUPDATE running :  [extract_file_to_tmp] : Found file
Jul 20 23:26:09 cubieboard-ng swupdate.sh[198]: [TRACE] : SWUPDATE running :  [extract_file_to_tmp] :         filename sw-description
Jul 20 23:26:09 cubieboard-ng swupdate.sh[198]: [TRACE] : SWUPDATE running :  [extract_file_to_tmp] :         size 1904
Jul 20 23:26:09 cubieboard-ng swupdate.sh[198]: [TRACE] : SWUPDATE running :  [extract_file_to_tmp] : Found file
Jul 20 23:26:09 cubieboard-ng swupdate.sh[198]: [TRACE] : SWUPDATE running :  [extract_file_to_tmp] :         filename sw-description.sig
Jul 20 23:26:09 cubieboard-ng swupdate.sh[198]: [TRACE] : SWUPDATE running :  [extract_file_to_tmp] :         size 2161
Jul 20 23:26:09 cubieboard-ng swupdate.sh[198]: 600311B6:error:17000064:CMS routines:cms_signerinfo_verify_cert:certificate verify error:/usr/src/debug/openssl/3.2.2/crypto/cms/cms_smime.c:290:Verify error: self-signed certificate
Jul 20 23:26:09 cubieboard-ng swupdate.sh[198]: [ERROR] : SWUPDATE failed [0] ERROR swupdate_cms_verify.c : swupdate_verify_file : 275 : Signature verification failed

The output indicates that the certificate on the root filesystem does not match the key that was used for signing the bundle, which is expected.

Testing rollback

In order to test the rollback, we can perform the upgrade, and then stop and restart QEMU for four times after the U-Boot loads the Linux kernel.

This way, the U-Boot will increment the boot counter every time, but the userspace service which is supposed to clear upgrade_available flag will not be executed.

After the fourth start, the altbootcmd will be executed, which will cause the system to roll back to the old root partition. The following U-Boot output can be noticed

Warning: Bootlimit (3) exceeded. Using altbootcmd.
Hit any key to stop autoboot:  0
Rollback upgrade

Also, the partitions can be checked to verify that the rollback has been performed.

Summary

In this post the steps for adding the SWupdate update mechanism to the Cubieboard-ng Yocto BSP were shown. Upgrade support is very important for modern embedded systems, since it allows both adding new features, improving security and deployment of bug fixes remotely after the device is put into the field.

Similar steps can be taken for other boards, but also for other upgrade mechanisms, like RAUC or Mender.

Some of the interesting improvements to the approach that is shown in this post would be to add separate partitions for the kernel and bootloader, which would allow individual updates of those components (not only with the whole root filesystem update as it is done now). Another thing that would be interesting is to use the eMMC bootparts for storing either U-Boot environment, or for storing copies of U-Boot, but QEMU does not support that level of detail in the eMMC memory emulation, so it requires a “real” board to implement and test.

In the end, the remote updates using Hawkbit server are left for some other post.

Subscribe
If you would like to get information as soon as new content is published, please subscribe to the "MistraSolutions newsletter".