Bridging Worlds: Major Improvements to U-Boot’s EFI App and Device Tree Handling

The U-Boot EFI application is a powerful tool, allowing U-Boot to run on top of existing UEFI firmware. This is increasingly important as we see new platforms, especially in the ARM64 world, shipping with complex, ACPI-based firmware. A prime example is the recent wave of laptops powered by the Qualcomm X-Elite, which rely on this boot model.

However, booting a traditional, device-tree-based OS like ARM Linux from this environment presents a unique set of challenges. A recent comprehensive patch series lands with a host of fixes and new features designed to bridge the gap between the EFI world and the device tree world, dramatically improving the experience and reliability of the U-Boot EFI app.

Let’s dive into what’s new.


The Core Challenge: Memory Map Misalignment

When Linux boots using a device tree, it trusts the /reserved-memory nodes in the FDT to know which memory regions to avoid. But when running as an EFI application, the underlying UEFI firmware has its own, separate memory map with regions reserved for runtime services, ACPI data, and other critical functions.

If U-Boot doesn’t transfer this information into the device tree, Linux has no idea these regions are off-limits. It might try to use them, leading to mysterious crashes and instability.

The Solution: Automatic Memory Map Synchronization

Enter a new mechanism to synchronize these two views of memory. A new command, efi memsync, has been introduced.

This command:

  1. Reads the memory map provided by the EFI boot services.
  2. Parses the existing /reserved-memory nodes in U-Boot’s working device tree.
  3. Compares the two, identifying any reserved regions from EFI that are missing in the device tree.
  4. Automatically creates new no-map reserved-memory nodes in the device tree for these missing regions.

This process is now automatically run during the boot flow when using the EFI app, ensuring that the device tree passed to Linux is a complete and accurate representation of the hardware’s memory layout.

=> efi memsync -v
Comparing EFI memory-map with reserved-memory
EFI Memory Map Analysis:
ID   Type                  Start              End                In DT?
------------------------------------------------------------------------
0    EFI_RUNTIME_SERVICES_CODE 0x001f9ef000       0x001faef000       no -> adding
1    EFI_ACPI_RECLAIM_MEMORY   0x001fb6f000       0x001fb7f000       no -> adding
2    EFI_ACPI_MEMORY_NVS       0x001fb7f000       0x001fbff000       yes
Regions added: 2

New command: fdt reserved

To complement efi memsync, you can now easily inspect the result with the fdt reserved command. It provides a clean, readable list of all regions in the /reserved-memory node.

=> fdt reserved
ID   Name                 Start            Size
----------------------------------------------------------------
0    secmon@1f000000      0x1f000000         0x1000000
1    efi-runtime@1f9ef000 0x1f9ef000         0x100000

Stability and Polish

Beyond the major new features, this series includes critical fixes that make the EFI app more robust and stable:

  • Correct Device Naming: A subtle bug was fixed where block devices were being named based on a temporary placeholder, leading to confusing device names. Names are now set correctly before the device is bound.
  • ARM64 Exception Level Handling: The app now correctly avoids trying to change CPU exception levels, which it is not permitted to do, preventing crashes on ARM platforms.
  • Graceful Exit: The app no longer tries to free its own memory right before exiting, which could cause a crash. It now relies on the underlying UEFI firmware to clean up resources.
  • Simple Framebuffer: For a better user experience, the app now automatically adds a simple-framebuffer node to the device tree, ensuring Linux has an early graphics console.

Conclusion

This is a small step forward for U-Boot’s capabilities as an EFI application. These changes are already proving essential for bringing up Linux on platforms like the Qualcomm X-Elite, showcasing U-Boot’s adaptability and continued importance in a complex and evolving boot ecosystem.




Concept mailing list and patch management

If you are interested in watching the development of U-Boot Concept, consider joining the mailing list. You can do this in your web browser here, or send an email with ‘subscribe’ in the subject to concept-join@u-boot.org. If you are able to review patches or contribute your own, please do!

Also the patchwork server is up and running so future patches will be visible there. This also helps to collect reviews, etc.

You may also find it interesting to look at the merge requests.

Development goals

With U-Boot Concept, the goal is to continue the transformation of the code base so that it remains relevant in future, even if only as fodder for new developments. Keys goals remaining for 2025 are:

  • Continue evolution of standard boot
  • Get the EFI app fully functional on x86 and ARM
  • Broaden the expo feature set a little, e.g. a text editor
  • Keep up with the development of the Flat Image Tree (FIT) specification
  • Create packages for QEMU and EFI (including versions more focussed on secure boot)
  • Take a look at what it would take to support Rust code
  • Build out the new filesystem and file uclasses (better Linux compatibility?)

