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(¶m);
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}