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