Not all of these will get done, but we’ll see what happens!




Fixing a Devious Memory Corruption Bug in Sandbox

We’ve just merged a fix in Concept for a particularly tricky memory corruption bug in the U-Boot sandbox environment. This bug was difficult to track down, so we wanted to share the story of the investigation and the solution.

The Symptom: Mysterious Heap Corruption

The problem first appeared as random and hard-to-reproduce memory corruption in the malloc() heap. These are often the most frustrating bugs to solve, as the place where the program crashes is rarely the place where the error actually occurred. After many hours of debugging, the root cause was finally identified, and it was quite unexpected.


The Culprit: A Memory Map Collision 💥

It turns out that the PCI Enhanced Allocation (EA) driver, a feature used for testing within sandbox, was mapping its simulated device memory at the 1MB physical address.

This might seem harmless, but there’s a problem. When using the vbe_abrec_os boot method in sandbox, the Linux kernel is also loaded and executed from that very same 1MB address.

The collision was triggered by a specific feature: measured boot. When enabled, the bootm_measure() command needs to read the kernel’s data to calculate its hash. To do this, it calls map_sysmem() on the kernel’s memory region at 1MB. However, because the PCI EA driver had claimed that address space, the call was “hijacked.” Instead of mapping and reading RAM where the kernel was supposed to be, U-Boot was unknowingly reading from the simulated PCI device!

This led to the measurement being performed on incorrect data and, through a complex chain of events, corrupted the heap, causing crashes later on.


The Fix: A Multi-Pronged Approach

The solution was implemented in a series of five patches that not only fix the bug but also add safeguards to prevent similar issues in the future.

1. Move the PCI Space

The core of the fix was to move the PCI EA test space out of the way. Patch 2/5 relocates it from 0x100000 (1MB) to a much safer address at 0x20000000, well clear of any system RAM used by sandbox. This resolves the memory collision entirely.

2. Make Partial Maps a Fatal Error

A key clue during debugging was this log message: map_physmem: Warning: partial map at 100000, wanted 4, got 2000

This warning indicated that a driver asked to map a certain number of bytes but the underlying device mapping had a different size. While this might sometimes be harmless, in this case, it was a giant red flag that something was wrong.

Patch 5/5 promotes this warning to a fatal error. It now prints a “Fatal: partial map…” message and calls os_abort(). This makes the system fail loudly and immediately, which is much better than silently continuing with a corrupted state. This change will make similar bugs far easier to spot in the future.

3. Clean up Mapping Sizes

With the new fatal error in place, we had to clean up a few drivers that were requesting inexact mapping sizes. Patches 3/5 and 4/5 update the ACPI PMC driver and the PCI EA tests to request the exact size of the memory they intend to map, silencing the new error and making the code more precise.

4. A Quick Logging Fix

Finally, patch 1/5 was a small but helpful cleanup, swapping the “wanted” and “got” values in the log message, which were reversed, to avoid any future confusion.

This was a great example of how a seemingly innocuous test configuration can have far-reaching and destructive consequences. By moving the test region and hardening our memory mapping code, the sandbox environment is now more robust. Happy hacking!




Making CI Work for You: New Controls for U-Boot’s GitLab Pipeline

Continuous Integration (CI) is the backbone of a large project like U-Boot, ensuring that every change is tested against a huge matrix of boards and configurations. While this comprehensive testing is vital for quality, it can also be time-consuming. When you’re focused on a specific feature, waiting for a full “world build” to complete can slow down your development cycle.

A little series of patches introduces several improvements to U-Boot’s GitLab CI, giving developers powerful new tools to get faster, more targeted feedback.


## A New Toolbox for CI Control

This series is all about empowering developers to tailor CI runs to their specific needs. Instead of a one-size-fits-all pipeline, you can now easily select which parts you want to run.

Stage-Level Control

You can now disable entire stages of the CI pipeline using simple variables. If your changes don’t affect the world build, you can just turn it off for your test push:

# Push to the 'ci' remote, disabling the world_build stage
$ git push ci -o ci.variable=WORLD_BUILD=0

Similar variables are available for the other main stages:

  • TEST_SUITES=0: Disables the collection of miscellaneous tests (pylint, docs, etc.).
  • TEST_PY=0: Disables all test.py jobs.

Granular test.py Selection

The test.py stage runs U-Boot on emulated hardware and is often the most critical part of the pipeline. The TEST_PY variable offers fine-grained control here. You can use it to run:

  • All tests for a single board: TEST_PY=sandbox
  • A single, specific test job: TEST_PY='sandbox with clang test.py'

