Skip to main content

bootc_lib/
boot_artifact.rs

1use anyhow::{Context, Result};
2use camino::{Utf8Path, Utf8PathBuf};
3use cap_std_ext::cap_std::fs::Dir;
4use composefs::erofs::format::FormatVersion;
5use composefs::fsverity::{FsVerityHashValue, Sha512HashValue};
6use composefs_ctl::composefs;
7use linux_kernel_cmdline::utf8::Cmdline;
8
9use crate::bootc_composefs::digest::compute_composefs_digest;
10use crate::bootc_composefs::status::build_composefs_karg;
11use crate::cli::BootArtifactErofsVersionArg;
12use crate::kernel::KernelType;
13
14fn resolve_erofs_versions(
15    requested: Option<BootArtifactErofsVersionArg>,
16) -> (FormatVersion, Option<FormatVersion>) {
17    match requested.unwrap_or(BootArtifactErofsVersionArg::V1) {
18        BootArtifactErofsVersionArg::V1 => (FormatVersion::V1, Some(FormatVersion::V2)),
19        BootArtifactErofsVersionArg::V1Only => (FormatVersion::V1, None),
20        BootArtifactErofsVersionArg::V2 => (FormatVersion::V2, None),
21    }
22}
23
24fn composefs_kargs_for_boot_artifact(
25    preferred_digest: Sha512HashValue,
26    preferred_version: FormatVersion,
27    compatibility_v2_digest: Option<Sha512HashValue>,
28    allow_missing_fsverity: bool,
29) -> Vec<String> {
30    let mut kargs = vec![build_composefs_karg(
31        preferred_digest,
32        preferred_version,
33        allow_missing_fsverity,
34    )];
35    if let Some(v2_digest) = compatibility_v2_digest {
36        kargs.push(build_composefs_karg(
37            v2_digest,
38            FormatVersion::V2,
39            allow_missing_fsverity,
40        ));
41    }
42    kargs
43}
44
45#[derive(Debug)]
46pub(crate) struct BootArtifactInputs {
47    pub(crate) kernel_version: String,
48    pub(crate) vmlinuz: Utf8PathBuf,
49    pub(crate) initramfs: Utf8PathBuf,
50    pub(crate) cmdline: String,
51}
52
53pub(crate) async fn prepare_boot_artifact(
54    rootfs: &Utf8Path,
55    kernel_dir: Option<&Utf8Path>,
56    extra_kargs: &[String],
57    allow_missing_fsverity: bool,
58    erofs_version: Option<BootArtifactErofsVersionArg>,
59    write_dumpfile_to: Option<&Utf8Path>,
60) -> Result<BootArtifactInputs> {
61    let root = Dir::open_ambient_dir(rootfs, cap_std_ext::cap_std::ambient_authority())
62        .with_context(|| format!("Opening rootfs {rootfs}"))?;
63
64    let (kernel_version, vmlinuz, initramfs, external_paths) = match kernel_dir {
65        Some(kernel_dir) => {
66            let kver = kernel_dir
67                .components()
68                .next_back()
69                .ok_or_else(|| anyhow::anyhow!("Could not determine kernel version"))?;
70            (
71                kver.to_string(),
72                kernel_dir.join("vmlinuz"),
73                kernel_dir.join("initramfs.img"),
74                true,
75            )
76        }
77        None => {
78            let kernel = crate::kernel::find_kernel(&root, None)?
79                .ok_or_else(|| anyhow::anyhow!("No kernel found in {rootfs}"))?;
80            match kernel.k_type {
81                KernelType::Vmlinuz { path, initramfs } => {
82                    (kernel.kernel.version, path, initramfs, false)
83                }
84                KernelType::Aboot { path, .. } | KernelType::Uki { path, .. } => anyhow::bail!(
85                    "Cannot build boot artifact: rootfs already contains a boot image at {path}"
86                ),
87            }
88        }
89    };
90
91    if external_paths {
92        if !vmlinuz.exists() {
93            anyhow::bail!("Kernel not found at {vmlinuz}");
94        }
95        if !initramfs.exists() {
96            anyhow::bail!("Initramfs not found at {initramfs}");
97        }
98    } else {
99        if !root.try_exists(&vmlinuz).context("Checking for vmlinuz")? {
100            anyhow::bail!("Kernel not found at {vmlinuz}");
101        }
102        if !root
103            .try_exists(&initramfs)
104            .context("Checking for initramfs")?
105        {
106            anyhow::bail!("Initramfs not found at {initramfs}");
107        }
108    }
109
110    let mut cmdline = crate::bootc_kargs::get_kargs_in_root(&root, std::env::consts::ARCH)?;
111    for karg in extra_kargs {
112        cmdline.extend(&Cmdline::from(karg));
113    }
114
115    let (erofs_version, compatibility_version) = resolve_erofs_versions(erofs_version);
116    let composefs_digest =
117        compute_composefs_digest(rootfs, erofs_version, write_dumpfile_to).await?;
118    let composefs_digest = Sha512HashValue::from_hex(&composefs_digest)
119        .context("Parsing computed composefs digest")?;
120    let compatibility_v2_digest = if let Some(version) = compatibility_version {
121        let digest = compute_composefs_digest(rootfs, version, None).await?;
122        Some(Sha512HashValue::from_hex(&digest).context("Parsing computed V2 digest")?)
123    } else {
124        None
125    };
126
127    for karg in composefs_kargs_for_boot_artifact(
128        composefs_digest,
129        erofs_version,
130        compatibility_v2_digest,
131        allow_missing_fsverity,
132    ) {
133        for param in &Cmdline::from(karg) {
134            cmdline.add_or_modify(&param);
135        }
136    }
137
138    Ok(BootArtifactInputs {
139        kernel_version,
140        vmlinuz,
141        initramfs,
142        cmdline: cmdline.to_string(),
143    })
144}
145
146#[cfg(test)]
147mod tests {
148    use std::fs;
149
150    use bootc_utils::create_minimal_pe;
151
152    use super::*;
153
154    #[tokio::test]
155    async fn no_kernel() {
156        let tempdir = tempfile::tempdir().unwrap();
157        let path = Utf8Path::from_path(tempdir.path()).unwrap();
158
159        let err = prepare_boot_artifact(path, None, &[], false, None, None)
160            .await
161            .unwrap_err();
162        assert!(format!("{err:#}").contains("No kernel found"));
163    }
164
165    #[tokio::test]
166    async fn rejects_uki() {
167        let tempdir = tempfile::tempdir().unwrap();
168        let path = Utf8Path::from_path(tempdir.path()).unwrap();
169        fs::create_dir_all(tempdir.path().join("boot/EFI/Linux")).unwrap();
170        fs::write(
171            tempdir.path().join("boot/EFI/Linux/test.efi"),
172            &create_minimal_pe(),
173        )
174        .unwrap();
175
176        let err = prepare_boot_artifact(path, None, &[], false, None, None)
177            .await
178            .unwrap_err();
179        assert!(format!("{err:#}").contains("already contains a boot image"));
180    }
181
182    #[test]
183    fn test_composefs_kargs_for_boot_artifact() {
184        let v1 = Sha512HashValue::EMPTY;
185        let v2 = Sha512HashValue::from_hex("aa".repeat(64)).unwrap();
186        let v1_arg = format!("composefs.digest=v1-sha512-12:{}", v1.to_hex());
187        let v2_arg = format!("composefs={}", v2.to_hex());
188        for (requested, expected_version, expected_fallback, expected_kargs) in [
189            (
190                None,
191                FormatVersion::V1,
192                Some(FormatVersion::V2),
193                vec![v1_arg.clone(), v2_arg.clone()],
194            ),
195            (
196                Some(BootArtifactErofsVersionArg::V1),
197                FormatVersion::V1,
198                Some(FormatVersion::V2),
199                vec![v1_arg.clone(), v2_arg.clone()],
200            ),
201            (
202                Some(BootArtifactErofsVersionArg::V1Only),
203                FormatVersion::V1,
204                None,
205                vec![v1_arg],
206            ),
207            (
208                Some(BootArtifactErofsVersionArg::V2),
209                FormatVersion::V2,
210                None,
211                vec![v2_arg],
212            ),
213        ] {
214            let (version, compatibility_version) = resolve_erofs_versions(requested);
215            let preferred_digest = if version == FormatVersion::V1 {
216                &v1
217            } else {
218                &v2
219            };
220            let fallback = compatibility_version.map(|_| v2.clone());
221            let kargs = composefs_kargs_for_boot_artifact(
222                preferred_digest.clone(),
223                version,
224                fallback,
225                false,
226            );
227            assert_eq!(kargs, expected_kargs, "{requested:?}");
228            assert_eq!(version, expected_version);
229            assert_eq!(compatibility_version, expected_fallback);
230        }
231    }
232}