The kernel itself directly launches one program in one user process at boot. That first process, and the program running in it, are called “userboot”.
This library facilitates writing userboot programs. It supports writing specific userboot programs to do whatever you want them to do. The library is not much involved in what userboot does. It takes care of how it can do anything in the special circumstances of what and where userboot is.
The userboot program is a single ELF file. Unlike all other user-space programs stored in a bootable ZBI, userboot is packed into the STORAGE_KERNEL ZBI item along with kernel binaries as part of the kernel ZBI. Normal userland is instead packed into the STORAGE_BOOTFS ZBI item that‘s combined with the kernel ZBI during the product assembly phase. (It’s userboot itself that‘s responsible for unpacking the BOOTFS item, so it can’t be inside there!)
The file is found inside the “kernel package” selected by the kernel.select boot option. In the build system, this means its executable() should be in deps of a kernel_package(). Then that goes into deps of a kernel_image().
The kernel.select.userboot boot option sets the file name to be found inside the selected kernel package; by default it's just userboot, but it can be any relative path matching the distribution_entries metadata used in a target that went into deps of kernel_package().
The kernel loads userboot as a mostly-normal ELF file, with some restrictions.
It must be a self-contained, statically linked executable (a static PIE): it cannot use a PT_INTERP (separate dynamic linker), nor have any DT_NEEDED dependencies other than the vDSO.
It must not have PT_LOAD segments that overlap in the file. This is a constraint on the link-time layout that is ordinarily met by binaries linked for Fuchsia, but is required by neither ELF nor the system program loader.
Like any Zircon process, userboot is started with a single handle from which it bootstraps all other capabilities and information it needs. The kernel passes a channel handle on which it sends two messages (not necessarily queued yet when the process starts). Each message contains only handles. There is no additional data about the handles, and no specified order or number of handles in each message. Each handle's purpose can be identified from its object type and the details available from queries via that handle (zx_object_get_info, etc.). The distinction between these two messages is reflected in the structure of the library.
The first message contains the few essential handles describing the userboot process itself. The userboot library provides custom startup code for libc that uses this message. The message has already been read to initialize library state before any C++ static constructors or the userboot program's main function run.
This message is kept separate precisely because of this topical separation: it's just about bootstrapping the userboot program. The userboot library handles this part completely.
The second and final message contains all the handles that constitute system-wide privilege:
zbi: The ZBI, where CMDLINE, STORAGE_BOOTFS, etc. are found.vdso/*: The various vDSO images as ELF in VMOs blessed to make syscalls./kernel/... files.The library does not consume this message. Instead, the library‘s startup support API hands the channel off to the program after reading the process capability. It’s entirely up to the program to read and decode the system capability message and bootstrap the whole system. The library doesn't impose any data structures to represent this, nor do any allocation to hold the message or its representation. If the details of this message change in the future, that will be between the kernel and userboot programs, without changes to the userboot library or its APIs.
This library facilitates writing a userboot program. In the build system, deps on the userboot library automatically propagate the use of pure static linking via the //sdk/lib/c:static target. (Note the userboot library target must be in the direct deps of the executable() GN target, or treated as such via public_deps propagation. Otherwise, it may be necessary to add a direct dependency on ...:static.)
The userboot library and the C library together take care of process startup so that things look somewhat “normal” to the userboot program. The usual main function is called as in any other program with the canonical signatures available. However, there are never any arguments or environment (such that argc == 0). So there's no reason to use a main signature that takes any arguments.
When main returns it will do the normal things just as when exit is called: run any destructors or atexit hooks, etc; then call zx_process_exit with the exit code (return value from main or argument to exit). However, nothing ever notices the exit code. If userboot crashes rather than exiting, the kernel will print exception details with a register dump, etc. But if userboot exits intentionally, the kernel considers this “normal”:
In short, the main “startup” support is just that the C library pretty much all works normally and is available to be used in normal ways. (The main caveats are just the context: there are no other processes on the system to provide any services; and only libraries that support full static linking can be used, e.g. there is no fdio---as well as nothing for it to talk to.) main should return (or lead to calling exit or _exit, etc.) either when it's done and other things are running now; or with no other process running when in a bad panic or minimal testing scenario without proper drivers to manage full-system shutdown.
<lib/userboot/startup.h> defines what little API there is for startup support per se: The single function TakeBootstrapChannel() returns the startup channel handle, transferring ownership to the caller. That channel is expected to get the system capability message from the kernel, and no further messages (the kernel closes its end after sending the message). There is no guarantee that the message has already been sent or the peer (kernel) side closed by the time the channel is handed over. The channel signals must be waited for as usual.
This simple API uses C linkage and a trivial signature so it can be used easily from C, C++, or Rust or other languages with even minimal C interoperation.
So far the library has no additional utility APIs. If there are common pieces that should be reused across different userboot programs but are very specific to the niche of userboot work, this is a natural place to add them.
Other libraries not specific to the userboot context support ZBI decoding, decompression, BOOTFS format, process management, fuchsia.ldsvc protocol implementation, ELF loading, etc.
One big feature of userboot programs being “somewhat normal” is that they can be tested and debugged somewhat normally as well. Aside from the special properties and privileges of particular resource objects, a userboot program running in a normal sandbox without special privileges works much the same as if it had been launched by the kernel at boot time. It‘s not alone on the system. But it only knows about the task hierarchy under the job it’s given as “the root job”. It only knows what resource and VMO handles it's given and what queries on those handles report about them, not what kernel or hardware privilege they truly confer.
The userboot-testing library provides support for writing Fuchsia test components with gtest that exercise userboot programs in sandbox environments.
The API provides for:
Fetching a userboot ELF file under test from the test package.
Creating an empty job to stand in for the root job for the life of a test.
Launching a userboot test process in a sandbox, including:
This allows the test logic to concentrate on the userboot “business logic”: what it expects in the system capability message; and what it does with that.