This is incredibly useful when you’re working on a single board and want to avoid running tests for dozens of others.


Laser-Focused Testing with TEST_SPEC

For the ultimate level of precision, the new TEST_SPEC variable lets you pass a test specifier directly to pytest. This allows you to run a single test or a specific group of tests across all relevant jobs.

For example, if you’re working on the bootstd command, you can run only the bootstd tests in CI with:

# Run only tests with 'bootstd' in their name
$ git push ci -o ci.variable=TEST_SPEC=bootstd

This can reduce a test run from 30 minutes to just a few minutes, providing a rapid feedback loop for iterative development.


A Cleaner, More Consistent CI

Beyond adding new features, the series also includes cleanups that make the CI configuration more consistent and maintainable. Stage names like world build have been standardized to world_build, and redundant shell logic has been removed in favor of letting GitLab’s own rules: engine handle job execution.

These small changes improve the readability and reliability of the CI infrastructure, making life easier for both developers and maintainers. By giving developers more control over their CI runs, these updates promise to make the development process faster and more efficient.




Taming Build Complexity: Introducing Config Fragments in Buildman

As U-Boot’s support for hardware grows, so does the complexity of managing build configurations. A single board might require several build variations—for example, one with network support and one without, or a standard build versus one tailored for Android booting. Historically, managing these variations often meant duplicating large defconfig files, a maintenance headache waiting to happen.

A recent patch series, with contributions from Heinrich Schuchardt and Simon Glass, introduces a powerful and elegant solution to this problem by formally integrating configuration fragments into buildman, U-Boot’s indispensable build-testing tool.


What are Config Fragments?

A config fragment is a small, targeted file containing just a few Kconfig options. For instance, an acpi.config fragment might contain only one line:

CONFIG_ACPIGEN=y

Instead of creating a whole new qemu-riscv64_smode_acpi_defconfig, you can now apply this small fragment on top of the existing qemu-riscv64_smode_defconfig to generate a new build variant on the fly. This approach is cleaner, reduces duplication, and makes configurations easier to manage.

While the underlying Kconfig system has supported this for a while, buildman was unaware of this mechanism—until now.


Official buildman Support: Two New Approaches

The new series enhances buildman by providing two ways to work with fragments.

The Manual Approach: --fragments

For quick tests or one-off builds, buildman now has a --fragments command-line option. You can pass a comma-separated list of fragment files to apply to your build.

For example, to build qemu-riscv64_smode with ACPI support, you can now run:

tools/buildman/buildman -k qemu-riscv64_smode --fragments acpi.config

buildman handles the rest, correctly merging the fragment with the board’s base defconfig.


The Automated Build Matrix: --extend and .buildman

The real impact of this series lies in its ability to automate the discovery and building of these variations. This is achieved through a new file format and a new command-line flag.

A new file, configs/extended.buildman, now defines the relationships between boards and their valid fragments, creating what are called “extended boards.”

The syntax is simple and powerful. You can define a build variant and specify which boards it applies to using wildcards or by checking for specific CONFIG options in the base defconfig.

Here’s an example from the series that creates ACPI-enabled variants for all RISC-V QEMU boards:

# Build RISC-V QEMU builds with ACPI
name: acpi
fragment: acpi
targets:
  qemu-riscv*

To tell buildman to parse this file and build all the resulting extended boards, you simply add the -X (or --extend) flag to your command. buildman will then discover that acpi,qemu-riscv64_smode is a valid new target and add it to its build queue.


Continuous Integration for All Variants

This isn’t just a tool for local development. The final piece of this series integrates the -X flag directly into U-Boot’s GitLab CI. This means that these “extended board” configurations are now built and tested automatically as part of the “world build,” guaranteeing that build variations are treated as first-class citizens and don’t suffer from bit-rot. This last patch is pending, due to some last-minute issues.

By formalizing the concept of config fragments, this series makes the U-Boot build system more robust, easier to maintain, and better equipped to handle the diverse configurations required by the embedded ecosystem.




QEMU improvements

Since 2018 U-Boot has had a good selection of features for running on top of QEMU, including:

  • virtio using PCI devices (legacy and modern)
  • virtio using memory-mapped I/O
  • block devices, to support filesystems, etc. (virtio-blk)
  • network devices (virtio-net)
  • random-number device (virtio-rng)

Most of this was written by Bin Meng. It uses driver model and is nicely implemented.

What’s new?

