1use std::io::BufRead;
2
3use anyhow::{Context, Result};
4use camino::{Utf8Path, Utf8PathBuf};
5use cap_std::fs::Dir;
6use cap_std_ext::{cap_std, dirext::CapStdExtDirExt};
7use fn_error_context::context;
8use ostree_ext::container_utils::{OSTREE_BOOTED, is_ostree_booted_in};
9use ostree_ext::{gio, ostree};
10use rustix::{fd::AsFd, fs::StatVfsMountFlags};
11
12use crate::install::DESTRUCTIVE_CLEANUP;
13
14const STATUS_ONBOOT_UNIT: &str = "bootc-status-updated-onboot.target";
15const STATUS_PATH_UNIT: &str = "bootc-status-updated.path";
16const CLEANUP_UNIT: &str = "bootc-destructive-cleanup.service";
17const MULTI_USER_TARGET: &str = "multi-user.target";
18const EDIT_UNIT: &str = "bootc-fstab-edit.service";
19const FSTAB_ANACONDA_STAMP: &str = "Created by anaconda";
20pub(crate) const BOOTC_EDITED_STAMP: &str = "Updated by bootc-fstab-edit.service";
21const TRANSIENT_RELABEL_UNIT: &str = "bootc-early-overlay-relabel.service";
22const SYSINIT_TARGET: &str = "sysinit.target";
23
24#[context("bootc generator")]
26pub(crate) fn fstab_generator_impl(root: &Dir, unit_dir: &Dir) -> Result<bool> {
27 if !is_ostree_booted_in(root)? {
29 return Ok(false);
30 }
31
32 if let Some(fd) = root
33 .open_optional("etc/fstab")
34 .context("Opening /etc/fstab")?
35 .map(std::io::BufReader::new)
36 {
37 let mut from_anaconda = false;
38 for line in fd.lines() {
39 let line = line.context("Reading /etc/fstab")?;
40 if line.contains(BOOTC_EDITED_STAMP) {
41 return Ok(false);
43 }
44 if line.contains(FSTAB_ANACONDA_STAMP) {
45 from_anaconda = true;
46 }
47 }
48 if !from_anaconda {
49 return Ok(false);
50 }
51 tracing::debug!("/etc/fstab from anaconda: {from_anaconda}");
52 if from_anaconda {
53 generate_fstab_editor(unit_dir)?;
54 return Ok(true);
55 }
56 }
57 Ok(false)
58}
59
60pub(crate) fn enable_unit(unitdir: &Dir, name: &str, target: &str) -> Result<()> {
61 let wants = Utf8PathBuf::from(format!("{target}.wants"));
62 unitdir
63 .create_dir_all(&wants)
64 .with_context(|| format!("Creating {wants}"))?;
65 let source = format!("/usr/lib/systemd/system/{name}");
66 let target = wants.join(name);
67 unitdir.remove_file_optional(&target)?;
68 unitdir
69 .symlink_contents(&source, &target)
70 .with_context(|| format!("Writing {name}"))?;
71 Ok(())
72}
73
74pub(crate) fn unit_enablement_impl(sysroot: &Dir, unit_dir: &Dir) -> Result<()> {
76 for unit in [STATUS_ONBOOT_UNIT, STATUS_PATH_UNIT] {
77 enable_unit(unit_dir, unit, MULTI_USER_TARGET)?;
78 }
79
80 if sysroot.try_exists(DESTRUCTIVE_CLEANUP)? {
81 tracing::debug!("Found {DESTRUCTIVE_CLEANUP}");
82 enable_unit(unit_dir, CLEANUP_UNIT, MULTI_USER_TARGET)?;
83 } else {
84 tracing::debug!("Didn't find {DESTRUCTIVE_CLEANUP}");
85 }
86
87 Ok(())
88}
89
90pub(crate) fn generator(root: &Dir, unit_dir: &Dir) -> Result<()> {
92 {
113 let st = rustix::fs::fstatfs(root.as_fd())?;
114 if st.f_type == libc::OVERLAYFS_SUPER_MAGIC {
115 let root_is_transient =
116 match bootc_mount::inspect_filesystem(camino::Utf8Path::new("/")) {
117 Ok(fs) => fs.source.starts_with("transient:composefs="),
118 Err(e) => {
119 tracing::debug!("Could not inspect root filesystem: {e:#}");
120 false
121 }
122 };
123 let submounts_are_transient = bootc_initramfs_setup::config_has_transient_submounts(
124 std::path::Path::new(bootc_initramfs_setup::SETUP_ROOT_CONF_PATH),
125 );
126 if root_is_transient || submounts_are_transient {
127 tracing::debug!(
128 root_is_transient,
129 submounts_are_transient,
130 "Transient overlay detected; generating relabel unit"
131 );
132 generate_transient_overlay_relabel(unit_dir)?;
133 }
134 }
135 }
136
137 if !root.try_exists(OSTREE_BOOTED)? {
140 return Ok(());
141 }
142
143 let Some(ref sysroot) = root.open_dir_optional("sysroot")? else {
144 return Ok(());
145 };
146
147 unit_enablement_impl(sysroot, unit_dir)?;
148
149 let st = rustix::fs::fstatfs(root.as_fd())?;
151 if st.f_type != libc::OVERLAYFS_SUPER_MAGIC {
152 tracing::trace!("Root is not overlayfs");
153 return Ok(());
154 }
155
156 let st = rustix::fs::fstatvfs(root.as_fd())?;
158 if !st.f_flag.contains(StatVfsMountFlags::RDONLY) {
159 tracing::trace!("Root is writable, skipping fstab generator");
160 return Ok(());
161 }
162
163 let updated = fstab_generator_impl(root, unit_dir)?;
164 tracing::trace!("Generated fstab: {updated}");
165
166 Ok(())
167}
168
169fn generate_fstab_editor(unit_dir: &Dir) -> Result<()> {
172 unit_dir.atomic_write(
173 EDIT_UNIT,
174 "[Unit]\n\
175DefaultDependencies=no\n\
176After=systemd-fsck-root.service\n\
177Before=local-fs-pre.target local-fs.target shutdown.target systemd-remount-fs.service\n\
178\n\
179[Service]\n\
180Type=oneshot\n\
181RemainAfterExit=yes\n\
182ExecStart=bootc internals fixup-etc-fstab\n\
183",
184 )?;
185 let target = "local-fs-pre.target.wants";
186 unit_dir.create_dir_all(target)?;
187 unit_dir.symlink(&format!("../{EDIT_UNIT}"), &format!("{target}/{EDIT_UNIT}"))?;
188 Ok(())
189}
190
191fn generate_transient_overlay_relabel(unit_dir: &Dir) -> Result<()> {
195 unit_dir.atomic_write(
196 TRANSIENT_RELABEL_UNIT,
197 include_str!("units/bootc-early-overlay-relabel.service"),
198 )?;
199 let wants = format!("{SYSINIT_TARGET}.wants");
200 unit_dir.create_dir_all(&wants)?;
201 unit_dir.symlink(
202 &format!("../{TRANSIENT_RELABEL_UNIT}"),
203 &format!("{wants}/{TRANSIENT_RELABEL_UNIT}"),
204 )?;
205 Ok(())
206}
207
208pub(crate) fn relabel_overlay_mountpoints() -> Result<()> {
214 let policy = ostree::SePolicy::new(&gio::File::for_path("/"), gio::Cancellable::NONE)
215 .context("Loading SELinux policy")?;
216 for path in ["/", "/etc", "/var"] {
217 let dir = Dir::open_ambient_dir(path, cap_std::ambient_authority())
218 .with_context(|| format!("Opening {path}"))?;
219 let st = rustix::fs::fstatfs(dir.as_fd())?;
220 if st.f_type != libc::OVERLAYFS_SUPER_MAGIC {
221 tracing::trace!("{path} is not an overlayfs mount, skipping relabel");
222 continue;
223 }
224 let stv = rustix::fs::fstatvfs(dir.as_fd())?;
225 if stv.f_flag.contains(StatVfsMountFlags::RDONLY) {
226 tracing::trace!("{path} is a read-only overlayfs (composefs), skipping relabel");
227 continue;
228 }
229 let metadata = dir.metadata(".").with_context(|| format!("stat {path}"))?;
230 crate::lsm::relabel(
231 &dir,
232 &metadata,
233 Utf8Path::new("."),
234 Some(Utf8Path::new(path)),
235 &policy,
236 )
237 .with_context(|| format!("Relabelling {path}"))?;
238 tracing::debug!("Relabelled {path}");
239 }
240 Ok(())
241}
242
243#[cfg(test)]
244mod tests {
245 use camino::Utf8Path;
246
247 use super::*;
248
249 fn fixture() -> Result<cap_std_ext::cap_tempfile::TempDir> {
250 let tempdir = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
251 tempdir.create_dir("etc")?;
252 tempdir.create_dir("run")?;
253 tempdir.create_dir("sysroot")?;
254 tempdir.create_dir_all("run/systemd/system")?;
255 Ok(tempdir)
256 }
257
258 #[test]
259 fn test_generator_no_fstab() -> Result<()> {
260 let tempdir = fixture()?;
261 let unit_dir = &tempdir.open_dir("run/systemd/system")?;
262 fstab_generator_impl(&tempdir, &unit_dir).unwrap();
263
264 assert_eq!(unit_dir.entries()?.count(), 0);
265 Ok(())
266 }
267
268 #[test]
269 fn test_units() -> Result<()> {
270 let tempdir = &fixture()?;
271 let sysroot = &tempdir.open_dir("sysroot").unwrap();
272 let unit_dir = &tempdir.open_dir("run/systemd/system")?;
273
274 let verify = |wantsdir: &Dir, n: u32| -> Result<()> {
275 assert_eq!(unit_dir.entries()?.count(), 1);
276 let r = wantsdir.read_link_contents(STATUS_ONBOOT_UNIT)?;
277 let r: Utf8PathBuf = r.try_into().unwrap();
278 assert_eq!(r, format!("/usr/lib/systemd/system/{STATUS_ONBOOT_UNIT}"));
279 assert_eq!(wantsdir.entries()?.count(), n as usize);
280 anyhow::Ok(())
281 };
282
283 unit_enablement_impl(sysroot, &unit_dir).unwrap();
286 unit_enablement_impl(sysroot, &unit_dir).unwrap();
287 let wantsdir = &unit_dir.open_dir("multi-user.target.wants")?;
288 verify(wantsdir, 2)?;
289 assert!(
290 wantsdir
291 .symlink_metadata_optional(CLEANUP_UNIT)
292 .unwrap()
293 .is_none()
294 );
295
296 unit_enablement_impl(sysroot, &unit_dir).unwrap();
298 verify(wantsdir, 2)?;
299
300 sysroot
302 .create_dir_all(Utf8Path::new(DESTRUCTIVE_CLEANUP).parent().unwrap())
303 .unwrap();
304 sysroot.atomic_write(DESTRUCTIVE_CLEANUP, b"").unwrap();
305 unit_enablement_impl(sysroot, unit_dir).unwrap();
306 verify(wantsdir, 3)?;
307
308 assert!(
310 wantsdir
311 .symlink_metadata(CLEANUP_UNIT)
312 .unwrap()
313 .is_symlink()
314 );
315
316 Ok(())
317 }
318
319 #[cfg(test)]
320 mod test {
321 use super::*;
322
323 use ostree_ext::container_utils::OSTREE_BOOTED;
324
325 #[test]
326 fn test_generator_fstab() -> Result<()> {
327 let tempdir = fixture()?;
328 let unit_dir = &tempdir.open_dir("run/systemd/system")?;
329 tempdir.atomic_write("etc/fstab", "# Some dummy fstab")?;
331 fstab_generator_impl(&tempdir, &unit_dir).unwrap();
332 assert_eq!(unit_dir.entries()?.count(), 0);
333
334 tempdir.atomic_write("etc/fstab", &format!("# {FSTAB_ANACONDA_STAMP}"))?;
336 fstab_generator_impl(&tempdir, &unit_dir).unwrap();
337 assert_eq!(unit_dir.entries()?.count(), 0);
338
339 tempdir.atomic_write(OSTREE_BOOTED, "ostree booted")?;
341 fstab_generator_impl(&tempdir, &unit_dir).unwrap();
342 assert_eq!(unit_dir.entries()?.count(), 2);
343
344 Ok(())
345 }
346
347 #[test]
348 fn test_transient_overlay_relabel_generated() -> Result<()> {
349 let tempdir = fixture()?;
350 let unit_dir = &tempdir.open_dir("run/systemd/system")?;
351
352 generate_transient_overlay_relabel(unit_dir)?;
354
355 assert!(unit_dir.try_exists(TRANSIENT_RELABEL_UNIT)?);
357 let wants = format!("{SYSINIT_TARGET}.wants");
359 let link = unit_dir.read_link_contents(format!("{wants}/{TRANSIENT_RELABEL_UNIT}"))?;
360 let link: camino::Utf8PathBuf = link.try_into().unwrap();
361 assert_eq!(link, format!("../{TRANSIENT_RELABEL_UNIT}"));
362 let content = unit_dir.read_to_string(TRANSIENT_RELABEL_UNIT)?;
364 assert!(
365 content.contains("ExecStart=bootc internals relabel-overlay-mountpoints"),
366 "unexpected unit content: {content}"
367 );
368
369 Ok(())
370 }
371
372 #[test]
373 fn test_transient_overlay_relabel_idempotent() -> Result<()> {
374 let tempdir = fixture()?;
375 let unit_dir = &tempdir.open_dir("run/systemd/system")?;
376
377 generate_transient_overlay_relabel(unit_dir)?;
379 let wants = format!("{SYSINIT_TARGET}.wants");
384 unit_dir.remove_file_optional(format!("{wants}/{TRANSIENT_RELABEL_UNIT}"))?;
385 generate_transient_overlay_relabel(unit_dir)?;
386
387 assert!(unit_dir.try_exists(TRANSIENT_RELABEL_UNIT)?);
388
389 Ok(())
390 }
391
392 #[test]
393 fn test_generator_fstab_idempotent() -> Result<()> {
394 let anaconda_fstab = indoc::indoc! { "
395#
396# /etc/fstab
397# Created by anaconda on Tue Mar 19 12:24:29 2024
398#
399# Accessible filesystems, by reference, are maintained under '/dev/disk/'.
400# See man pages fstab(5), findfs(8), mount(8) and/or blkid(8) for more info.
401#
402# After editing this file, run 'systemctl daemon-reload' to update systemd
403# units generated from this file.
404#
405# Updated by bootc-fstab-edit.service
406UUID=715be2b7-c458-49f2-acec-b2fdb53d9089 / xfs ro 0 0
407UUID=341c4712-54e8-4839-8020-d94073b1dc8b /boot xfs defaults 0 0
408" };
409 let tempdir = fixture()?;
410 let unit_dir = &tempdir.open_dir("run/systemd/system")?;
411
412 tempdir.atomic_write("etc/fstab", anaconda_fstab)?;
413 tempdir.atomic_write(OSTREE_BOOTED, "ostree booted")?;
414 let updated = fstab_generator_impl(&tempdir, &unit_dir).unwrap();
415 assert!(!updated);
416 assert_eq!(unit_dir.entries()?.count(), 0);
417
418 Ok(())
419 }
420 }
421}