Image builder

Introduction

Embedded system images for Linux are usually built using Yocto, Buildroot or similar tools. While these are useful tools, our experience has been that it is sometimes quite hard to control what gets into the file-system of the device. Yocto has a lot of configuration possibilities and with recipe overrides and complex inheritance hiearchies, it is a challenge to figure out why a certain file is on the target filesystem.

Embedded Linux, unlike desktop Linux tends to be much smaller and have a limited set of software package. It is also important to restrict packages in the filesystem due to licensing reasons. When it comes to safety and security, it is important to justify the need for each file on the target and this becomes easier to manage with an approach that explicitly adds each file into the filesystem.

The Sabaton imagebuilder application is a powerful tool that you can use to put together the target flash image using a declarative configuration file.

The Sabaton imagebuilder is bundled with the Sabaton commandline tool.

Start with the device manifest

The first file that imagebuilder looks at is the device manifest. Here is an example of the manifest for the QEMU device. The device manifest is always called device.toml. The root of each device directory will have a single device.toml that is the entry point for that device.

1## Qemu device configuration. You can have multiple ones
2[[profile]]
3name = "QEMU-sysd"
4device_id = "c7892a10-9217-11ec-b59d-0242ac110002"
5version = "0.0.1"
6# Use these image files
7images = ["image.toml","vendor.toml","data.toml"]
8## Linux Kernel information
9# Pull the kernel from this location
10kernel_src = "git@github.com:gregkh/linux.git"
11# Use this branch
12kernel_checkout = "1cd6e30b83d741562b55bf5b7763b1238a91150c"
13# Kernel configuration to use
14kernel_config = "defconfig"
15# Override the .config of the kernel (Optional)
16kernel_dot_config = "kernel/config.release"
17
18# This kernel can support the folling targets.
19target = "aarch64-unknown-linux-gnu"
20flash_layout = "partition.toml"
21initrd = { image="initrd.toml", target="aarch64-unknown-linux-musl"}
22
23image_to_partition = [
24 [["system_a","system_b"],"system.ext2", "options"], # recovery_a and recovery_b contains the same recovery image. fs type is ext2
25 [["vendor_a","vendor_b"],"vendor.ext2", "options"], #
26 [["data"],"data.ext2","options"],
27]
28
29# Configure where the verity hashes are stored. the vbmeta_a and vbmeta_b partitions must exist in the flash_layout configuration.
30verity_partitions = [
31 ["vbmeta_a",["system_a","vendor_a"]], # vbmeta_a partition contains verity hash for system_a and vendor_a
32 ["vbmeta_b",["system_b","vendor_b"]], # vbmeta_b partition contains verity hash for system_b and vendor_b
33]
34
35# The private key is stored in the device specific config.
36# Signing of the verity hash can also be done on a remote server.
37verity_key = "secrets/verity/verity_rsa-2048-private-key.pk8"

Explanation of the Device Manifest (device.toml)

All imagebuilder config files use the TOML syntax. TOML.

Name of device

In Line 3, the name field specifies the name of the device. This was what was listed when you typed cargo brrr -l .

Device ID

The device_id field is a unique identifier for this device. This information is encoded in the filesystem as you will see later on, and can be used by software to detect the type of device.

Version

This is the software version number of the device. Use semantic versioning for this field as per the following rules.

  1. Changing the partition layout or size is considered a major version change. Changing the partition layout results in a non-failsafe update in case the system needs repartitioning on the field.
  2. Increment the minor version for updated software revisions which introduce new features.
  3. Increment the patch version for updated software revisins which do not introduce new features.

Images

Each device consists of different partitions. Some partitions may be read-only and some others read-write. The image field in the device manifest is a list of image manifests that should be parsed by the imagebuilder. The Image manifest syntax is documented below.

Linux Kernel Information

Sabaton builds the Linux Kernel using the Kernel build tools. The information here feeds the Kernel build process.

