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]]
3 name = "QEMU-sysd"
4 device_id = "c7892a10-9217-11ec-b59d-0242ac110002"
5 version = "0.0.1"
6 # Use these image files
7 images = ["image.toml","vendor.toml","data.toml"]
8 ## Linux Kernel information
9 # Pull the kernel from this location
10 kernel_src = "git@github.com:gregkh/linux.git"
11 # Use this branch
12 kernel_checkout = "1cd6e30b83d741562b55bf5b7763b1238a91150c"
13 # Kernel configuration to use
14 kernel_config = "defconfig"
15 # Override the .config of the kernel (Optional)
16 kernel_dot_config = "kernel/config.release"
17
18 # This kernel can support the folling targets.
19 target = "aarch64-unknown-linux-gnu"
20 flash_layout = "partition.toml"
21 initrd = { image="initrd.toml", target="aarch64-unknown-linux-musl"}
22
23 image_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.
30 verity_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.
37 verity_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.
- 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.
- Increment the minor version for updated software revisions which introduce new features.
- 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
3 kernel_src = "git@github.com:gregkh/linux.git"
4 # Use this branch
5 kernel_checkout = "1cd6e30b83d741562b55bf5b7763b1238a91150c"
6 # Kernel configuration to use
7 kernel_config = "defconfig"
8 # Override the .config of the kernel (Optional)
9 kernel_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.
1 defaults = [
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]]
8 name = "system.ext2"
9 version = "1.0.1"
10 image_type = "ext2"
11 fs_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.
Field | Supported values | Description |
---|---|---|
name | any identifier | The 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_count | integer | number of blocks in the filesystem |
fs_options.log_block_size | 2 -> 4K Blocks, 1-> 1K Blocks | This value may be overridden by the value set in the partition manifest |
fs_options.uuid | UUID string | This 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]]
3 name = "/sbin"
4 mode = 0o555 # Optional
5 uid = 0 # Optional
6 gid = 0 # Optional
7 xattrs = [ ["key","value"]] # Optional
This creates a directory called /sbin
in the image.
File
1 [[img.file]]
2 name = "/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.
- The device manifest directory
- The device target directory
- The target directory
- The toolchain sysroot
You can also specify an absolute path for the source by using the source field as below.
1 [[img.file]]
2 name = "/usr/lib/softhsm/libsofthsm2.so"
3 source = "files/lib/override/libsofthsm2.so" # Use this as the source
Inline File
1 [[img.ifile]]
2 name = "/etc/passwd"
3 mode = 0o400
4 source = '''
5 root:x:0:0:root:/:/usr/bin/zsh
6 daemon: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.
Links
1
2 [[img.link]]
3 name = "/lib/libsystemd.so.0"
4 source = "/lib/libsystemd.so.0.31.0"
Devices
1 [[img.dev]]
2 name = "/dev/devname"
3 major = 5
4 minor = 0
5 device_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]]
2 name = "/sbin/vpncloud"
3 src_uri = { git = "https://github.com/dswd/vpncloud.git"}
4 executables = [
5 {source = "vpncloud", target = "/sbin/vpncloud"}
6 ]
7 mode = 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]]
2 name = "/lib/systemd/systemd"
3 strip = 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]
4 size = "512MiB"
5 # Logical block size. Only 512 and 4096 are supported.
6 lbs = "Lb4096"
7 guid = "eb01b55a-227f-11ec-bc73-571d43468c90"
8
9 [[flash.partition]]
10 # Size in bytes
11 name = "boot_a"
12 size = "50MiB"
13 partition_type = "EFI"
14 flags = 0
15
16 [[flash.partition]]
17 # Size in bytes
18 name = "boot_b"
19 size = "50MiB"
20 partition_type = "EFI"
21 flags = 0
22
23 [[flash.partition]]
24 # Size in bytes
25 name = "settings_a"
26 size = "4MiB"
27 partition_type = "Linux_FS"
28 flags = 0
29
30 [[flash.partition]]
31 # Size in bytes
32 name = "settings_b"
33 size = "4MiB"
34 partition_type = "Linux_FS"
35 flags = 0
36
37 [[flash.partition]]
38 # Size in bytes
39 name = "firmware_a"
40 size = "4MiB"
41 partition_type = "Linux_FS"
42 flags = 0
43
44 [[flash.partition]]
45 # Size in bytes
46 name = "firmware_b"
47 size = "4MiB"
48 partition_type = "Linux_FS"
49 flags = 0
50
51 [[flash.partition]]
52 # Size in bytes
53 name = "vbmeta_a"
54 size = "1MiB"
55 partition_type = "Linux_FS"
56 flags = 0
57
58 [[flash.partition]]
59 # Size in bytes
60 name = "vbmeta_b"
61 size = "1MiB"
62 partition_type = "Linux_FS"
63 flags = 0
64
65 [[flash.partition]]
66 # Size in bytes
67 name = "system_a"
68 size = "100MiB"
69 partition_type = "Linux_FS"
70 flags = 0
71
72 [[flash.partition]]
73 # Size in bytes
74 name = "system_b"
75 size = "100MiB"
76 partition_type = "Linux_FS"
77 flags = 0
78
79 [[flash.partition]]
80 # Size in bytes
81 name = "vendor_a"
82 size = "50MiB"
83 partition_type = "Linux_FS"
84 flags = 0
85
86 [[flash.partition]]
87 # Size in bytes
88 name = "vendor_b"
89 size = "50MiB"
90 partition_type = "Linux_FS"
91 flags = 0
92
93 [[flash.partition]]
94 # Size in bytes
95 name = "data"
96 size = "30MiB"
97 partition_type = "Linux_FS"
98 flags = 0