More recently a few more features have been added:

  • SCSI devices, for more flexible discovery with multiple disks (virtio-scsi)
  • Filesystem devices, for access to host files (virtio-fs). See the separate post about this.
  • Visibility into the available virtio devices (virtio list)
  • Additions to the qfw command to improve visibility

The `virtio list` command can be useful for seeing what paravirtualised devices are available and whether U-Boot has a driver for them. Here you can see U-Boot running on an x86 host.

=> virtio scan
=> virtio list
Name                  Type            Driver
--------------------  --------------  ---------------
virtio-pci.m#0         5: balloon     (none)
virtio-pci.m#1         4: rng         virtio-rng#1
virtio-pci.m#2        12: input-host  (none)
virtio-pci.m#3        12: input-host  (none)
virtio-pci.m#4        13: vsock       (none)
virtio-pci.m#5         3: serial      (none)
virtio-pci.m#6         8: scsi        virtio-scsi#6
virtio-pci.m#7         9: 9p          (none)
virtio-pci.m#8        1a: fs          virtio-fs#8
virtio-pci.m#9        10: gpu         (none)
virtio-pci.m#10        1: net         virtio-net#a
=>

Here you can see how the random-number driver can be used:

=> random 1000 10
16 bytes filled with random data
=> md.b 1000 10
00001000: 00 3f e2 f8 a1 70 4e 5f 8c 19 19 ba 18 76 32 bc  .?...pN_.....v2.
=> 

SCSI is accessed by scanning it first. Note that standard boot does this automatically, if you are just booting an OS.

=> scsi scan
scanning bus for devices...
  Device 0: (0:1) Vendor: QEMU Prod.: QEMU HARDDISK Rev: 2.5+
            Type: Hard Disk
            Capacity: 10240.0 MB = 10.0 GB (20971520 x 512)
=> part list scsi 0

Partition Map for scsi device 0  --   Partition Type: EFI

Part	Start LBA	End LBA		Name
	Attributes
	Type GUID
	Partition GUID
  1	0x00200800	0x013fffde	""
	attrs:	0x0000000000000000
	type:	0fc63daf-8483-4772-8e79-3d69d8477de4
		(linux)
	guid:	e53def26-b3a7-4227-8175-b933282b824f
  e	0x00000800	0x000027ff	""
	attrs:	0x0000000000000000
	type:	21686148-6449-6e6f-744e-656564454649
		(21686148-6449-6e6f-744e-656564454649)
	guid:	a52718a3-62a4-483a-b8fa-38cefacad2fd
  f	0x00002800	0x000377ff	""
	attrs:	0x0000000000000000
	type:	c12a7328-f81f-11d2-ba4b-00a0c93ec93b
		(EFI System Partition)
	guid:	077b8491-26d1-4984-a86f-2c8674c438ee
 10	0x00037800	0x00200000	""
	attrs:	0x0000000000000000
	type:	bc13c2ff-59e6-4262-a352-b275fd6f7172
		(bc13c2ff-59e6-4262-a352-b275fd6f7172)
	guid:	3b088c0d-2f7b-4d92-b7c0-561d0e2cdd30
=> 

You can also inspect some of the qfw tables directly. The qfw list command has been around for a while, although some minor updates were added recently. It shows the files that QEMU presents to U-Boot:

=> qfw list 
    Addr     Size Sel Name
-------- -------- --- ------------
       0        0  20 bios-geometry                                           
       0       6d  21 bootorder                                               
3fcab000       14  22 etc/acpi/rsdp                                           
3fcad000    20000  23 etc/acpi/tables                                         
       0        4  24 etc/boot-fail-wait                                      
       0       28  25 etc/e820                                                
       0        8  26 etc/msr_feature_control                                 
       0       18  27 etc/smbios/smbios-anchor                                
       0      169  28 etc/smbios/smbios-tables                                
       0        1  29 etc/smi/features-ok                                     
       0        8  2a etc/smi/requested-features                              
       0        8  2b etc/smi/supported-features                              
       0        6  2c etc/system-states                                       
       0     1000  2d etc/table-loader                                        
       0        0  2e etc/tpm/log                                             
       0        8  2f etc/vmgenid_addr                                        
3fcac000     1000  30 etc/vmgenid_guid                                        
       0     2400  31 genroms/kvmvapic.bin 

Low-level features

The new qfw table command can be useful to seeing exactly how the ACPI tables are provided:

