1use anyhow::{Context, Result};
3use bootc_kernel_cmdline::utf8::{Cmdline, CmdlineOwned};
4use camino::Utf8Path;
5use cap_std_ext::cap_std::fs::Dir;
6use cap_std_ext::cap_std::fs_utf8::Dir as DirUtf8;
7use cap_std_ext::dirext::CapStdExtDirExt;
8use cap_std_ext::dirext::CapStdExtDirExtUtf8;
9use ostree::gio;
10use ostree_ext::ostree;
11use ostree_ext::ostree::Deployment;
12use ostree_ext::prelude::Cast;
13use ostree_ext::prelude::FileEnumeratorExt;
14use ostree_ext::prelude::FileExt;
15use serde::Deserialize;
16
17use crate::deploy::ImageState;
18use crate::store::Storage;
19
20const KARGS_PATH: &str = "usr/lib/bootc/kargs.d";
22
23pub(crate) const ROOT_KEY: &str = "root";
25pub(crate) const INITRD_ARG_PREFIX: &str = "rd.";
27pub(crate) const ROOTFLAGS_KEY: &str = "rootflags";
29
30#[derive(Deserialize)]
32#[serde(rename_all = "kebab-case", deny_unknown_fields)]
33struct Config {
34 kargs: Vec<String>,
36 match_architectures: Option<Vec<String>>,
39}
40
41impl Config {
42 fn filename_matches(name: &str) -> bool {
44 matches!(Utf8Path::new(name).extension(), Some("toml"))
45 }
46}
47
48fn compute_apply_kargs_diff(
51 existing_kargs: &Cmdline,
52 remote_kargs: &Cmdline,
53 new_kargs: &mut Cmdline,
54) {
55 let added_kargs: Vec<_> = remote_kargs
57 .iter()
58 .filter(|item| !existing_kargs.iter().any(|existing| *item == existing))
59 .collect();
60 let removed_kargs: Vec<_> = existing_kargs
61 .iter()
62 .filter(|item| !remote_kargs.iter().any(|remote| *item == remote))
63 .collect();
64
65 tracing::debug!("kargs: added={:?} removed={:?}", added_kargs, removed_kargs);
66
67 for arg in &removed_kargs {
69 new_kargs.remove_exact(arg);
70 }
71 for arg in &added_kargs {
72 new_kargs.add(arg);
73 }
74}
75
76pub(crate) fn compute_new_kargs(
92 new_fs: &Dir,
93 current_root: Option<&Dir>,
94 new_kargs: &mut Cmdline,
95) -> Result<()> {
96 let remote_kargs = get_kargs_in_root(new_fs, std::env::consts::ARCH)?;
97
98 let existing_kargs = match current_root {
99 Some(root) => get_kargs_in_root(root, std::env::consts::ARCH)?,
100 None => Cmdline::new(),
101 };
102
103 compute_apply_kargs_diff(&existing_kargs, &remote_kargs, new_kargs);
104
105 Ok(())
106}
107
108pub(crate) fn get_kargs_in_root(d: &Dir, sys_arch: &str) -> Result<CmdlineOwned> {
111 let Some(d) = d.open_dir_optional(KARGS_PATH)?.map(DirUtf8::from_cap_std) else {
113 return Ok(Default::default());
114 };
115 let mut ret = Cmdline::new();
116 let entries = d.filenames_filtered_sorted(|_, name| Config::filename_matches(name))?;
117 for name in entries {
118 let buf = d.read_to_string(&name)?;
119 if let Some(kargs) =
120 parse_kargs_toml(&buf, sys_arch).with_context(|| format!("Parsing {name}"))?
121 {
122 ret.extend(&kargs)
123 }
124 }
125 Ok(ret)
126}
127
128pub(crate) fn root_args_from_cmdline(cmdline: &Cmdline) -> CmdlineOwned {
129 let mut result = Cmdline::new();
130 for param in cmdline {
131 let key = param.key();
132 if key == ROOT_KEY.into()
133 || key == ROOTFLAGS_KEY.into()
134 || key.starts_with(INITRD_ARG_PREFIX)
135 {
136 result.add(¶m);
137 }
138 }
139 result
140}
141
142pub(crate) fn get_kargs_from_ostree_root(
144 repo: &ostree::Repo,
145 root: &ostree::RepoFile,
146 sys_arch: &str,
147) -> Result<CmdlineOwned> {
148 let kargsd = root.resolve_relative_path(KARGS_PATH);
149 let kargsd = kargsd.downcast_ref::<ostree::RepoFile>().expect("downcast");
150 if !kargsd.query_exists(gio::Cancellable::NONE) {
151 return Ok(Default::default());
152 }
153 get_kargs_from_ostree(repo, kargsd, sys_arch)
154}
155
156fn get_kargs_from_ostree(
158 repo: &ostree::Repo,
159 fetched_tree: &ostree::RepoFile,
160 sys_arch: &str,
161) -> Result<CmdlineOwned> {
162 let cancellable = gio::Cancellable::NONE;
163 let queryattrs = "standard::name,standard::type";
164 let queryflags = gio::FileQueryInfoFlags::NOFOLLOW_SYMLINKS;
165 let fetched_iter = fetched_tree.enumerate_children(queryattrs, queryflags, cancellable)?;
166 let mut ret = Cmdline::new();
167 while let Some(fetched_info) = fetched_iter.next_file(cancellable)? {
168 let name = fetched_info.name();
170 let Some(name) = name.to_str() else {
171 continue;
172 };
173 if !Config::filename_matches(name) {
174 continue;
175 }
176
177 let fetched_child = fetched_iter.child(&fetched_info);
178 let fetched_child = fetched_child
179 .downcast::<ostree::RepoFile>()
180 .expect("downcast");
181 fetched_child.ensure_resolved()?;
182 let fetched_contents_checksum = fetched_child.checksum();
183 let f = ostree::Repo::load_file(repo, fetched_contents_checksum.as_str(), cancellable)?;
184 let file_content = f.0;
185 let mut reader =
186 ostree_ext::prelude::InputStreamExtManual::into_read(file_content.unwrap());
187 let s = std::io::read_to_string(&mut reader)?;
188 if let Some(parsed_kargs) =
189 parse_kargs_toml(&s, sys_arch).with_context(|| format!("Parsing {name}"))?
190 {
191 ret.extend(&parsed_kargs);
192 }
193 }
194 Ok(ret)
195}
196
197pub(crate) fn get_kargs(
201 sysroot: &Storage,
202 merge_deployment: &Deployment,
203 fetched: &ImageState,
204) -> Result<CmdlineOwned> {
205 let cancellable = gio::Cancellable::NONE;
206 let ostree = sysroot.get_ostree()?;
207 let repo = &ostree.repo();
208 let sys_arch = std::env::consts::ARCH;
209
210 let mut kargs = ostree::Deployment::bootconfig(merge_deployment)
212 .and_then(|bootconfig| {
213 ostree::BootconfigParser::get(&bootconfig, "options")
214 .map(|options| Cmdline::from(options.to_string()))
215 })
216 .unwrap_or_default();
217
218 let merge_root = &crate::utils::deployment_fd(ostree, merge_deployment)?;
220 let existing_kargs = get_kargs_in_root(merge_root, sys_arch)?;
221
222 let (fetched_tree, _) = repo.read_commit(fetched.ostree_commit.as_str(), cancellable)?;
224 let fetched_tree = fetched_tree.resolve_relative_path(KARGS_PATH);
225 let fetched_tree = fetched_tree
226 .downcast::<ostree::RepoFile>()
227 .expect("downcast");
228 if !fetched_tree.query_exists(cancellable) {
231 kargs.extend(&existing_kargs);
232 return Ok(kargs);
233 }
234
235 let remote_kargs = get_kargs_from_ostree(repo, &fetched_tree, sys_arch)?;
237
238 compute_apply_kargs_diff(&existing_kargs, &remote_kargs, &mut kargs);
239
240 Ok(kargs)
241}
242
243fn parse_kargs_toml(contents: &str, sys_arch: &str) -> Result<Option<CmdlineOwned>> {
247 let de: Config = toml::from_str(contents)?;
248 let matched = de
251 .match_architectures
252 .map(|arches| arches.iter().any(|s| s == sys_arch))
253 .unwrap_or(true);
254 let r = if matched {
255 Some(Cmdline::from(de.kargs.join(" ")))
256 } else {
257 None
258 };
259 Ok(r)
260}
261
262#[cfg(test)]
263mod tests {
264 use cap_std_ext::cap_std;
265 use fn_error_context::context;
266 use rustix::fd::{AsFd, AsRawFd};
267
268 use super::*;
269
270 fn assert_cmdline_eq(cmdline: &Cmdline, expected_params: &[&str]) {
271 let actual_params: Vec<_> = cmdline.iter_str().collect();
272 assert_eq!(actual_params, expected_params);
273 }
274
275 #[test]
276 fn test_arch() {
278 let sys_arch = "x86_64";
280 let file_content = r##"kargs = ["console=tty0", "nosmt"]"##.to_string();
281 let parsed_kargs = parse_kargs_toml(&file_content, sys_arch).unwrap().unwrap();
282 assert_cmdline_eq(&parsed_kargs, &["console=tty0", "nosmt"]);
283
284 let sys_arch = "aarch64";
285 let parsed_kargs = parse_kargs_toml(&file_content, sys_arch).unwrap().unwrap();
286 assert_cmdline_eq(&parsed_kargs, &["console=tty0", "nosmt"]);
287
288 let sys_arch = "aarch64";
290 let file_content = r##"kargs = ["console=tty0", "nosmt"]
291match-architectures = ["x86_64"]
292"##
293 .to_string();
294 let parsed_kargs = parse_kargs_toml(&file_content, sys_arch).unwrap();
295 assert!(parsed_kargs.is_none());
296 let file_content = r##"kargs = ["console=tty0", "nosmt"]
297match-architectures = ["aarch64"]
298"##
299 .to_string();
300 let parsed_kargs = parse_kargs_toml(&file_content, sys_arch).unwrap().unwrap();
301 assert_cmdline_eq(&parsed_kargs, &["console=tty0", "nosmt"]);
302
303 let sys_arch = "x86_64";
305 let file_content = r##"kargs = ["console=tty0", "nosmt"]
306match-architectures = ["x86_64", "aarch64"]
307"##
308 .to_string();
309 let parsed_kargs = parse_kargs_toml(&file_content, sys_arch).unwrap().unwrap();
310 assert_cmdline_eq(&parsed_kargs, &["console=tty0", "nosmt"]);
311
312 let sys_arch = "aarch64";
313 let parsed_kargs = parse_kargs_toml(&file_content, sys_arch).unwrap().unwrap();
314 assert_cmdline_eq(&parsed_kargs, &["console=tty0", "nosmt"]);
315 }
316
317 #[test]
318 fn test_invalid() {
320 let test_invalid_extra = r#"kargs = ["console=tty0", "nosmt"]\nfoo=bar"#;
321 assert!(parse_kargs_toml(test_invalid_extra, "x86_64").is_err());
322
323 let test_missing = r#"foo=bar"#;
324 assert!(parse_kargs_toml(test_missing, "x86_64").is_err());
325 }
326
327 #[context("writing test kargs")]
328 fn write_test_kargs(td: &Dir) -> Result<()> {
329 td.write(
330 "usr/lib/bootc/kargs.d/01-foo.toml",
331 r##"kargs = ["console=tty0", "nosmt"]"##,
332 )?;
333 td.write(
334 "usr/lib/bootc/kargs.d/02-bar.toml",
335 r##"kargs = ["console=ttyS1"]"##,
336 )?;
337
338 Ok(())
339 }
340
341 #[test]
342 fn test_get_kargs_in_root() -> Result<()> {
343 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
344
345 assert_eq!(get_kargs_in_root(&td, "x86_64").unwrap().iter().count(), 0);
347 td.create_dir_all("usr/lib/bootc/kargs.d")?;
349 assert_eq!(get_kargs_in_root(&td, "x86_64").unwrap().iter().count(), 0);
350 td.write("usr/lib/bootc/kargs.d/somegarbage", "garbage")?;
352 assert_eq!(get_kargs_in_root(&td, "x86_64").unwrap().iter().count(), 0);
353
354 write_test_kargs(&td)?;
355
356 let args = get_kargs_in_root(&td, "x86_64").unwrap();
357 assert_cmdline_eq(&args, &["console=tty0", "nosmt", "console=ttyS1"]);
358
359 Ok(())
360 }
361
362 #[context("ostree commit")]
363 fn ostree_commit(
364 repo: &ostree::Repo,
365 d: &Dir,
366 path: &Utf8Path,
367 ostree_ref: &str,
368 ) -> Result<()> {
369 let cancellable = gio::Cancellable::NONE;
370 let txn = repo.auto_transaction(cancellable)?;
371
372 let mt = ostree::MutableTree::new();
373 let commitmod_flags = ostree::RepoCommitModifierFlags::SKIP_XATTRS;
374 let commitmod = ostree::RepoCommitModifier::new(commitmod_flags, None);
375 repo.write_dfd_to_mtree(
376 d.as_fd().as_raw_fd(),
377 path.as_str(),
378 &mt,
379 Some(&commitmod),
380 cancellable,
381 )
382 .context("Writing merged filesystem to mtree")?;
383
384 let merged_root = repo
385 .write_mtree(&mt, cancellable)
386 .context("Writing mtree")?;
387 let merged_root = merged_root.downcast::<ostree::RepoFile>().unwrap();
388 let merged_commit = repo
389 .write_commit(None, None, None, None, &merged_root, cancellable)
390 .context("Writing commit")?;
391 repo.transaction_set_ref(None, &ostree_ref, Some(merged_commit.as_str()));
392 txn.commit(cancellable)?;
393 Ok(())
394 }
395
396 #[test]
397 fn test_get_kargs_in_ostree() -> Result<()> {
398 let cancellable = gio::Cancellable::NONE;
399 let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
400
401 td.create_dir("repo")?;
402 let repo = &ostree::Repo::create_at(
403 td.as_fd().as_raw_fd(),
404 "repo",
405 ostree::RepoMode::Bare,
406 None,
407 gio::Cancellable::NONE,
408 )?;
409
410 td.create_dir("rootfs")?;
411 let test_rootfs = &td.open_dir("rootfs")?;
412
413 ostree_commit(repo, &test_rootfs, ".".into(), "testref")?;
414 let get_kargs = |sys_arch: &str| -> Result<CmdlineOwned> {
416 let rootfs = repo.read_commit("testref", cancellable)?.0;
417 let rootfs = rootfs.downcast_ref::<ostree::RepoFile>().unwrap();
418 let fetched_tree = rootfs.resolve_relative_path("/usr/lib/bootc/kargs.d");
419 let fetched_tree = fetched_tree
420 .downcast::<ostree::RepoFile>()
421 .expect("downcast");
422 if !fetched_tree.query_exists(cancellable) {
423 return Ok(Default::default());
424 }
425 get_kargs_from_ostree(repo, &fetched_tree, sys_arch)
426 };
427
428 assert_eq!(get_kargs("x86_64").unwrap().iter().count(), 0);
430
431 test_rootfs.create_dir_all("usr/lib/bootc/kargs.d")?;
432 write_test_kargs(&test_rootfs).unwrap();
433 ostree_commit(repo, &test_rootfs, ".".into(), "testref")?;
434
435 let args = get_kargs("x86_64").unwrap();
436 assert_cmdline_eq(&args, &["console=tty0", "nosmt", "console=ttyS1"]);
437
438 Ok(())
439 }
440}