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_mount::tempmount::TempMount;
9use bootc_utils::CommandRunExt;
10use camino::Utf8PathBuf;
11use canon_json::CanonJsonSerialize;
12use cap_std_ext::cap_std::ambient_authority;
13use cap_std_ext::cap_std::fs::{Dir, Permissions, PermissionsExt};
14use cap_std_ext::dirext::CapStdExtDirExt;
15use composefs::fsverity::{FsVerityHashValue, Sha512HashValue};
16use composefs_ctl::composefs;
17use fn_error_context::context;
18use linux_kernel_cmdline::utf8::Cmdline;
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 read_boot_type(sysroot: &Dir, deployment_id: &str) -> Result<Option<BootType>> {
68 Sha512HashValue::from_hex(deployment_id).context("Invalid deployment digest")?;
69 let Some(origin) = read_origin(sysroot, deployment_id)? else {
70 return Ok(None);
71 };
72 origin
73 .get::<String>(ORIGIN_KEY_BOOT, ORIGIN_KEY_BOOT_TYPE)
74 .map(|value| BootType::try_from(value.as_str()))
75 .transpose()
76}
77
78pub(crate) fn get_booted_bls(boot_dir: &Dir, booted_cfs: &BootedComposefs) -> Result<BLSConfig> {
79 let sorted_entries = get_sorted_type1_boot_entries(boot_dir, true)?;
80
81 for entry in sorted_entries {
82 match &entry.cfg_type {
83 BLSConfigType::EFI { key } => {
84 let path = match key {
85 EFIKey::Efi(path) | EFIKey::Uki(path) => path,
86 };
87 if path.as_str().contains(&*booted_cfs.cmdline.digest) {
88 return Ok(entry);
89 }
90 }
91
92 BLSConfigType::NonEFI { options, .. } => {
93 let Some(opts) = options else {
94 anyhow::bail!("options not found in bls config")
95 };
96
97 let cfs_cmdline = ComposefsCmdline::find_in_cmdline(&Cmdline::from(opts))?
98 .ok_or_else(|| anyhow::anyhow!("composefs param not found in cmdline"))?;
99
100 if cfs_cmdline.digest == booted_cfs.cmdline.digest {
101 return Ok(entry);
102 }
103 }
104
105 BLSConfigType::Unknown => anyhow::bail!("Unknown BLS Config type"),
106 };
107 }
108
109 Err(anyhow::anyhow!("Booted BLS not found"))
110}
111
112#[context("Initializing /etc and /var for state")]
115pub(crate) fn initialize_state(
116 sysroot_path: &Utf8PathBuf,
117 erofs_id: &String,
118 state_path: &Utf8PathBuf,
119 initialize_var: bool,
120 allow_missing_fsverity: bool,
121) -> Result<()> {
122 let sysroot_fd = open(
123 sysroot_path.as_std_path(),
124 OFlags::PATH | OFlags::DIRECTORY | OFlags::CLOEXEC,
125 Mode::empty(),
126 )
127 .context("Opening sysroot")?;
128
129 let composefs_fd = bootc_initramfs_setup::mount_composefs_image(
130 &sysroot_fd,
131 &erofs_id,
132 allow_missing_fsverity,
133 )?;
134
135 let tempdir = TempMount::mount_fd(composefs_fd)?;
136
137 if initialize_var {
139 Command::new("cp")
140 .args([
141 "-a",
142 "--remove-destination",
143 &format!("{}/var/.", tempdir.dir.path().as_str()?),
144 &format!("{state_path}/var/."),
145 ])
146 .run_capture_stderr()?;
147 }
148
149 Command::new("cp")
150 .args([
151 "-a",
152 "--remove-destination",
153 &format!("{}/etc/.", tempdir.dir.path().as_str()?),
154 &format!("{state_path}/etc/."),
155 ])
156 .run_capture_stderr()?;
157
158 let state_etc = Dir::open_ambient_dir(format!("{state_path}/etc"), ambient_authority())
164 .context("Opening state etc dir")?;
165 state_etc
166 .remove_file_optional(".updated")
167 .context("Removing /etc/.updated")?;
168
169 Ok(())
170}
171
172fn add_update_in_origin(
175 storage: &Storage,
176 deployment_id: &str,
177 section: &str,
178 kv_pairs: &[(&str, &str)],
179) -> Result<()> {
180 let path = Path::new(STATE_DIR_RELATIVE).join(deployment_id);
181
182 let state_dir = storage
183 .physical_root
184 .open_dir(path)
185 .context("Opening state dir")?;
186
187 let origin_filename = format!("{deployment_id}.origin");
188
189 let origin_file = state_dir
190 .read_to_string(&origin_filename)
191 .context("Reading origin file")?;
192
193 let mut ini =
194 tini::Ini::from_string(&origin_file).context("Failed to parse file origin file as ini")?;
195
196 for (key, value) in kv_pairs {
197 ini = ini.section(section).item(*key, *value);
198 }
199
200 state_dir
201 .atomic_replace_with(origin_filename, move |f| -> std::io::Result<_> {
202 f.write_all(ini.to_string().as_bytes())?;
203 f.flush()?;
204
205 let perms = Permissions::from_mode(0o644);
206 f.get_mut().as_file_mut().set_permissions(perms)?;
207
208 Ok(())
209 })
210 .context("Writing to origin file")?;
211
212 Ok(())
213}
214
215pub(crate) fn update_boot_digest_in_origin(
216 storage: &Storage,
217 digest: &str,
218 boot_digest: &str,
219) -> Result<()> {
220 add_update_in_origin(
221 storage,
222 digest,
223 ORIGIN_KEY_BOOT,
224 &[(ORIGIN_KEY_BOOT_DIGEST, boot_digest)],
225 )
226}
227
228#[context("Writing composefs state")]
254pub(crate) async fn write_composefs_state(
255 root_path: &Utf8PathBuf,
256 deployment_id: &Sha512HashValue,
257 target_imgref: &ImageReference,
258 initialize_var: bool,
259 boot_type: BootType,
260 boot_digest: String,
261 manifest_digest: &str,
262 allow_missing_fsverity: bool,
263) -> Result<()> {
264 let state_path = root_path
265 .join(STATE_DIR_RELATIVE)
266 .join(deployment_id.to_hex());
267
268 create_dir_all(state_path.join("etc"))?;
269
270 let actual_var_path = root_path.join(SHARED_VAR_PATH);
271 create_dir_all(&actual_var_path)?;
272
273 symlink(
274 path_relative_to(state_path.as_std_path(), actual_var_path.as_std_path())
275 .context("Getting var symlink path")?,
276 state_path.join("var"),
277 )
278 .context("Failed to create symlink for /var")?;
279
280 initialize_state(
281 &root_path,
282 &deployment_id.to_hex(),
283 &state_path,
284 initialize_var,
285 allow_missing_fsverity,
286 )?;
287
288 let imgref = target_imgref.to_image_proxy_ref()?;
289
290 let mut config = tini::Ini::new().section("origin").item(
291 ORIGIN_CONTAINER,
292 format!("ostree-unverified-image:{imgref}"),
294 );
295
296 config = config
297 .section(ORIGIN_KEY_BOOT)
298 .item(ORIGIN_KEY_BOOT_TYPE, boot_type);
299
300 config = config
301 .section(ORIGIN_KEY_BOOT)
302 .item(ORIGIN_KEY_BOOT_DIGEST, boot_digest);
303
304 config = config
307 .section(ORIGIN_KEY_IMAGE)
308 .item(ORIGIN_KEY_MANIFEST_DIGEST, manifest_digest);
309
310 let state_dir =
311 Dir::open_ambient_dir(&state_path, ambient_authority()).context("Opening state dir")?;
312
313 state_dir
314 .atomic_write(
315 format!("{}.origin", deployment_id.to_hex()),
316 config.to_string().as_bytes(),
317 )
318 .context("Failed to write to .origin file")?;
319
320 Ok(())
321}
322
323pub(crate) fn write_staged_deployment(staged: &StagedDeployment) -> Result<()> {
324 std::fs::create_dir_all(COMPOSEFS_TRANSIENT_STATE_DIR)
325 .with_context(|| format!("Creating {COMPOSEFS_TRANSIENT_STATE_DIR}"))?;
326 let dir = Dir::open_ambient_dir(COMPOSEFS_TRANSIENT_STATE_DIR, ambient_authority())
327 .with_context(|| format!("Opening {COMPOSEFS_TRANSIENT_STATE_DIR}"))?;
328 dir.atomic_write(
329 COMPOSEFS_STAGED_DEPLOYMENT_FNAME,
330 staged
331 .to_canon_json_vec()
332 .context("Failed to serialize staged deployment JSON")?,
333 )
334 .with_context(|| format!("Writing to {COMPOSEFS_STAGED_DEPLOYMENT_FNAME}"))
335}
336
337pub(crate) fn remove_staged_deployment() -> Result<()> {
338 let Some(dir) =
339 Dir::open_ambient_dir("/run", ambient_authority())?.open_dir_optional("composefs")?
340 else {
341 return Ok(());
342 };
343 dir.remove_file_optional(COMPOSEFS_STAGED_DEPLOYMENT_FNAME)?;
344 Ok(())
345}
346
347pub(crate) fn composefs_usr_overlay(access_mode: FilesystemOverlayAccessMode) -> Result<()> {
348 let status = get_usr_overlay_status()?;
349 if status.is_some() {
350 println!("An overlayfs is already mounted on /usr");
351 return Ok(());
352 }
353
354 let usr = Dir::open_ambient_dir("/usr", ambient_authority()).context("Opening /usr")?;
355
356 let mount_attr_flags = match access_mode {
357 FilesystemOverlayAccessMode::ReadOnly => Some(MountAttrFlags::MOUNT_ATTR_RDONLY),
358 FilesystemOverlayAccessMode::ReadWrite => None,
359 };
360
361 let overlay_fd = overlay_transient(usr.as_fd(), "transient", mount_attr_flags)?;
362 mount_at_wrapper(overlay_fd, &usr, ".").context("Attaching /usr overlay")?;
363
364 println!("A {} overlayfs is now mounted on /usr", access_mode);
365 println!("All changes there will be discarded on reboot.");
366
367 Ok(())
368}
369
370pub(crate) fn get_usr_overlay_status() -> Result<Option<FilesystemOverlay>> {
371 let usr = Dir::open_ambient_dir("/usr", ambient_authority()).context("Opening /usr")?;
372 let is_usr_mounted = usr
373 .is_mountpoint(".")
374 .context("Failed to get mount details for /usr")?
375 .ok_or_else(|| anyhow::anyhow!("Failed to get mountinfo"))?;
376
377 if is_usr_mounted {
378 let st =
379 rustix::fs::fstatvfs(usr.as_fd()).context("Failed to get filesystem info for /usr")?;
380 let permissions = if st.f_flag.contains(StatVfsMountFlags::RDONLY) {
381 FilesystemOverlayAccessMode::ReadOnly
382 } else {
383 FilesystemOverlayAccessMode::ReadWrite
384 };
385 Ok(Some(FilesystemOverlay {
387 access_mode: permissions,
388 persistence: FilesystemOverlayPersistence::Transient,
389 }))
390 } else {
391 Ok(None)
392 }
393}