=> qfw table
  0 alloc: align 10 zone fseg name 'etc/acpi/rsdp'
  1 alloc: align 1000 zone high name 'etc/vmgenid_guid'
  2 alloc: align 40 zone high name 'etc/acpi/tables'
  3 add-chksum offset 49 start 40 length 30f7 name 'etc/acpi/tables'
  4 add-ptr offset 315b size 4 dest 'etc/acpi/tables' src 'etc/acpi/tables'
  5 add-ptr offset 315f size 4 dest 'etc/acpi/tables' src 'etc/acpi/tables'
  6 add-ptr offset 31c3 size 8 dest 'etc/acpi/tables' src 'etc/acpi/tables'
  7 add-chksum offset 3140 start 3137 length f4 name 'etc/acpi/tables'
  8 add-chksum offset 3234 start 322b length 120 name 'etc/acpi/tables'
  9  10 add-ptr offset 3375 size 4 dest 'etc/acpi/tables' src 'etc/vmgenid_guid'
 11 add-chksum offset 3354 start 334b length ca name 'etc/acpi/tables'
 12 add-chksum offset 341e start 3415 length 38 name 'etc/acpi/tables'
 13 add-chksum offset 3456 start 344d length 208 name 'etc/acpi/tables'
 14 add-chksum offset 365e start 3655 length 3c name 'etc/acpi/tables'
 15 add-chksum offset 369a start 3691 length 28 name 'etc/acpi/tables'
 16 add-ptr offset 36dd size 4 dest 'etc/acpi/tables' src 'etc/acpi/tables'
 17 add-ptr offset 36e1 size 4 dest 'etc/acpi/tables' src 'etc/acpi/tables'
 18 add-ptr offset 36e5 size 4 dest 'etc/acpi/tables' src 'etc/acpi/tables'
 19 add-ptr offset 36e9 size 4 dest 'etc/acpi/tables' src 'etc/acpi/tables'
 20 add-ptr offset 36ed size 4 dest 'etc/acpi/tables' src 'etc/acpi/tables'
 21 add-ptr offset 36f1 size 4 dest 'etc/acpi/tables' src 'etc/acpi/tables'
 22 add-ptr offset 36f5 size 4 dest 'etc/acpi/tables' src 'etc/acpi/tables'
 23 add-chksum offset 36c2 start 36b9 length 40 name 'etc/acpi/tables'
 24 add-ptr offset 10 size 4 dest 'etc/acpi/rsdp' src 'etc/acpi/tables'
 25 add-chksum offset 8 start 0 length 14 name 'etc/acpi/rsdp'

Since QEMU can also provide SMBIOS tables, these can be inspected using the smbios command:

=> smbios
SMBIOS 3.0.0 present.
9 structures occupying 361 bytes
Table at 0x3fcdc018

Handle 0x0100, DMI type 1, 27 bytes at 0x3fcdc018
System Information
	Manufacturer: QEMU
	Product Name: Standard PC (Q35 + ICH9, 2009)
	Version: pc-q35-8.2
	Serial Number: 
	UUID: 8967d155-8cbb-484f-9246-d2c4eeeedff1
	Wake-up Type: Power Switch
	SKU Number: 
	Family: 

Handle 0x0200, DMI type 2, 15 bytes at 0x3fcdc063
Baseboard Information
	Manufacturer: Canonical Ltd.
	Product Name: LXD
	Version: pc-q35-8.2
	Serial Number: 
	Asset Tag: 
	Feature Flags: 0x01
	Chassis Location: 
	Chassis Handle: 0x0300
	Board Type: Motherboard
	Number of Contained Object Handles: 0x00

Handle 0x0300, DMI type 3, 22 bytes at 0x3fcdc091
Baseboard Information
	Manufacturer: QEMU
	Type: 0x01
	Version: pc-q35-8.2
	Serial Number: 
	Asset Tag: 
	Boot-up State: Safe
	Power Supply State: Safe
	Thermal State: Safe
	Security Status: Unknown
	OEM-defined: 0x00000000
	Height: 0x00
	Number of Power Cords: 0x00
	Contained Element Count: 0x00
	Contained Element Record Length: 0x00
	SKU Number: 

Handle 0x0400, DMI type 4, 48 bytes at 0x3fcdc0b8
Processor Information:
	Socket Designation: CPU 0
	Processor Type: Central Processor
	Processor Family: Other
	Processor Manufacturer: QEMU
	Processor ID word 0: 0x000a06a4
	Processor ID word 1: 0x0f8bfbff
	Processor Version: pc-q35-8.2
	Voltage: 0x00
	External Clock: 0x0000
	Max Speed: 0x07d0
	Current Speed: 0x07d0
	Status: 0x41
	Processor Upgrade: Other
	L1 Cache Handle: 0xffff
	L2 Cache Handle: 0xffff
	L3 Cache Handle: 0xffff
	Serial Number: 
	Asset Tag: 
	Part Number: 
	Core Count: 0x16
	Core Enabled: 0x16
	Thread Count: 0x16
	Processor Characteristics: 0x0002
	Processor Family 2: Other
	Core Count 2: 0x0016
	Core Enabled 2: 0x0016
	Thread Count 2: 0x0016
	Thread Enabled: 0x5043

