bootc_lib/bootc_composefs/
state.rs1use std::io::Write;
2use std::os::unix::fs::symlink;
3use std::path::Path;
4use std::{fs::create_dir_all, process::Command};
5
6use anyhow::{Context, Result};
7use bootc_initramfs_setup::{mount_at_wrapper, overlay_transient};
8use bootc_kernel_cmdline::utf8::Cmdline;
9use bootc_mount::tempmount::TempMount;
10use bootc_utils::CommandRunExt;
11use camino::Utf8PathBuf;
12use canon_json::CanonJsonSerialize;
13use cap_std_ext::cap_std::ambient_authority;
14use cap_std_ext::cap_std::fs::{Dir, Permissions, PermissionsExt};
15use cap_std_ext::dirext::CapStdExtDirExt;
16use composefs::fsverity::{FsVerityHashValue, Sha512HashValue};
17use composefs_ctl::composefs;
18use fn_error_context::context;
19
20use ostree_ext::container::deploy::ORIGIN_CONTAINER;
21use rustix::{
22 fd::AsFd,
23 fs::{Mode, OFlags, StatVfsMountFlags, open},
24 mount::MountAttrFlags,
25 path::Arg,
26};
27
28use crate::bootc_composefs::boot::BootType;
29use crate::bootc_composefs::status::{
30 ComposefsCmdline, StagedDeployment, get_sorted_type1_boot_entries,
31};
32use crate::parsers::bls_config::{BLSConfigType, EFIKey};
33use crate::store::{BootedComposefs, Storage};
34use crate::{
35 composefs_consts::{
36 COMPOSEFS_STAGED_DEPLOYMENT_FNAME, COMPOSEFS_TRANSIENT_STATE_DIR, ORIGIN_KEY_BOOT,
37 ORIGIN_KEY_BOOT_DIGEST, ORIGIN_KEY_BOOT_TYPE, ORIGIN_KEY_IMAGE, ORIGIN_KEY_MANIFEST_DIGEST,
38 SHARED_VAR_PATH, STATE_DIR_RELATIVE,
39 },
40 parsers::bls_config::BLSConfig,
41 spec::ImageReference,
42 spec::{FilesystemOverlay, FilesystemOverlayAccessMode, FilesystemOverlayPersistence},
43 utils::path_relative_to,
44};
45
46#[context("Reading origin for deployment {deployment_id}")]
51pub(crate) fn read_origin(sysroot: &Dir, deployment_id: &str) -> Result<Option<tini::Ini>> {
52 let depl_state_path = std::path::PathBuf::from(STATE_DIR_RELATIVE).join(deployment_id);
53
54 let Some(state_dir) = sysroot.open_dir_optional(&depl_state_path)? else {
55 return Ok(None);
56 };
57
58 let origin_filename = format!("{deployment_id}.origin");
59 let Some(origin_contents) = state_dir.read_to_string_optional(&origin_filename)? else {
60 return Ok(None);
61 };
62
63 let ini = tini::Ini::from_string(&origin_contents).context("Failed to parse origin file")?;
64 Ok(Some(ini))
65}
66
67pub(crate) fn get_booted_bls(boot_dir: &Dir, booted_cfs: &BootedComposefs) -> Result<BLSConfig> {
68 let sorted_entries = get_sorted_type1_boot_entries(boot_dir, true)?;
69
70 for entry in sorted_entries {
71 match &entry.cfg_type {
72 BLSConfigType::EFI { key } => {
73 let path = match key {
74 EFIKey::Efi(path) | EFIKey::Uki(path) => path,
75 };
76 if path.as_str().contains(&*booted_cfs.cmdline.digest) {
77 return Ok(entry);
78 }
79 }
80
81 BLSConfigType::NonEFI { options, .. } => {
82 let Some(opts) = options else {
83 anyhow::bail!("options not found in bls config")
84 };
85
86 let cfs_cmdline = ComposefsCmdline::find_in_cmdline(&Cmdline::from(opts))
87 .ok_or_else(|| anyhow::anyhow!("composefs param not found in cmdline"))?;
88
89 if cfs_cmdline.digest == booted_cfs.cmdline.digest {
90 return Ok(entry);
91 }
92 }
93
94 BLSConfigType::Unknown => anyhow::bail!("Unknown BLS Config type"),
95 };
96 }
97
98 Err(anyhow::anyhow!("Booted BLS not found"))
99}
100
101#[context("Initializing /etc and /var for state")]
104pub(crate) fn initialize_state(
105 sysroot_path: &Utf8PathBuf,
106 erofs_id: &String,
107 state_path: &Utf8PathBuf,
108 initialize_var: bool,
109 allow_missing_fsverity: bool,
110) -> Result<()> {
111 let sysroot_fd = open(
112 sysroot_path.as_std_path(),
113 OFlags::PATH | OFlags::DIRECTORY | OFlags::CLOEXEC,
114 Mode::empty(),
115 )
116 .context("Opening sysroot")?;
117
118 let composefs_fd = bootc_initramfs_setup::mount_composefs_image(
119 &sysroot_fd,
120 &erofs_id,
121 allow_missing_fsverity,
122 )?;
123
124 let tempdir = TempMount::mount_fd(composefs_fd)?;
125
126 if initialize_var {
128 Command::new("cp")
129 .args([
130 "-a",
131 "--remove-destination",
132 &format!("{}/var/.", tempdir.dir.path().as_str()?),
133 &format!("{state_path}/var/."),
134 ])
135 .run_capture_stderr()?;
136 }
137
138 Command::new("cp")
139 .args([
140 "-a",
141 "--remove-destination",
142 &format!("{}/etc/.", tempdir.dir.path().as_str()?),
143 &format!("{state_path}/etc/."),
144 ])
145 .run_capture_stderr()?;
146
147 let state_etc = Dir::open_ambient_dir(format!("{state_path}/etc"), ambient_authority())
153 .context("Opening state etc dir")?;
154 state_etc
155 .remove_file_optional(".updated")
156 .context("Removing /etc/.updated")?;
157
158 Ok(())
159}
160
161fn add_update_in_origin(
164 storage: &Storage,
165 deployment_id: &str,
166 section: &str,
167 kv_pairs: &[(&str, &str)],
168) -> Result<()> {
169 let path = Path::new(STATE_DIR_RELATIVE).join(deployment_id);
170
171 let state_dir = storage
172 .physical_root
173 .open_dir(path)
174 .context("Opening state dir")?;
175
176 let origin_filename = format!("{deployment_id}.origin");
177
178 let origin_file = state_dir
179 .read_to_string(&origin_filename)
180 .context("Reading origin file")?;
181
182 let mut ini =
183 tini::Ini::from_string(&origin_file).context("Failed to parse file origin file as ini")?;
184
185 for (key, value) in kv_pairs {
186 ini = ini.section(section).item(*key, *value);
187 }
188
189 state_dir
190 .atomic_replace_with(origin_filename, move |f| -> std::io::Result<_> {
191 f.write_all(ini.to_string().as_bytes())?;
192 f.flush()?;
193
194 let perms = Permissions::from_mode(0o644);
195 f.get_mut().as_file_mut().set_permissions(perms)?;
196
197 Ok(())
198 })
199 .context("Writing to origin file")?;
200
201 Ok(())
202}
203
204pub(crate) fn update_boot_digest_in_origin(
205 storage: &Storage,
206 digest: &str,
207 boot_digest: &str,
208) -> Result<()> {
209 add_update_in_origin(
210 storage,
211 digest,
212 ORIGIN_KEY_BOOT,
213 &[(ORIGIN_KEY_BOOT_DIGEST, boot_digest)],
214 )
215}
216
217#[context("Writing composefs state")]
244pub(crate) async fn write_composefs_state(
245 root_path: &Utf8PathBuf,
246 deployment_id: &Sha512HashValue,
247 target_imgref: &ImageReference,
248 staged: Option<StagedDeployment>,
249 boot_type: BootType,
250 boot_digest: String,
251 manifest_digest: &str,
252 allow_missing_fsverity: bool,
253) -> Result<()> {
254 let state_path = root_path
255 .join(STATE_DIR_RELATIVE)
256 .join(deployment_id.to_hex());
257
258 create_dir_all(state_path.join("etc"))?;
259
260 let actual_var_path = root_path.join(SHARED_VAR_PATH);
261 create_dir_all(&actual_var_path)?;
262
263 symlink(
264 path_relative_to(state_path.as_std_path(), actual_var_path.as_std_path())
265 .context("Getting var symlink path")?,
266 state_path.join("var"),
267 )
268 .context("Failed to create symlink for /var")?;
269
270 initialize_state(
271 &root_path,
272 &deployment_id.to_hex(),
273 &state_path,
274 staged.is_none(),
275 allow_missing_fsverity,
276 )?;
277
278 let imgref = target_imgref.to_image_proxy_ref()?;
279
280 let mut config = tini::Ini::new().section("origin").item(
281 ORIGIN_CONTAINER,
282 format!("ostree-unverified-image:{imgref}"),
284 );
285
286 config = config
287 .section(ORIGIN_KEY_BOOT)
288 .item(ORIGIN_KEY_BOOT_TYPE, boot_type);
289
290 config = config
291 .section(ORIGIN_KEY_BOOT)
292 .item(ORIGIN_KEY_BOOT_DIGEST, boot_digest);
293
294 config = config
297 .section(ORIGIN_KEY_IMAGE)
298 .item(ORIGIN_KEY_MANIFEST_DIGEST, manifest_digest);
299
300 let state_dir =
301 Dir::open_ambient_dir(&state_path, ambient_authority()).context("Opening state dir")?;
302
303 state_dir
304 .atomic_write(
305 format!("{}.origin", deployment_id.to_hex()),
306 config.to_string().as_bytes(),
307 )
308 .context("Failed to write to .origin file")?;
309
310 if let Some(staged) = staged {
311 std::fs::create_dir_all(COMPOSEFS_TRANSIENT_STATE_DIR)
312 .with_context(|| format!("Creating {COMPOSEFS_TRANSIENT_STATE_DIR}"))?;
313
314 let staged_depl_dir =
315 Dir::open_ambient_dir(COMPOSEFS_TRANSIENT_STATE_DIR, ambient_authority())
316 .with_context(|| format!("Opening {COMPOSEFS_TRANSIENT_STATE_DIR}"))?;
317
318 staged_depl_dir
319 .atomic_write(
320 COMPOSEFS_STAGED_DEPLOYMENT_FNAME,
321 staged
322 .to_canon_json_vec()
323 .context("Failed to serialize staged deployment JSON")?,
324 )
325 .with_context(|| format!("Writing to {COMPOSEFS_STAGED_DEPLOYMENT_FNAME}"))?;
326 }
327
328 Ok(())
329}
330
331pub(crate) fn composefs_usr_overlay(access_mode: FilesystemOverlayAccessMode) -> Result<()> {
332 let status = get_composefs_usr_overlay_status()?;
333 if status.is_some() {
334 println!("An overlayfs is already mounted on /usr");
335 return Ok(());
336 }
337
338 let usr = Dir::open_ambient_dir("/usr", ambient_authority()).context("Opening /usr")?;
339
340 let mount_attr_flags = match access_mode {
341 FilesystemOverlayAccessMode::ReadOnly => Some(MountAttrFlags::MOUNT_ATTR_RDONLY),
342 FilesystemOverlayAccessMode::ReadWrite => None,
343 };
344
345 let overlay_fd = overlay_transient(usr.as_fd(), "transient", mount_attr_flags)?;
346 mount_at_wrapper(overlay_fd, &usr, ".").context("Attaching /usr overlay")?;
347
348 println!("A {} overlayfs is now mounted on /usr", access_mode);
349 println!("All changes there will be discarded on reboot.");
350
351 Ok(())
352}
353
354pub(crate) fn get_composefs_usr_overlay_status() -> Result<Option<FilesystemOverlay>> {
355 let usr = Dir::open_ambient_dir("/usr", ambient_authority()).context("Opening /usr")?;
356 let is_usr_mounted = usr
357 .is_mountpoint(".")
358 .context("Failed to get mount details for /usr")?
359 .ok_or_else(|| anyhow::anyhow!("Failed to get mountinfo"))?;
360
361 if is_usr_mounted {
362 let st =
363 rustix::fs::fstatvfs(usr.as_fd()).context("Failed to get filesystem info for /usr")?;
364 let permissions = if st.f_flag.contains(StatVfsMountFlags::RDONLY) {
365 FilesystemOverlayAccessMode::ReadOnly
366 } else {
367 FilesystemOverlayAccessMode::ReadWrite
368 };
369 Ok(Some(FilesystemOverlay {
371 access_mode: permissions,
372 persistence: FilesystemOverlayPersistence::Transient,
373 }))
374 } else {
375 Ok(None)
376 }
377}