This is the second post in the series about building u-boot based gadget snaps, following Building u-boot gadget snap packages from source.
If you have read the last post in this series, you have likely noticed that there is a uboot.patch file being applied to the board config before building the u-boot binaries. This post will take a closer look at this patch.
As you might know already, Ubuntu Core will perform a fully automatic roll-back of upgrades of the kernel or the core snap (rootfs), if it detects that a reboot after the upgrade has not fully succeeded. If an upgrade of the kernel or core snap gets applied, snapd sets a flag in the bootloader configuration called “snap_mode=” and additionally sets the “snap_try_core=” and/or “snap_try_kernel=” variables.
To set these flags and variables that the bootloader should be able to read at next boot, snapd will need write access to the bootloader configuration.
Now, u-boot is the most flexible of all bootloaders, the configuration can live in a uEnv.txt file, in a boot.scr or boot.ini script on a filesystem, in raw space on the boot media or on some flash storage dedicated to u-boot or even a combination of these (and I surely forgot other variations in that list). This setup can vary from board to board and there is no actual standard.
Since it would be a massive amount of work and code to support all possible variations of u-boot configuration management in snapd, the Ubuntu Core team had to decided for one default process and pick a standard here.
Ubuntu Core is designed with completely unattended installations in mind, being the truly rolling Ubuntu, it should be able to upgrade itself at any time over the network and should never corrupt any of its setup or configuration, not even when a power loss occurs in the middle of an update or while the bootloader config is updated. No matter if your device is an embedded industrial controller mounted to the ceiling of a multi level factory hall, a cell tower far out in the woods or some floating sensor device on the ocean, the risk of corrupting any of the bootloader config needs to be as minimal as possible.
Opening a file, pulling it to RAM, changing it, then writing it to a filesystem cache and flushing that in the last step is quite a time-consuming thing. The time window where the system is vulnerable to corruption due to power outage is quite big. Instead we want to atomically toggle a value; preferably directly on disk with no caches at all. This cuts the potential corruption time down to the actual physical write operation, but also rules out most of the file based bits from the above list (uEnv.txt or boot.scr/.ini) and leaves us with the raw options.
That said, we can not really enforce an additional partition for a raw environment, a board might have a certain boot process that requires a very specific setup of partitions shipping binary blobs from the vendor before even getting to the bootloader (i.e. see the dragonboard-410c. Qualcomm requires 8 partitions with different blobs to initialize the hardware before even getting to u-boot.bin). To not exclude such boards we need to find a more generic setup. The solution here is a compromise between filesystem based and raw … we create an img file with fixed size (which allows the atomic writing we want) but put that on top of a vfat partition (our system-boot partition that also carries kernel, initrd and dtb) for biggest flexibility.
To make it easier for snapd and the user space side, we define a fixed size (the same size on all boards) for this img file. We also tell u-boot and the userspace tools to use redundancy for this file which allows the desired atomic writing.
Lets move on with some real-world example, looking at a board i recently created a gadget snap for …
I have an old Freescale SabreLite (IMX6) board lying around here, its native SCSI controller and gigabit ethernet make it a wonderful target device for i.e. a NAS or really fast Ubuntu Core based netxtcloud box.
A little research shows it uses the nitrogen6x configuration from the u-boot source tree which is stored in include/configs/nitrogen6x.h
To find the currently used environment setup for this board we just grep for “CONFIG_ENV_IS_IN” in that file and will find the following block:
#if defined(CONFIG_SABRELITE) #define CONFIG_ENV_IS_IN_MMC #else #define CONFIG_ENV_IS_IN_SPI_FLASH #endif
So this board defines a raw space on the MMC to be used for the environment if we build for the SabreLite, but we want to use CONFIG_ENV_IS_IN_FAT with the right parameters to make use of an uboot.env file from the first vfat partition on the first SD card.
Lets tell this in the config:
#if defined(CONFIG_SABRELITE) #define CONFIG_ENV_IS_IN_MMC +#undef CONFIG_ENV_IS_IN_MMC +#define CONFIG_ENV_IS_IN_FAT #else #define CONFIG_ENV_IS_IN_SPI_FLASH #endif
If we just set this we’ll run into build errors though, since the CONFIG_ENV_IS_IN_FAT also wants to know which interface, device and filename it should use:
#if defined(CONFIG_SABRELITE) #define CONFIG_ENV_IS_IN_MMC +#undef CONFIG_ENV_IS_IN_MMC +#define CONFIG_ENV_IS_IN_FAT +#define FAT_ENV_INTERFACE "mmc" +#define FAT_ENV_DEVICE_AND_PART "1" +#define FAT_ENV_FILE "uboot.env" #else #define CONFIG_ENV_IS_IN_SPI_FLASH #endif
So here we tell u-boot that it should use mmc device number 1 and read a file called uboot.env.
FAT_ENV_DEVICE_AND_PART can actually take a partition number, but if we do not set it, it will try to automatically use the very first partition found … (so “1” is equivalent to “1:1” in this case … on something like the dragonboard where the vfat is actually the 8th partition we use “1:8”).
While the above patch would already work with some uboot.env file, it would not yet work with the one we need for Ubuntu Core. Remember the atomic writing thing from above ? This requires us to set the CONFIG_SYS_REDUNDAND_ENVIRONMENT option too (note i did not typo this, the option is really called “REDUNDAND” for whatever reason).
Setting this option tells u-boot that there is a different header on the file and that write operations should be done atomic.
Ubuntu Core defaults to a fixed file size for uboot.env. We expect the file to be exactly 128k big, so lets find the “CONFIG_ENV_SIZE” option in the config file and adjust it too if it does define a different size:
/* Environment organization */ -#define CONFIG_ENV_SIZE (8 * 1024) +#define CONFIG_ENV_SIZE (128 * 1024) #if defined(CONFIG_SABRELITE) #define CONFIG_ENV_IS_IN_MMC +#undef CONFIG_ENV_IS_IN_MMC +#define CONFIG_ENV_IS_IN_FAT +#define FAT_ENV_INTERFACE "mmc" +#define FAT_ENV_DEVICE_AND_PART "1" +#define FAT_ENV_FILE "uboot.env" +#define CONFIG_SYS_REDUNDAND_ENVIRONMENT #else #define CONFIG_ENV_IS_IN_SPI_FLASH #endif
Trying to build the above will actually end up with a build error complaining that fat writing is not enabled, so we will have to add that too …
One other bit that Ubuntu core expects is that we can load a proper initrd.img without having to mangle or modify it in the kernel snap (by i.e. making it a uInitrd or whatnot) so we need to define the CONFIG_SUPPORT_RAW_INITRD option as well since it is not set by default for this board.
Our final patch now looks like:
/* Environment organization */ -#define CONFIG_ENV_SIZE (8 * 1024) +#define CONFIG_ENV_SIZE (128 * 1024) #if defined(CONFIG_SABRELITE) #define CONFIG_ENV_IS_IN_MMC +#undef CONFIG_ENV_IS_IN_MMC +#define CONFIG_ENV_IS_IN_FAT +#define FAT_ENV_INTERFACE "mmc" +#define FAT_ENV_DEVICE_AND_PART "1" +#define FAT_ENV_FILE "uboot.env" +#define CONFIG_SYS_REDUNDAND_ENVIRONMENT #else #define CONFIG_ENV_IS_IN_SPI_FLASH #endif +#define CONFIG_FAT_WRITE +#define CONFIG_SUPPORT_RAW_INITRD
With this we are now able to build a u-boot.bin that will handle the Ubuntu Core uboot.env file from the system-boot partition, read and write the environment from there and allow snapd to modify the same file from user space on a booted system when kernel or core snap updates occur.
The actual uboot.env file needs to be created using the “mkenvimage” tool with the “-r” (redundant) and “-s 131072” (128k size) options, from an input file. In the branch at  you will find the call of this command in the snapcraft.yaml file in the “install” script snippet. It uses the uboot.env.in textfile that stores the default environment we use …
The next post in this series will take a closer look at the contents of this uboot.env.in file, what we actually need in there to achieve proper rollback handling and how to obtain the default values for it.
If you have any questions about the process, feel free to ask here in the comments or open a thread on https://forum.snapcraft.io in the device category.