Handle 0x1000, DMI type 16, 23 bytes at 0x3fcdc0ff
Header and Data:
	00000000: 10 17 00 10 01 03 06 00 00 10 00 fe ff 01 00 00
	00000010: 00 00 00 00 00 00 00

Handle 0x1100, DMI type 17, 40 bytes at 0x3fcdc118
Header and Data:
	00000000: 11 28 00 11 00 10 fe ff ff ff ff ff 00 04 09 00
	00000010: 01 00 07 02 00 00 00 02 00 00 00 00 00 00 00 00
	00000020: 00 00 00 00 00 00 00 00
Strings:
	String 1: DIMM 0
	String 2: QEMU

Handle 0x1300, DMI type 19, 31 bytes at 0x3fcdc14d
Header and Data:
	00000000: 13 1f 00 13 00 00 00 00 ff ff 0f 00 00 10 01 00
	00000010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

Handle 0x2000, DMI type 32, 11 bytes at 0x3fcdc16e
Header and Data:
	00000000: 20 0b 00 20 00 00 00 00 00 00 00

Handle 0x7f00, DMI type 127, 4 bytes at 0x3fcdc17b
End Of Table

You can see above that these are created by QEMU, under control of LXD. Thus the environment which U-Boot sees can be controlled by QEMU or by tools which integrate QEMU.

What’s next?

U-Boot has a fairly solid set of QEMU features at this point. It provides an alternative to OVMF in some cases, with a faster boot, while still using EFI. Future work may include looking at booting without EFI, thus saving time and reducing complexity.




Host-file Access with New virtio-fs

What is virtio-fs?

For those unfamiliar, virtio-fs is a modern shared filesystem designed specifically for virtualised environments. It allows a virtual machine (the “guest”) to access a directory on the host system, but it does so with a focus on performance and providing local filesystem semantics.

Unlike traditional methods like network filesystems (e.g., NFS, Samba) or even the older virtio-9p protocol, virtio-fs is engineered to take advantage of the fact that the guest and host are running on the same machine. By leveraging shared memory and a design based on FUSE (Filesystem in Userspace), it bypasses much of the communication overhead that can slow down other solutions. The result is a faster, more seamless file sharing experience that is ideal for development, testing, and booting from a root filesystem located on the host.

virtio-fs arrives in U-Boot Concept

The recent merge request in U-Boot Concept introduces a new virtio-fs driver within U-Boot. This initial implementation enables two key functions:

  • List directories on the host
  • Read files from the host

This is made possible by a new filesystem driver that integrates with U-Boot’s new FS, DIR, and FILE uclasses. A compatibility layer is included so that existing command-line functionalities continue to work as expected.

This new capability in U-Boot opens up more flexible and efficient workflows. For example, developers can now more easily load kernels, device tree blobs, or other artifacts directly from their development workstation into a QEMU guest running U-Boot, streamlining the entire test and debug cycle. For cloud use cases, reading configuration files from via virtio-fs is a common requirement.

Overall this lays a strong foundation for future enhancements to virtio-fs support within U-Boot, promising even tighter integration between guest environments and the host system.




Keeping Our Linker Lists in Line

U-Boot makes extensive use of linker-generated lists to discover everything from drivers to commands at runtime. This clever mechanism allows developers to add new features with a single macro, and the linker automatically assembles them into a contiguous array. The C code can then iterate through this array by finding its start and end markers, which are also provided by the linker.

For this to work, there’s a critical assumption: the array of structs is perfectly contiguous, with each element having the exact same size. But what happens when the linker, in its quest for optimisation, breaks this assumption?

A Little Wrinkle

We have known for a while about a subtle issue where the linker, in certain cases, would insert a few bytes of padding between elements in these lists. This is usually done to align the next element to a more efficient memory boundary (like 8 or 16 bytes).

While this is often harmless, it breaks U-Boot’s C code, which expects to find the next element by simply adding a fixed size to the address of the current one. This unexpected padding can lead to misaligned memory access, corrupted data, and hard-to-debug crashes.

Here is an example of what this looks like in the symbol table. Notice the gap between virtio_fs and virtio_fs_dir is 0x80 bytes, while the expected size is 0x78:

