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;
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 { efi } => {
73 if efi.as_str().contains(&*booted_cfs.cmdline.digest) {
74 return Ok(entry);
75 }
76 }
77
78 BLSConfigType::NonEFI { options, .. } => {
79 let Some(opts) = options else {
80 anyhow::bail!("options not found in bls config")
81 };
82
83 let cfs_cmdline = ComposefsCmdline::find_in_cmdline(&Cmdline::from(opts))
84 .ok_or_else(|| anyhow::anyhow!("composefs param not found in cmdline"))?;
85
86 if cfs_cmdline.digest == booted_cfs.cmdline.digest {
87 return Ok(entry);
88 }
89 }
90
91 BLSConfigType::Unknown => anyhow::bail!("Unknown BLS Config type"),
92 };
93 }
94
95 Err(anyhow::anyhow!("Booted BLS not found"))
96}
97
98#[context("Initializing /etc and /var for state")]
101pub(crate) fn initialize_state(
102 sysroot_path: &Utf8PathBuf,
103 erofs_id: &String,
104 state_path: &Utf8PathBuf,
105 initialize_var: bool,
106 allow_missing_fsverity: bool,
107) -> Result<()> {
108 let sysroot_fd = open(
109 sysroot_path.as_std_path(),
110 OFlags::PATH | OFlags::DIRECTORY | OFlags::CLOEXEC,
111 Mode::empty(),
112 )
113 .context("Opening sysroot")?;
114
115 let composefs_fd = bootc_initramfs_setup::mount_composefs_image(
116 &sysroot_fd,
117 &erofs_id,
118 allow_missing_fsverity,
119 )?;
120
121 let tempdir = TempMount::mount_fd(composefs_fd)?;
122
123 if initialize_var {
125 Command::new("cp")
126 .args([
127 "-a",
128 "--remove-destination",
129 &format!("{}/var/.", tempdir.dir.path().as_str()?),
130 &format!("{state_path}/var/."),
131 ])
132 .run_capture_stderr()?;
133 }
134
135 let cp_ret = Command::new("cp")
136 .args([
137 "-a",
138 "--remove-destination",
139 &format!("{}/etc/.", tempdir.dir.path().as_str()?),
140 &format!("{state_path}/etc/."),
141 ])
142 .run_capture_stderr();
143
144 cp_ret
145}
146
147fn add_update_in_origin(
150 storage: &Storage,
151 deployment_id: &str,
152 section: &str,
153 kv_pairs: &[(&str, &str)],
154) -> Result<()> {
155 let path = Path::new(STATE_DIR_RELATIVE).join(deployment_id);
156
157 let state_dir = storage
158 .physical_root
159 .open_dir(path)
160 .context("Opening state dir")?;
161
162 let origin_filename = format!("{deployment_id}.origin");
163
164 let origin_file = state_dir
165 .read_to_string(&origin_filename)
166 .context("Reading origin file")?;
167
168 let mut ini =
169 tini::Ini::from_string(&origin_file).context("Failed to parse file origin file as ini")?;
170
171 for (key, value) in kv_pairs {
172 ini = ini.section(section).item(*key, *value);
173 }
174
175 state_dir
176 .atomic_replace_with(origin_filename, move |f| -> std::io::Result<_> {
177 f.write_all(ini.to_string().as_bytes())?;
178 f.flush()?;
179
180 let perms = Permissions::from_mode(0o644);
181 f.get_mut().as_file_mut().set_permissions(perms)?;
182
183 Ok(())
184 })
185 .context("Writing to origin file")?;
186
187 Ok(())
188}
189
190pub(crate) fn update_boot_digest_in_origin(
191 storage: &Storage,
192 digest: &str,
193 boot_digest: &str,
194) -> Result<()> {
195 add_update_in_origin(
196 storage,
197 digest,
198 ORIGIN_KEY_BOOT,
199 &[(ORIGIN_KEY_BOOT_DIGEST, boot_digest)],
200 )
201}
202
203#[context("Writing composefs state")]
230pub(crate) async fn write_composefs_state(
231 root_path: &Utf8PathBuf,
232 deployment_id: &Sha512HashValue,
233 target_imgref: &ImageReference,
234 staged: Option<StagedDeployment>,
235 boot_type: BootType,
236 boot_digest: String,
237 manifest_digest: &str,
238 allow_missing_fsverity: bool,
239) -> Result<()> {
240 let state_path = root_path
241 .join(STATE_DIR_RELATIVE)
242 .join(deployment_id.to_hex());
243
244 create_dir_all(state_path.join("etc"))?;
245
246 let actual_var_path = root_path.join(SHARED_VAR_PATH);
247 create_dir_all(&actual_var_path)?;
248
249 symlink(
250 path_relative_to(state_path.as_std_path(), actual_var_path.as_std_path())
251 .context("Getting var symlink path")?,
252 state_path.join("var"),
253 )
254 .context("Failed to create symlink for /var")?;
255
256 initialize_state(
257 &root_path,
258 &deployment_id.to_hex(),
259 &state_path,
260 staged.is_none(),
261 allow_missing_fsverity,
262 )?;
263
264 let imgref = target_imgref.to_image_proxy_ref()?;
265
266 let mut config = tini::Ini::new().section("origin").item(
267 ORIGIN_CONTAINER,
268 format!("ostree-unverified-image:{imgref}"),
270 );
271
272 config = config
273 .section(ORIGIN_KEY_BOOT)
274 .item(ORIGIN_KEY_BOOT_TYPE, boot_type);
275
276 config = config
277 .section(ORIGIN_KEY_BOOT)
278 .item(ORIGIN_KEY_BOOT_DIGEST, boot_digest);
279
280 config = config
283 .section(ORIGIN_KEY_IMAGE)
284 .item(ORIGIN_KEY_MANIFEST_DIGEST, manifest_digest);
285
286 let state_dir =
287 Dir::open_ambient_dir(&state_path, ambient_authority()).context("Opening state dir")?;
288
289 state_dir
290 .atomic_write(
291 format!("{}.origin", deployment_id.to_hex()),
292 config.to_string().as_bytes(),
293 )
294 .context("Failed to write to .origin file")?;
295
296 if let Some(staged) = staged {
297 std::fs::create_dir_all(COMPOSEFS_TRANSIENT_STATE_DIR)
298 .with_context(|| format!("Creating {COMPOSEFS_TRANSIENT_STATE_DIR}"))?;
299
300 let staged_depl_dir =
301 Dir::open_ambient_dir(COMPOSEFS_TRANSIENT_STATE_DIR, ambient_authority())
302 .with_context(|| format!("Opening {COMPOSEFS_TRANSIENT_STATE_DIR}"))?;
303
304 staged_depl_dir
305 .atomic_write(
306 COMPOSEFS_STAGED_DEPLOYMENT_FNAME,
307 staged
308 .to_canon_json_vec()
309 .context("Failed to serialize staged deployment JSON")?,
310 )
311 .with_context(|| format!("Writing to {COMPOSEFS_STAGED_DEPLOYMENT_FNAME}"))?;
312 }
313
314 Ok(())
315}
316
317pub(crate) fn composefs_usr_overlay(access_mode: FilesystemOverlayAccessMode) -> Result<()> {
318 let status = get_composefs_usr_overlay_status()?;
319 if status.is_some() {
320 println!("An overlayfs is already mounted on /usr");
321 return Ok(());
322 }
323
324 let usr = Dir::open_ambient_dir("/usr", ambient_authority()).context("Opening /usr")?;
325
326 let mount_attr_flags = match access_mode {
327 FilesystemOverlayAccessMode::ReadOnly => Some(MountAttrFlags::MOUNT_ATTR_RDONLY),
328 FilesystemOverlayAccessMode::ReadWrite => None,
329 };
330
331 let overlay_fd = overlay_transient(usr.as_fd(), "transient", mount_attr_flags)?;
332 mount_at_wrapper(overlay_fd, &usr, ".").context("Attaching /usr overlay")?;
333
334 println!("A {} overlayfs is now mounted on /usr", access_mode);
335 println!("All changes there will be discarded on reboot.");
336
337 Ok(())
338}
339
340pub(crate) fn get_composefs_usr_overlay_status() -> Result<Option<FilesystemOverlay>> {
341 let usr = Dir::open_ambient_dir("/usr", ambient_authority()).context("Opening /usr")?;
342 let is_usr_mounted = usr
343 .is_mountpoint(".")
344 .context("Failed to get mount details for /usr")?
345 .ok_or_else(|| anyhow::anyhow!("Failed to get mountinfo"))?;
346
347 if is_usr_mounted {
348 let st =
349 rustix::fs::fstatvfs(usr.as_fd()).context("Failed to get filesystem info for /usr")?;
350 let permissions = if st.f_flag.contains(StatVfsMountFlags::RDONLY) {
351 FilesystemOverlayAccessMode::ReadOnly
352 } else {
353 FilesystemOverlayAccessMode::ReadWrite
354 };
355 Ok(Some(FilesystemOverlay {
357 access_mode: permissions,
358 persistence: FilesystemOverlayPersistence::Transient,
359 }))
360 } else {
361 Ok(None)
362 }
363}