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