...
00000000011d0070 D _u_boot_list_2_driver_2_virtio_blk
00000000011d0160 D _u_boot_list_2_driver_2_virtio_fs
00000000011d01e0 D _u_boot_list_2_driver_2_virtio_fs_dir
...

This 8-byte padding (0x80 - 0x78) is the source of the problem.

A Script to the Rescue

To catch these alignment problems automatically, we’ve developed a new Python script, check_list_alignment.py, now in U-Boot Concept (merge).

The script works as follows:

  1. Runs nm -n on the final u-boot ELF file to get all symbols sorted by address.
  2. Automatically discovers all the different linker lists in use (e.g., driver, cmd, uclass_driver).
  3. For each list, calculates the gap between every consecutive element.
  4. Determines the most common gap size, assuming this is the correct sizeof(struct).
  5. Flags any gap that doesn’t match this common size.

Now, if the linker introduces any unexpected padding, the build will fail immediately with a clear error message:

$ ./scripts/check_list_alignment.py -v u-boot
List Name           # Symbols   Struct Size (hex)
-----------------   -----------   -----------------
...
driver                       65              0x78
  - Bad gap (0x80) before symbol: _u_boot_list_2_driver_2_virtio_fs_dir
...

FAILURE: Found 1 alignment problems

This simple check provides a powerful guarantee. It ensures the integrity of our linker lists, prevents a whole class of subtle bugs, and allows developers to continue using this powerful U-Boot feature with confidence.




Filesystems in U-Boot

U-Boot supports a fairly wide variety of filesystems, including ext4, ubifs, fat, exfat, zfs, btrfs. These are an important part of bootloader functionality, since reading files from bare partitions or disk offsets is neither scalable nor convenient.

The filesystem API is functional but could use an overhaul. The main interface is in fs/fs.c, which looks like this:

struct fstype_info {
	int fstype;
	char *name;
	/*
	 * Is it legal to pass NULL as .probe()'s  fs_dev_desc parameter? This
	 * should be false in most cases. For "virtual" filesystems which
	 * aren't based on a U-Boot block device (e.g. sandbox), this can be
	 * set to true. This should also be true for the dummy entry at the end
	 * of fstypes[], since that is essentially a "virtual" (non-existent)
	 * filesystem.
	 */
	bool null_dev_desc_ok;
	int (*probe)(struct blk_desc *fs_dev_desc,
		     struct disk_partition *fs_partition);
	int (*ls)(const char *dirname);
	int (*exists)(const char *filename);
	int (*size)(const char *filename, loff_t *size);
	int (*read)(const char *filename, void *buf, loff_t offset,
		    loff_t len, loff_t *actread);
	int (*write)(const char *filename, void *buf, loff_t offset,
		     loff_t len, loff_t *actwrite);
	void (*close)(void);
	int (*uuid)(char *uuid_str);
	/*
	 * Open a directory stream.  On success return 0 and directory
	 * stream pointer via 'dirsp'.  On error, return -errno.  See
	 * fs_opendir().
	 */
	int (*opendir)(const char *filename, struct fs_dir_stream **dirsp);
	/*
	 * Read next entry from directory stream.  On success return 0
	 * and directory entry pointer via 'dentp'.  On error return
	 * -errno.  See fs_readdir().
	 */
	int (*readdir)(struct fs_dir_stream *dirs, struct fs_dirent **dentp);
	/* see fs_closedir() */
	void (*closedir)(struct fs_dir_stream *dirs);
	int (*unlink)(const char *filename);
	int (*mkdir)(const char *dirname);
	int (*ln)(const char *filename, const char *target);
	int (*rename)(const char *old_path, const char *new_path);
};

At first glance this seems like a reasonable API. But where is the filesystem specified? The API seems to assume that this is already present somehow.

In fact there is a pair of separate functions responsible for selecting which filesystem the API acts on:

int fs_set_blk_dev(const char *ifname, const char *dev_part_str, int fstype)
int fs_set_blk_dev_with_part(struct blk_desc *desc, int part)

When you want to access a file, call either of these functions. It sets three ‘global’ variables, fs_dev_desc, fs_dev_part and fs_type . After each operation, a call to fs_close() resets things. This means you must select the block device before each operation. For example, see this code in bootmeth-uclass.c:

	if (IS_ENABLED(CONFIG_BOOTSTD_FULL) && bflow->fs_type)
		fs_set_type(bflow->fs_type);

	ret = fs_size(path, &size);
	log_debug("   %s - err=%d\n", path, ret);

	/* Sadly FS closes the file after fs_size() so we must redo this */
	ret2 = bootmeth_setup_fs(bflow, desc);
	if (ret2)
		return log_msg_ret("fs", ret2);

