bootc_lib/
bootloader.rs

1use std::fs::create_dir_all;
2use std::process::Command;
3
4use anyhow::{Context, Result, anyhow, bail};
5use bootc_utils::{BwrapCmd, CommandRunExt};
6use camino::Utf8Path;
7use cap_std_ext::cap_std::fs::Dir;
8use cap_std_ext::dirext::CapStdExtDirExt;
9use fn_error_context::context;
10
11use bootc_blockdev::{Partition, PartitionTable};
12use bootc_mount as mount;
13
14use crate::bootc_composefs::boot::{SecurebootKeys, get_sysroot_parent_dev, mount_esp};
15use crate::{discoverable_partition_specification, utils};
16
17/// The name of the mountpoint for efi (as a subdirectory of /boot, or at the toplevel)
18pub(crate) const EFI_DIR: &str = "efi";
19/// The EFI system partition GUID
20/// Path to the bootupd update payload
21#[allow(dead_code)]
22const BOOTUPD_UPDATES: &str = "usr/lib/bootupd/updates";
23
24// from: https://github.com/systemd/systemd/blob/26b2085d54ebbfca8637362eafcb4a8e3faf832f/man/systemd-boot.xml#L392
25const SYSTEMD_KEY_DIR: &str = "loader/keys";
26
27#[allow(dead_code)]
28pub(crate) fn esp_in(device: &PartitionTable) -> Result<&Partition> {
29    device
30        .find_partition_of_type(discoverable_partition_specification::ESP)
31        .ok_or(anyhow::anyhow!("ESP not found in partition table"))
32}
33
34/// Get esp partition node based on the root dir
35pub(crate) fn get_esp_partition_node(root: &Dir) -> Result<Option<String>> {
36    let device = get_sysroot_parent_dev(&root)?;
37    let base_partitions = bootc_blockdev::partitions_of(Utf8Path::new(&device))?;
38    let esp = base_partitions.find_partition_of_esp()?;
39    Ok(esp.map(|v| v.node.clone()))
40}
41
42/// Mount ESP part at /boot/efi
43pub(crate) fn mount_esp_part(root: &Dir, root_path: &Utf8Path, is_ostree: bool) -> Result<()> {
44    let efi_path = Utf8Path::new("boot").join(crate::bootloader::EFI_DIR);
45    let Some(esp_fd) = root
46        .open_dir_optional(&efi_path)
47        .context("Opening /boot/efi")?
48    else {
49        return Ok(());
50    };
51
52    let Some(false) = esp_fd.is_mountpoint(".")? else {
53        return Ok(());
54    };
55
56    tracing::debug!("Not a mountpoint: /boot/efi");
57    // On ostree env with enabled composefs, should be /target/sysroot
58    let physical_root = if is_ostree {
59        &root.open_dir("sysroot").context("Opening /sysroot")?
60    } else {
61        root
62    };
63    if let Some(esp_part) = get_esp_partition_node(physical_root)? {
64        bootc_mount::mount(&esp_part, &root_path.join(&efi_path))?;
65        tracing::debug!("Mounted {esp_part} at /boot/efi");
66    }
67    Ok(())
68}
69
70/// Determine if the invoking environment contains bootupd, and if there are bootupd-based
71/// updates in the target root.
72#[context("Querying for bootupd")]
73pub(crate) fn supports_bootupd(root: &Dir) -> Result<bool> {
74    if !utils::have_executable("bootupctl")? {
75        tracing::trace!("No bootupctl binary found");
76        return Ok(false);
77    };
78    let r = root.try_exists(BOOTUPD_UPDATES)?;
79    tracing::trace!("bootupd updates: {r}");
80    Ok(r)
81}
82
83#[context("Installing bootloader")]
84pub(crate) fn install_via_bootupd(
85    device: &PartitionTable,
86    rootfs: &Utf8Path,
87    configopts: &crate::install::InstallConfigOpts,
88    deployment_path: Option<&str>,
89) -> Result<()> {
90    let verbose = std::env::var_os("BOOTC_BOOTLOADER_DEBUG").map(|_| "-vvvv");
91    // bootc defaults to only targeting the platform boot method.
92    let bootupd_opts = (!configopts.generic_image).then_some(["--update-firmware", "--auto"]);
93
94    // When not running inside the target container (through `--src-imgref`) we use
95    // will bwrap as a chroot to run bootupctl from the deployment.
96    // This makes sure we use binaries from the target image rather than the buildroot.
97    // In that case, the target rootfs is replaced with `/` because this is just used by
98    // bootupd to find the backing device.
99    let rootfs_mount = if deployment_path.is_none() {
100        rootfs.as_str()
101    } else {
102        "/"
103    };
104
105    println!("Installing bootloader via bootupd");
106
107    // Build the bootupctl arguments
108    let mut bootupd_args: Vec<&str> = vec!["backend", "install"];
109    if configopts.bootupd_skip_boot_uuid {
110        bootupd_args.push("--with-static-configs")
111    } else {
112        bootupd_args.push("--write-uuid");
113    }
114    if let Some(v) = verbose {
115        bootupd_args.push(v);
116    }
117
118    if let Some(ref opts) = bootupd_opts {
119        bootupd_args.extend(opts.iter().copied());
120    }
121    bootupd_args.extend(["--device", device.path().as_str(), rootfs_mount]);
122
123    // Run inside a bwrap container. It takes care of mounting and creating
124    // the necessary API filesystems in the target deployment and acts as
125    // a nicer `chroot`.
126    if let Some(deploy) = deployment_path {
127        let target_root = rootfs.join(deploy);
128        let boot_path = rootfs.join("boot");
129
130        tracing::debug!("Running bootupctl via bwrap in {}", target_root);
131
132        // Prepend "bootupctl" to the args for bwrap
133        let mut bwrap_args = vec!["bootupctl"];
134        bwrap_args.extend(bootupd_args);
135
136        let mut cmd = BwrapCmd::new(&target_root)
137            // Bind mount /boot from the physical target root so bootupctl can find
138            // the boot partition and install the bootloader there
139            .bind(&boot_path, &"/boot")
140            // Bind the target block device inside the bwrap container so bootupctl can access it
141            .bind_device(device.path().as_str());
142
143        // Also bind all partitions of the tafet block device
144        for partition in &device.partitions {
145            cmd = cmd.bind_device(&partition.node);
146        }
147
148        // The $PATH in the bwrap env is not complete enough for some images
149        // so we inject a reasonnable default.
150        // This is causing bootupctl and/or sfdisk binaries
151        // to be not found with fedora 43.
152        cmd.setenv(
153            "PATH",
154            "/bin:/usr/bin:/sbin:/usr/sbin:/usr/local/bin:/usr/local/sbin",
155        )
156        .run(bwrap_args)
157    } else {
158        // Running directly without chroot
159        Command::new("bootupctl")
160            .args(&bootupd_args)
161            .log_debug()
162            .run_inherited_with_cmd_context()
163    }
164}
165
166#[context("Installing bootloader")]
167pub(crate) fn install_systemd_boot(
168    device: &PartitionTable,
169    _rootfs: &Utf8Path,
170    _configopts: &crate::install::InstallConfigOpts,
171    _deployment_path: Option<&str>,
172    autoenroll: Option<SecurebootKeys>,
173) -> Result<()> {
174    let esp_part = device
175        .find_partition_of_type(discoverable_partition_specification::ESP)
176        .ok_or_else(|| anyhow::anyhow!("ESP partition not found"))?;
177
178    let esp_mount = mount_esp(&esp_part.node).context("Mounting ESP")?;
179    let esp_path = Utf8Path::from_path(esp_mount.dir.path())
180        .ok_or_else(|| anyhow::anyhow!("Failed to convert ESP mount path to UTF-8"))?;
181
182    println!("Installing bootloader via systemd-boot");
183    Command::new("bootctl")
184        .args(["install", "--esp-path", esp_path.as_str()])
185        .log_debug()
186        .run_inherited_with_cmd_context()?;
187
188    if let Some(SecurebootKeys { dir, keys }) = autoenroll {
189        let path = esp_path.join(SYSTEMD_KEY_DIR);
190        create_dir_all(&path)?;
191
192        let keys_dir = esp_mount
193            .fd
194            .open_dir(SYSTEMD_KEY_DIR)
195            .with_context(|| format!("Opening {path}"))?;
196
197        for filename in keys.iter() {
198            let p = path.join(&filename);
199
200            // create directory if it doesn't already exist
201            if let Some(parent) = p.parent() {
202                create_dir_all(parent)?;
203            }
204
205            dir.copy(&filename, &keys_dir, &filename)
206                .with_context(|| format!("Copying secure boot key: {p}"))?;
207            println!("Wrote Secure Boot key: {p}");
208        }
209        if keys.is_empty() {
210            tracing::debug!("No Secure Boot keys provided for systemd-boot enrollment");
211        }
212    }
213
214    Ok(())
215}
216
217#[context("Installing bootloader using zipl")]
218pub(crate) fn install_via_zipl(device: &PartitionTable, boot_uuid: &str) -> Result<()> {
219    // Identify the target boot partition from UUID
220    let fs = mount::inspect_filesystem_by_uuid(boot_uuid)?;
221    let boot_dir = Utf8Path::new(&fs.target);
222    let maj_min = fs.maj_min;
223
224    // Ensure that the found partition is a part of the target device
225    let device_path = device.path();
226
227    let partitions = bootc_blockdev::list_dev(device_path)?
228        .children
229        .with_context(|| format!("no partition found on {device_path}"))?;
230    let boot_part = partitions
231        .iter()
232        .find(|part| part.maj_min.as_deref() == Some(maj_min.as_str()))
233        .with_context(|| format!("partition device {maj_min} is not on {device_path}"))?;
234    let boot_part_offset = boot_part.start.unwrap_or(0);
235
236    // Find exactly one BLS configuration under /boot/loader/entries
237    // TODO: utilize the BLS parser in ostree
238    let bls_dir = boot_dir.join("boot/loader/entries");
239    let bls_entry = bls_dir
240        .read_dir_utf8()?
241        .try_fold(None, |acc, e| -> Result<_> {
242            let e = e?;
243            let name = Utf8Path::new(e.file_name());
244            if let Some("conf") = name.extension() {
245                if acc.is_some() {
246                    bail!("more than one BLS configurations under {bls_dir}");
247                }
248                Ok(Some(e.path().to_owned()))
249            } else {
250                Ok(None)
251            }
252        })?
253        .with_context(|| format!("no BLS configuration under {bls_dir}"))?;
254
255    let bls_path = bls_dir.join(bls_entry);
256    let bls_conf =
257        std::fs::read_to_string(&bls_path).with_context(|| format!("reading {bls_path}"))?;
258
259    let mut kernel = None;
260    let mut initrd = None;
261    let mut options = None;
262
263    for line in bls_conf.lines() {
264        match line.split_once(char::is_whitespace) {
265            Some(("linux", val)) => kernel = Some(val.trim().trim_start_matches('/')),
266            Some(("initrd", val)) => initrd = Some(val.trim().trim_start_matches('/')),
267            Some(("options", val)) => options = Some(val.trim()),
268            _ => (),
269        }
270    }
271
272    let kernel = kernel.ok_or_else(|| anyhow!("missing 'linux' key in default BLS config"))?;
273    let initrd = initrd.ok_or_else(|| anyhow!("missing 'initrd' key in default BLS config"))?;
274    let options = options.ok_or_else(|| anyhow!("missing 'options' key in default BLS config"))?;
275
276    let image = boot_dir.join(kernel).canonicalize_utf8()?;
277    let ramdisk = boot_dir.join(initrd).canonicalize_utf8()?;
278
279    // Execute the zipl command to install bootloader
280    println!("Running zipl on {device_path}");
281    Command::new("zipl")
282        .args(["--target", boot_dir.as_str()])
283        .args(["--image", image.as_str()])
284        .args(["--ramdisk", ramdisk.as_str()])
285        .args(["--parameters", options])
286        .args(["--targetbase", device_path.as_str()])
287        .args(["--targettype", "SCSI"])
288        .args(["--targetblocksize", "512"])
289        .args(["--targetoffset", &boot_part_offset.to_string()])
290        .args(["--add-files", "--verbose"])
291        .log_debug()
292        .run_inherited_with_cmd_context()
293}