To minimize writes to the ESP but still find necessary changes, compare
the hashes of the files on the ESP with the "expected" hashes. Only copy
and overwrite already existing files if the hashes don't match. This
ensures a working-as-expected state on the ESP as opposed to previously
where already existing files were just ignored.
Previously, generations were installed one after another. Now all
artifacts (kernels, initrd etc.) are first collected and then installed.
This way the writes to the ESP are reduced as duplicate paths are
already removed in the collection phase.
Using random names for tempfiles makes handling them easier. It reduces
the amount of noise in the code because no custom name needs to be
provided for each tempfile. The names were not really useful in any
case.
It also does not burden the developer with ensuring uniqueness of names.
This is relevant when files for multiple generations need to be stored
in the same directory (e.g. because they need to be accessed after
handling one generation).
Out of an abundance of caution, 32 random alphanumeric characters are
chosen for each filename. The tempfile crate, in comparison, only
chooses 8. 32 characters should be enough to avoid collisions, even
if the PRNG is not of cryptographic quality.
Leverage the bootspec `label` field in its intended way. The VERSION_ID
of the os-release in the stub now only contains the generation number
and the build time. This makes a correct PRETTY_NAME entirely dependent
on correct information in the bootspec `label` field.
Read the build time from generation symlinks in /nix/var/nix/profiles
instead of from the underlying derivation. The derivation build time
will always be a UNIX epoch of 0 because of the `nix-build` sandbox,
which is useless for identifying when a generation was created.
Do not pass our own cmdline on to the kernel. It may have been set by a
malicious boot loader specification entry, and could instruct the
kernel to load an arbitrary unprotected initrd (or perform some other
fun stuff). Instead, always pass the command line built into the UKI,
which is properly authenticated.
Malicious boot loader specification entries could be used to make a
signed kernel load arbitrary unprotected initrds. Since we do not want
this, do not sign the kernel. This way, the only things allowed to boot
are our UKI stubs, which do verify the initrd.
When loading something with UEFI LoadImage, signature validation is
performed. However, we verify the kernel by its hash already, and don't
want to sign it. Hence, we have to load it on our own.
To minimize the number of arguments passed to `lzbt`, the loader config
is assembled outside `lzbt` and passed as a single argument.
Instead of reimplementing `consoleMode` under the `lanzaboote`
namespace, `config.loader.systemd-boot.consoleMode` is reused as is.
To minimize the potential for irrecoverable errors, only atomic writes
to the ESP are performed. This is implemented by first copying the file
to the destination with a `.tmp` suffix and then renaming it to the
final desintation. This is atomic because the rename operation is atomic
on POSIX platforms.
Specifically, this means that even if the system crashes during the
operation, the final desintation path will most likely be intact if it
exists at all. There are some nuances to this however. It **cannot** be
actually guaranteed that the operation was performed on the filesystem
level. However, this is the best we can do for now.
For reference:
- POSIX rename(2): https://pubs.opengroup.org/onlinepubs/9699919799/
- Rust fs::rename corresponds to rename(2) on Unix: https://doc.rust-lang.org/std/fs/fn.rename.html
- Rust fs::rename is implemented using libc's rename: https://github.com/rust-lang/rust/blob/master/library/std/src/sys/unix/fs.rs#L1397
- Renaming in libc is atomic: https://www.gnu.org/software/libc/manual/html_node/Renaming-Files.html
The test attributes and names are simplified and standardized. They now
roughly follow the same structure as the systemd-boot test in Nixpkgs.
Some comments are added and variable names changed to make it more clear
what they actually do.
To make handling systemd versions more robust, they are parsed into a
u32 tuple instead of an f32. Additionally, some unit tests for correct
parsing and comparing of versions are added.