It is a bit clumsy. Obviously this interface is not set up to support caching. In fact the filesystem is mounted afresh each time it is accessed. In a bootloader this is normally not too much of a problem. Since the OS and associated files are normally packaged in a FIT, a single read is enough to obtain everything that is needed. But if multiple directories need to be searched to find that FIT, or if there are multiple files to read, the repeated mounting does slow things down.

If you have sharp eyes you might have seen another problem. The two functions above assume that they are dealing with a block device. In fact, struct blk_desc is the uclass-private data for a block device. What about when the filesystem is on the network? Also, with sandbox it is possible to access host files:

=> ls hostfs 0 /tmp/gimp
DIR    1044480 ..
DIR       4096 .
DIR       4096 2.10
=> 

Clearly, the files on the hostsystem are not accessed at the block level. How does that work?

The key to this is null_dev_desc_ok , which is true for the hostfs filesystem. There is a special case in the code to handle this.

int blk_get_device_part_str(const char *ifname, const char *dev_part_str,
			     struct blk_desc **desc,
			     struct disk_partition *info, int allow_whole_dev)
{
...
#if IS_ENABLED(CONFIG_SANDBOX) || IS_ENABLED(CONFIG_SEMIHOSTING)
	/*
	 * Special-case a pseudo block device "hostfs", to allow access to the
	 * host's own filesystem.
	 */
	if (!strcmp(ifname, "hostfs")) {
		strcpy((char *)info->type, BOOT_PART_TYPE);
		strcpy((char *)info->name, "Host filesystem");

		return 0;
	}
#endif

It isn’t great. I’ve been looking at virtio-fs lately, which also doesn’t use a block device.

There are other things that could be improved, too:

  • Filesystems must be specified explicitly by their device and partition number. It would be nice to have a unified ‘VFS’ like Linux (and Barebox) so filesystems could be mounted within a unified space.
  • Files cannot be accessed from a device, nor is there any way to maintain a reference to a file you are working with
  • Reading a file must done all at once, in most cases. It would be nice to have an interface to open, read and close the file.

Instead of adding yet more special cases, it may be time to overhaul the code a little.




Verified Boot for Embedded on RK3399

VBE has been a long-running project to create a smaller and faster alternative to EFI. It was originally introduced as a concept in 2022, along with a sandbox implementation and a simple firmware updater for fwupd.

In the intervening period an enormous about of effort has gone into getting this landed in U-Boot for a real board. This has resulted in 10 additional series, on top of the sandbox work:

  • A – Various MMC and SPL tweaks (14 patches, series)
  • B – binman: Enhance FIT support for firmware (20 patches, series)
  • C – binman: More patches to support VBE (15 patches, series)
  • D – A collection of minor tweaks in MMC and elsewhere (18 patches, series)
  • E – SPL improvements and other tweaks (19 patches, series)
  • F – VBE implementation itself, with SPL ‘relocating jump’ (22 patches, series)
  • G – VBE ‘ABrec’ implementation in TPL/SPL/VPL (19 patches, series)
  • H – xPL-stack cleanup (4 patches, series)
  • I – Convert rockchip to use Binman templates (7 patches, series), kindly taken over and landed by Jonas Karlman
  • J – Implementation for RK3399 (25 patches, series)

That’s a total of 163 patches!

The Firefly RK3399 board was chosen, since it has (just) enough SRAM and is fully open source.

The final series has not yet landed in the main tree and it is unclear whether it will. For now I have put it in the Concept tree. You can see a video of it booting below:

I have been thinking about why this took so long to (almost) land. Here is my list, roughly in order from most important to least:

  1. Each series had to land before the next could be sent, with it taking at least one release cycle (3 months) to land each one
  2. Some of the new features were difficult to implement, particularly the relocating SPL jump and the new Binman features
  3. Many of the patches seemed aimless or irrelevant when sent, since they had no useful purpose before VBE could fully land. This created resistance in review
  4. On the other hand, sending too many patches at once would cause people to ignore the series

Overall it was a very difficult process, even for someone who knows U-Boot well. It concerns me that it has become considerably harder to introduce major new things in U-Boot, compared to the days of sandbox or driver model. I don’t have much of a comparison with other firmware projects, but I’m interested in hearing other people’s point of view. Please add a comment if you have thoughts on this.

Anyway, I am pleased to be done with it. The only thing missing at present is ‘ABrec’ updates in fwupd. It should be fairly easy to do, but for the signature checking. Since fwupd has its own implementation of libfdt, that might be non-trivial.

More information on VBE: