arcbox_snapshot/snapshot_cow/
persistence.rs1use std::collections::HashSet;
2use std::collections::hash_map::Entry;
3use std::io::Write;
4use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
5use std::path::{Path, PathBuf};
6use std::process::Command;
7
8use tracing::{debug, info, warn};
9
10use super::block_tools::loop_backing_file;
11use super::{
12 COW_FILE_PREFIX, COW_FILE_SUFFIX, CowHandle, CowManager, DM_NAME_PREFIX, TEMPLATE_LOOP_DIR,
13 TEMPLATE_MARKER_TEMP_PREFIX, TEMPLATE_PENDING_PREFIX, TemplateEntry, dmsetup_remove,
14};
15use crate::error::{Result, SnapshotError};
16
17#[derive(Clone, Default)]
18pub(super) struct SetupOrphan {
19 pending_template_marker: Option<PathBuf>,
20 template_loop: Option<(String, PathBuf)>,
21 template_path: Option<PathBuf>,
22 dm_name: Option<String>,
23 cow_loop: Option<String>,
24 cow_file: Option<PathBuf>,
25}
26
27impl SetupOrphan {
28 fn is_empty(&self) -> bool {
29 self.pending_template_marker.is_none()
30 && self.template_loop.is_none()
31 && self.template_path.is_none()
32 && self.dm_name.is_none()
33 && self.cow_loop.is_none()
34 && self.cow_file.is_none()
35 }
36}
37
38impl CowManager {
39 pub fn adopt(&self, handle: &CowHandle) -> Result<()> {
51 #[cfg(any(test, feature = "test-probe"))]
52 if self.test_probe.is_some() {
53 return Ok(());
55 }
56 if !Path::new(&handle.dm_device).exists() {
59 return Err(SnapshotError::FailedPrecondition(format!(
60 "sandbox device {} is gone",
61 handle.dm_device
62 )));
63 }
64 let loop_device = match loop_devices_for_backing_sync(&handle.template_path)?.as_slice() {
68 [device] => device.clone(),
69 [] => {
70 return Err(SnapshotError::FailedPrecondition(format!(
71 "no loop device backs template {}",
72 handle.template_path.display()
73 )));
74 }
75 devices => {
79 return Err(SnapshotError::FailedPrecondition(format!(
80 "template {} is attached through {}",
81 handle.template_path.display(),
82 devices.join(", ")
83 )));
84 }
85 };
86 let sectors = self.tools.device_sectors(&loop_device)?;
91 self.take_template_ref(&handle.template_path, &loop_device, sectors)?;
92 info!(
93 dm = %handle.dm_name,
94 template = %handle.template_path.display(),
95 loop_device = %loop_device,
96 "adopted a live sandbox's dm-snapshot"
97 );
98 Ok(())
99 }
100
101 pub(super) fn take_template_ref(
107 &self,
108 template: &Path,
109 loop_device: &str,
110 sectors: u64,
111 ) -> Result<()> {
112 match self.templates.lock().unwrap().entry(template.to_path_buf()) {
113 Entry::Occupied(mut occupied) => {
114 let entry = occupied.get_mut();
115 if entry.refcount == 0 {
119 return Err(SnapshotError::Unavailable(format!(
120 "template {} still owns resources from an incomplete CoW setup",
121 template.display()
122 )));
123 }
124 if entry.loop_device != loop_device {
128 return Err(SnapshotError::FailedPrecondition(format!(
129 "template {} is held through {} but backs {loop_device}",
130 template.display(),
131 entry.loop_device
132 )));
133 }
134 entry.refcount += 1;
135 }
136 Entry::Vacant(vacant) => {
137 vacant.insert(TemplateEntry {
138 loop_device: loop_device.to_owned(),
139 sectors,
140 refcount: 1,
141 });
142 }
143 }
144 Ok(())
145 }
146
147 pub fn reconcile_stale(
173 &self,
174 keep_cow_files: &HashSet<String>,
175 live_devices: &HashSet<String>,
176 ) -> Result<()> {
177 let dmsetup = self.dmsetup_bin.as_deref();
178
179 if let Some(dmsetup) = dmsetup {
182 let output =
183 run_sync_checked(Command::new(dmsetup).args(["ls", "--target", "snapshot"]))?;
184 let stdout = String::from_utf8_lossy(&output.stdout);
185 for line in stdout.lines() {
186 if let Some(name) = line.split_whitespace().next()
187 && let Some(owner) = name.strip_prefix(DM_NAME_PREFIX)
188 {
189 if live_devices.contains(owner) {
193 debug!(dm = %name, "keeping live sandbox dm-snapshot");
194 continue;
195 }
196 debug!(dm = %name, "removing stale dm-snapshot");
197 run_sync_checked(Command::new(dmsetup).args(["remove", "--retry", name]))?;
199 }
200 }
201 }
202
203 for entry in std::fs::read_dir(&self.cow_dir)? {
206 let entry = entry?;
207 let path = entry.path();
208 let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
209 continue;
210 };
211 if let Some(rest) = name.strip_prefix(COW_FILE_PREFIX) {
212 let id = rest.strip_suffix(COW_FILE_SUFFIX).unwrap_or(rest);
213 if keep_cow_files.contains(id) || live_devices.contains(id) {
214 debug!(file = %path.display(), "keeping retained sandbox cow file");
215 continue;
216 }
217 for loop_device in loop_devices_for_backing_sync(&path)? {
218 self.tools.detach_loop(&loop_device)?;
219 }
220 debug!(file = %path.display(), "removing stale cow file");
221 remove_file_durable(&path)?;
222 }
223 }
224
225 self.cleanup_stale_template_markers()?;
238 Ok(())
239 }
240
241 pub(super) fn template_marker_path(&self, loop_dev: &str) -> Option<PathBuf> {
245 let basename = Path::new(loop_dev).file_name()?;
246 Some(self.cow_dir.join(TEMPLATE_LOOP_DIR).join(basename))
247 }
248
249 pub(super) fn write_template_marker(&self, loop_dev: &str, template_path: &Path) -> Result<()> {
250 let Some(marker) = self.template_marker_path(loop_dev) else {
251 return Err(SnapshotError::DeviceMapper(format!(
252 "unparseable template loop device: {loop_dev}"
253 )));
254 };
255 write_owner_marker(&marker, template_path)?;
256 Ok(())
257 }
258
259 pub(super) fn write_template_pending(
260 &self,
261 sandbox_id: &str,
262 template_path: &Path,
263 ) -> Result<PathBuf> {
264 let marker = self
265 .cow_dir
266 .join(TEMPLATE_LOOP_DIR)
267 .join(format!("{TEMPLATE_PENDING_PREFIX}{sandbox_id}"));
268 write_owner_marker(&marker, template_path)?;
269 Ok(marker)
270 }
271
272 pub(super) async fn abort_template_acquisition(
273 &self,
274 sandbox_id: &str,
275 pending: &Path,
276 loop_device: Option<&str>,
277 template_path: &Path,
278 error: SnapshotError,
279 ) -> SnapshotError {
280 let mut failures = Vec::new();
281 let detached = match loop_device {
282 Some(loop_device) => match self.detach_template_loop(loop_device).await {
283 Ok(()) => true,
284 Err(cleanup) => {
285 failures.push(cleanup.to_string());
286 false
287 }
288 },
289 None => true,
290 };
291 if detached && let Err(cleanup) = clear_owner_marker(pending) {
292 failures.push(cleanup.to_string());
293 }
294 if !failures.is_empty() {
295 self.setup_orphans.lock().unwrap().insert(
296 sandbox_id.to_owned(),
297 SetupOrphan {
298 pending_template_marker: Some(pending.to_path_buf()),
299 template_loop: loop_device
300 .map(|loop_device| (loop_device.to_owned(), template_path.to_path_buf())),
301 ..Default::default()
302 },
303 );
304 }
305 incomplete_cleanup(error, failures)
306 }
307
308 pub(super) fn cleanup_stale_template_markers(&self) -> Result<()> {
309 let dir = self.cow_dir.join(TEMPLATE_LOOP_DIR);
310 let entries = match std::fs::read_dir(&dir) {
311 Ok(entries) => entries,
312 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
313 Err(error) => return Err(error.into()),
314 };
315 for entry in entries {
316 let entry = entry?;
317 let marker_path = entry.path();
318 let Some(loop_basename) = marker_path.file_name().and_then(|n| n.to_str()) else {
319 return Err(SnapshotError::DeviceMapper(format!(
320 "invalid template marker path: {}",
321 marker_path.display()
322 )));
323 };
324 if loop_basename.starts_with(TEMPLATE_MARKER_TEMP_PREFIX) {
325 clear_owner_marker(&marker_path)?;
326 continue;
327 }
328 let expected_backing = std::fs::read_to_string(&marker_path)?.trim().to_string();
329 if expected_backing.is_empty() {
330 return Err(SnapshotError::DeviceMapper(format!(
331 "empty template marker: {}",
332 marker_path.display()
333 )));
334 }
335 if self
347 .templates
348 .lock()
349 .unwrap()
350 .contains_key(Path::new(&expected_backing))
351 {
352 debug!(
353 marker = %marker_path.display(),
354 template = %expected_backing,
355 "keeping live template loop"
356 );
357 continue;
358 }
359 if loop_basename.starts_with(TEMPLATE_PENDING_PREFIX) {
360 for loop_device in loop_devices_for_backing_sync(Path::new(&expected_backing))? {
361 self.tools.detach_loop(&loop_device)?;
362 }
363 clear_owner_marker(&marker_path)?;
364 continue;
365 }
366
367 let dev = format!("/dev/{loop_basename}");
368 let expected = backing_spellings(Path::new(&expected_backing));
377 let actual_backing = loop_backing_file(&dev)?;
378
379 if actual_backing
380 .as_ref()
381 .is_some_and(|actual| expected.contains(actual))
382 {
383 debug!(dev = %dev, "detaching stale template loop");
384 self.tools.detach_loop(&dev)?;
385 } else {
386 debug!(
387 dev = %dev,
388 expected = %expected_backing,
389 actual = ?actual_backing,
390 "skipping stale template loop: backing mismatch"
391 );
392 }
393
394 clear_owner_marker(&marker_path)?;
395 }
396 std::fs::File::open(&dir)?.sync_all()?;
397 Ok(())
398 }
399
400 pub(super) async fn rollback_setup(
401 &self,
402 sandbox_id: &str,
403 template_path: &Path,
404 dm_name: Option<&str>,
405 cow_loop: Option<&str>,
406 cow_file: Option<&Path>,
407 error: SnapshotError,
408 ) -> SnapshotError {
409 let mut failures = Vec::new();
410 let mut orphan = SetupOrphan::default();
411 let dm_removed = match dm_name {
412 Some(dm_name) if super::dm_present(dm_name) => {
417 let cleanup = match self.dmsetup_bin.as_deref() {
418 Some(dmsetup) => dmsetup_remove(dmsetup, dm_name).await,
419 None => Err(SnapshotError::DeviceMapper(
420 "dmsetup binary not found".into(),
421 )),
422 };
423 match cleanup {
424 Ok(()) => true,
425 Err(cleanup) => {
426 failures.push(cleanup.to_string());
427 orphan.dm_name = Some(dm_name.to_owned());
428 false
429 }
430 }
431 }
432 _ => true,
433 };
434 let cow_detached = match (dm_removed, cow_loop) {
435 (false, Some(loop_device)) => {
436 orphan.cow_loop = Some(loop_device.to_owned());
437 false
438 }
439 (true, Some(loop_device)) => match self.detach_loop(loop_device).await {
440 Ok(()) => true,
441 Err(cleanup) => {
442 failures.push(cleanup.to_string());
443 orphan.cow_loop = Some(loop_device.to_owned());
444 false
445 }
446 },
447 (_, None) => true,
448 };
449 if let Some(cow_file) = cow_file {
450 if cow_detached {
451 if let Err(cleanup) = remove_file_durable(cow_file) {
452 failures.push(format!("remove {}: {cleanup}", cow_file.display()));
453 orphan.cow_file = Some(cow_file.to_path_buf());
454 }
455 } else if !cow_detached {
456 orphan.cow_file = Some(cow_file.to_path_buf());
457 }
458 }
459 if let Err(cleanup) = self.release_template_ref(template_path, false).await {
460 failures.push(cleanup.to_string());
461 orphan.template_path = Some(template_path.to_path_buf());
462 }
463 if !orphan.is_empty() {
464 self.setup_orphans
465 .lock()
466 .unwrap()
467 .insert(sandbox_id.to_owned(), orphan);
468 }
469 incomplete_cleanup(error, failures)
470 }
471
472 pub async fn cleanup_setup_orphan(&self, sandbox_id: &str) -> Result<()> {
473 let Some(orphan) = self.setup_orphans.lock().unwrap().get(sandbox_id).cloned() else {
474 return Ok(());
475 };
476
477 if let Some(dm_name) = &orphan.dm_name
478 && Path::new(&format!("/dev/mapper/{dm_name}")).exists()
479 {
480 let dmsetup = self
481 .dmsetup_bin
482 .as_deref()
483 .ok_or_else(|| SnapshotError::DeviceMapper("dmsetup binary not found".into()))?;
484 dmsetup_remove(dmsetup, dm_name).await?;
485 }
486 if let (Some(cow_loop), Some(cow_file)) = (&orphan.cow_loop, &orphan.cow_file)
487 && loop_backs_path(cow_loop, cow_file)?
488 {
489 self.detach_loop(cow_loop).await?;
490 }
491 if let Some(cow_file) = &orphan.cow_file {
492 remove_file_durable(cow_file)?;
493 }
494 if let Some(template_path) = &orphan.template_path {
495 self.release_template_ref(template_path, false).await?;
496 }
497 if let Some((template_loop, template_path)) = &orphan.template_loop
498 && loop_backs_path(template_loop, template_path)?
499 {
500 self.detach_template_loop(template_loop).await?;
501 }
502 if let Some(pending) = &orphan.pending_template_marker {
503 clear_owner_marker(pending)?;
504 }
505 self.setup_orphans.lock().unwrap().remove(sandbox_id);
506 Ok(())
507 }
508
509 pub(super) async fn release_template_ref(
516 &self,
517 template_path: &Path,
518 restore_ref_on_failure: bool,
519 ) -> Result<()> {
520 let _losetup_guard = self.losetup_lock.lock().await;
521 let Some(entry) = ({
522 let mut templates = self.templates.lock().unwrap();
523 let Some(entry) = templates.get_mut(template_path) else {
524 return Ok(());
525 };
526 if entry.refcount > 1 {
527 entry.refcount -= 1;
528 return Ok(());
529 }
530 templates.remove(template_path)
531 }) else {
532 return Ok(());
533 };
534
535 if let Err(error) = self.detach_template_loop(&entry.loop_device).await {
536 let mut entry = entry;
537 entry.refcount = usize::from(restore_ref_on_failure);
538 self.templates
539 .lock()
540 .unwrap()
541 .insert(template_path.to_path_buf(), entry);
542 return Err(error);
543 }
544 Ok(())
545 }
546
547 async fn detach_template_loop(&self, loop_device: &str) -> Result<()> {
548 self.detach_loop(loop_device).await?;
549 if let Some(marker) = self.template_marker_path(loop_device)
550 && let Err(error) = clear_owner_marker(&marker)
551 {
552 warn!(
553 marker = %marker.display(),
554 error = %error,
555 "detached template loop but failed to remove recovery marker"
556 );
557 }
558 Ok(())
559 }
560}
561
562fn write_owner_marker(marker: &Path, backing: &Path) -> Result<()> {
563 let parent = marker
564 .parent()
565 .ok_or_else(|| SnapshotError::DeviceMapper("resource marker has no parent".into()))?;
566 let name = marker
567 .file_name()
568 .ok_or_else(|| SnapshotError::DeviceMapper("resource marker has no file name".into()))?;
569 std::fs::create_dir_all(parent)?;
570 std::fs::set_permissions(parent, std::fs::Permissions::from_mode(0o700))?;
571 let temporary = parent.join(format!(
572 "{TEMPLATE_MARKER_TEMP_PREFIX}{}",
573 name.to_string_lossy()
574 ));
575 let mut file = std::fs::OpenOptions::new()
576 .write(true)
577 .create(true)
578 .truncate(true)
579 .mode(0o600)
580 .open(&temporary)?;
581 file.set_permissions(std::fs::Permissions::from_mode(0o600))?;
582 file.write_all(backing.to_string_lossy().as_bytes())?;
583 file.sync_all()?;
584 std::fs::rename(&temporary, marker)?;
585 std::fs::File::open(parent)?.sync_all()?;
586 if let Some(grandparent) = parent.parent() {
587 std::fs::File::open(grandparent)?.sync_all()?;
588 }
589 Ok(())
590}
591
592pub(super) fn clear_owner_marker(marker: &Path) -> Result<()> {
593 match std::fs::remove_file(marker) {
594 Ok(()) => {}
595 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
596 Err(error) => return Err(error.into()),
597 }
598 let parent = marker
599 .parent()
600 .ok_or_else(|| SnapshotError::DeviceMapper("resource marker has no parent".into()))?;
601 std::fs::File::open(parent)?.sync_all()?;
602 Ok(())
603}
604
605pub(super) fn remove_file_durable(path: &Path) -> Result<()> {
606 match std::fs::remove_file(path) {
607 Ok(()) => {}
608 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
609 Err(error) => return Err(error.into()),
610 }
611 let parent = path
612 .parent()
613 .ok_or_else(|| SnapshotError::DeviceMapper("owned file has no parent".into()))?;
614 std::fs::File::open(parent)?.sync_all()?;
615 Ok(())
616}
617
618pub(super) fn backing_spellings(backing: &Path) -> Vec<String> {
630 let literal = backing.to_string_lossy().into_owned();
631 let resolved = std::fs::canonicalize(backing)
632 .map(|path| path.to_string_lossy().into_owned())
633 .ok()
634 .filter(|resolved| *resolved != literal);
635 std::iter::once(literal).chain(resolved).collect()
636}
637
638pub(super) fn loop_backs_path(loop_device: &str, backing: &Path) -> Result<bool> {
639 let expected = backing_spellings(backing);
640 Ok(loop_backing_file(loop_device)?.is_some_and(|actual| expected.contains(&actual)))
641}
642
643pub(super) fn loop_devices_for_backing_sync(backing: &Path) -> Result<Vec<String>> {
644 let expected = backing_spellings(backing);
645 let mut devices = Vec::new();
646 for entry in std::fs::read_dir("/sys/block")? {
647 let entry = entry?;
648 let Some(name) = entry.file_name().to_str().map(str::to_owned) else {
649 continue;
650 };
651 let Some(index) = name.strip_prefix("loop") else {
652 continue;
653 };
654 if index.is_empty() || !index.bytes().all(|byte| byte.is_ascii_digit()) {
655 continue;
656 }
657 let device = format!("/dev/{name}");
658 if loop_backing_file(&device)?.is_some_and(|actual| expected.contains(&actual)) {
659 devices.push(device);
660 }
661 }
662 devices.sort();
663 Ok(devices)
664}
665
666fn incomplete_cleanup(
667 error: SnapshotError,
668 failures: impl IntoIterator<Item = String>,
669) -> SnapshotError {
670 let failures: Vec<_> = failures.into_iter().collect();
671 if failures.is_empty() {
672 error
673 } else {
674 SnapshotError::Unavailable(format!(
675 "{error}; resource rollback is incomplete: {}",
676 failures.join("; ")
677 ))
678 }
679}
680
681fn run_sync_checked(command: &mut Command) -> Result<std::process::Output> {
682 let output = command
683 .output()
684 .map_err(|error| SnapshotError::DeviceMapper(format!("command spawn: {error}")))?;
685 if output.status.success() {
686 Ok(output)
687 } else {
688 Err(SnapshotError::DeviceMapper(
689 String::from_utf8_lossy(&output.stderr).trim().to_owned(),
690 ))
691 }
692}