1## Linux Kernel information
2# Pull the kernel from this location
3kernel_src = "git@github.com:gregkh/linux.git"
4# Use this branch
5kernel_checkout = "1cd6e30b83d741562b55bf5b7763b1238a91150c"
6# Kernel configuration to use
7kernel_config = "defconfig"
8# Override the .config of the kernel (Optional)
9kernel_dot_config = "kernel/config.release"

Kernel Source

The kernel_src field is a giturl that points to the GIT repository of the Kernel. The kernel_checkout field points to the version of the repository to use. The kernel_config specifies the name of the configuration to use, and this can be further overridden by a kernel configuration that is part of the device tree.

Target

The target field indicates which Rust target to use for this device.

Flash Layout

The flash layout is described in a partition configuration file. The flash_layout field points to the actual partition configuration file. The syntax of partition configuration is described below.

Initial Ramdisk

Sabaton uses an initrial ramdisk based startup. The initrd prepares a consistent environment for the main image to start. It is an important variation point for hardware specific adaptations. Each board may have a unique initrd.

Image to partition mapping

Sabaton supports dual partitions. When building the manufacturing flash image, the same partition content may need to be replicated in two partitions. The partition mapping provides this information to imagebuilder.

Image Manifest

The Image Manifest describes the contents of each file-system of the device.

1defaults = [
2 { path = "/lib/*", mode=0o555, uid=0, gid=0, xattrs=[ ["",""]]},
3 { path = "/etc/*", mode=0o555, uid=0, gid=0, xattrs=[ ["",""]]},
4 { path = ".*", mode=0o400, uid=0, gid=0, xattrs=[ ["",""]]},
5 ]
6
7[[img]]
8name = "system.ext2"
9version = "1.0.1"
10image_type = "ext2"
11fs_options = { blocks_count = 2048, log_block_size = 2, extra="", uuid="065bb6b6ab6111eba5c500155dd3df0d" }

Default attributes

The image manifest starts with a defaults array. If the file attributes for any of the files in the manifest are not specified, this array is looked up from top to bottom. If the file path matches in the path field, the attributes for the file are taking from the matching entry in this array.

This enables you to ensure that no file gets into the target without the attributes being specified for it. The build is aborted if a matching entry is not found in the defaults section.

The image manifest header

The manifest starts with the [[img]] entry. Each image must have the following fields set.

FieldSupported valuesDescription
nameany identifierThe name of this image. This name can be referenced in the image_to_partition section of the device manifest
version"X.X.X"Version of this image
image_type"ext2","ext4","fat32", "tar"The image type to generate
fs_options.blocks_countintegernumber of blocks in the filesystem
fs_options.log_block_size2 -> 4K Blocks, 1-> 1K BlocksThis value may be overridden by the value set in the partition manifest
fs_options.uuidUUID stringThis is the UUID to be used for filesystem

Image contents

Following the manifest header, you can have as many image entries as you need. The following entries are supported. For all entries, the mode, uid, gid and xattrs are optional. When not specified, the corresponding value is used from the defaults array.

Directory

1# A directory
2[[img.dir]]
3name = "/sbin"
4mode = 0o555 # Optional
5uid = 0 # Optional
6gid = 0 # Optional
7xattrs = [ ["key","value"]] # Optional

This creates a directory called /sbin in the image.

File

1[[img.file]]
2name = "/usr/lib/softhsm/libsofthsm2.so"

The above entry will add the libsofthsm2.so file to the filesystem at the specified path. During the build, imagebuilder will search for this path in the build environment, searching in the following locations with this order.

  1. The device manifest directory
  2. The device target directory
  3. The target directory
  4. The toolchain sysroot

You can also specify an absolute path for the source by using the source field as below.

1[[img.file]]
2name = "/usr/lib/softhsm/libsofthsm2.so"
3source = "files/lib/override/libsofthsm2.so" # Use this as the source

Inline File

1[[img.ifile]]
2name = "/etc/passwd"
3mode = 0o400
4source = '''
5root:x:0:0:root:/:/usr/bin/zsh
6daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
7'''

The above entry creates the /etc/passwd file. The content of the file is specified in the source field. This is convenient to create configuration files.

1
2[[img.link]]
3name = "/lib/libsystemd.so.0"
4source = "/lib/libsystemd.so.0.31.0"

Devices

1[[img.dev]]
2name = "/dev/devname"
3major = 5
4minor = 0
5device_type = "c" # Can be "c","b","f", "s" for char, block, Fifo or Socket

Rust Crates

You can directly inject Rust application crates into the image.

1[[img.crate]]
2name = "/sbin/vpncloud"
3src_uri = { git = "https://github.com/dswd/vpncloud.git"}
4executables = [
5 {source = "vpncloud", target = "/sbin/vpncloud"}
6]
7mode = 0o555

The above entry downloads and builds the vpncloud crate before injecting it into the image. Since we haven’t specified any other information, imagebuilder assumes that we intend to use the latest commit on the main branch to build our package. You can combine the git key with the rev, tag, or branch keys to specify something else.

The name field in the crate entry is a placeholder and does not affect the image.

Stripping ELF binaries

Image builder supports stripping of ELF binaries before injecting into the image.

1[[img.file]]
2name = "/lib/systemd/systemd"
3strip = true

The above entry will strip the systemd executable before injecting into the image. The strip utility of the correct target toolchain will be used. Strip will be ignored if the file is not an ELF binary.

Partition Manifest

The Partition Manifest describes the partition layout of the device. Here is an example of the layout for a 512MB flash devive. The partition table is GUID based (GPT). The partition manifest is referenced in the device manifest by the flash_layout field of the device manifest.

1# Partition information for device
2# 512MB Flash device
3[flash]
4size = "512MiB"
5# Logical block size. Only 512 and 4096 are supported.
6lbs = "Lb4096"
7guid = "eb01b55a-227f-11ec-bc73-571d43468c90"
8
9[[flash.partition]]
10# Size in bytes
11name = "boot_a"
12size = "50MiB"
13partition_type = "EFI"
14flags = 0
15
16[[flash.partition]]
17# Size in bytes
18name = "boot_b"
19size = "50MiB"
20partition_type = "EFI"
21flags = 0
22
23[[flash.partition]]
24# Size in bytes
25name = "settings_a"
26size = "4MiB"
27partition_type = "Linux_FS"
28flags = 0
29
30[[flash.partition]]
31# Size in bytes
32name = "settings_b"
33size = "4MiB"
34partition_type = "Linux_FS"
35flags = 0
36
37[[flash.partition]]
38# Size in bytes
39name = "firmware_a"
40size = "4MiB"
41partition_type = "Linux_FS"
42flags = 0
43
44[[flash.partition]]
45# Size in bytes
46name = "firmware_b"
47size = "4MiB"
48partition_type = "Linux_FS"
49flags = 0
50
51[[flash.partition]]
52# Size in bytes
53name = "vbmeta_a"
54size = "1MiB"
55partition_type = "Linux_FS"
56flags = 0
57
58[[flash.partition]]
59# Size in bytes
60name = "vbmeta_b"
61size = "1MiB"
62partition_type = "Linux_FS"
63flags = 0
64
65[[flash.partition]]
66# Size in bytes
67name = "system_a"
68size = "100MiB"
69partition_type = "Linux_FS"
70flags = 0
71
72[[flash.partition]]
73# Size in bytes
74name = "system_b"
75size = "100MiB"
76partition_type = "Linux_FS"
77flags = 0
78
79[[flash.partition]]
80# Size in bytes
81name = "vendor_a"
82size = "50MiB"
83partition_type = "Linux_FS"
84flags = 0
85
86[[flash.partition]]
87# Size in bytes
88name = "vendor_b"
89size = "50MiB"
90partition_type = "Linux_FS"
91flags = 0
92
93[[flash.partition]]
94# Size in bytes
95name = "data"
96size = "30MiB"
97partition_type = "Linux_FS"
98flags = 0