1#[cfg(target_os = "macos")]
15mod apfs;
16mod baseline;
17mod builder;
18#[cfg(unix)]
19mod ext4;
20#[cfg(any(target_os = "macos", all(unix, test)))]
21mod ext4_artifact;
22#[cfg(any(target_os = "macos", all(unix, test)))]
23mod ext4_cache;
24#[cfg(target_os = "macos")]
25mod guest_native_ext4;
26#[cfg(target_os = "macos")]
27mod guest_native_migration;
28mod layout;
29#[cfg(any(target_os = "macos", all(unix, test)))]
30mod oci_ext4;
31pub(crate) mod overlay;
32mod provider;
33mod staging_path;
34
35pub use baseline::{
36 create_diff_baseline_if_absent, guest_diff_baseline_required, publish_guest_diff_baseline,
37 walk_rootfs, RootfsFileInfo, DIFF_BASELINE_FILE,
38};
39pub use builder::RootfsBuilder;
40#[cfg(unix)]
41pub use ext4::{
42 publish_ext4_artifact, Ext4Artifact, Ext4ArtifactManifest, Ext4ArtifactOptions,
43 EXT4_ARTIFACT_SCHEMA, EXT4_BUILDER_ID,
44};
45#[cfg(target_os = "macos")]
46pub(crate) use ext4_cache::{Ext4ArtifactCache, Ext4CacheIdentity};
47#[cfg(target_os = "macos")]
48pub use guest_native_ext4::GuestNativeExt4Provider;
49pub use layout::{GuestLayout, GUEST_WORKDIR};
50pub use provider::{
51 default_provider, default_provider_for_box, CopyProvider, OverlayProvider, ResumedRootfs,
52 RootfsArtifactCacheOptions, RootfsFinalizeOptions, RootfsOciPrepareOptions,
53 RootfsPrepareOptions, RootfsProvider, RootfsResumeOptions,
54};
55pub(crate) use provider::{default_provider_for_boot, default_provider_for_box_boot};
56pub(crate) use staging_path::{
57 ensure_directory_transport_is_lossless, host_staging_path, logical_path_for_staged_child,
58 staging_path_map,
59};
60
61pub(crate) fn is_mountpoint(path: &Path) -> bool {
66 overlay::is_mountpoint(path)
67}
68
69#[allow(dead_code)]
72pub(crate) fn writable_layer_quota_supported() -> bool {
73 overlay::writable_layer_quota_supported()
74}
75
76pub(crate) fn cleanup_bounded_writable_layer_for_removal(
85 box_dir: &Path,
86) -> a3s_box_core::error::Result<()> {
87 overlay::cleanup_bounded_writable_layer(box_dir, false)
88}
89
90use std::io::Read;
91use std::path::{Path, PathBuf};
92
93use a3s_box_core::error::{BoxError, Result};
94use a3s_box_core::guest_exec::{
95 GuestTerminalStatus, GUEST_TERMINAL_STATUS_FILE_NAME, MAX_GUEST_TERMINAL_STATUS_BYTES,
96};
97
98enum TerminalStatusRead {
99 Absent,
100 PendingOrInvalid,
101 Complete(GuestTerminalStatus),
102}
103
104fn read_guest_terminal_status(box_dir: &Path) -> TerminalStatusRead {
105 let path = box_dir
106 .join("runtime-control")
107 .join(GUEST_TERMINAL_STATUS_FILE_NAME);
108 let metadata = match std::fs::symlink_metadata(&path) {
109 Ok(metadata) => metadata,
110 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
111 return TerminalStatusRead::Absent;
112 }
113 Err(_) => return TerminalStatusRead::PendingOrInvalid,
114 };
115 if !metadata.is_file()
116 || metadata.file_type().is_symlink()
117 || metadata.len() > MAX_GUEST_TERMINAL_STATUS_BYTES as u64
118 {
119 return TerminalStatusRead::PendingOrInvalid;
120 }
121
122 let mut options = std::fs::OpenOptions::new();
123 options.read(true);
124 #[cfg(unix)]
125 {
126 use std::os::unix::fs::OpenOptionsExt;
127 options.custom_flags(libc::O_CLOEXEC | libc::O_NOFOLLOW);
128 }
129 let Ok(file) = options.open(&path) else {
130 return TerminalStatusRead::PendingOrInvalid;
131 };
132 let mut bytes = Vec::with_capacity(metadata.len() as usize);
133 if file
134 .take(MAX_GUEST_TERMINAL_STATUS_BYTES as u64 + 1)
135 .read_to_end(&mut bytes)
136 .is_err()
137 || bytes.is_empty()
138 || bytes.len() > MAX_GUEST_TERMINAL_STATUS_BYTES
139 {
140 return TerminalStatusRead::PendingOrInvalid;
141 }
142 let Ok(status) = serde_json::from_slice::<GuestTerminalStatus>(&bytes) else {
143 return TerminalStatusRead::PendingOrInvalid;
144 };
145 if status.validate().is_err() {
146 return TerminalStatusRead::PendingOrInvalid;
147 }
148 TerminalStatusRead::Complete(status)
149}
150
151pub(crate) fn guest_rootfs_handoff_complete(box_dir: &Path) -> bool {
154 matches!(
155 read_guest_terminal_status(box_dir),
156 TerminalStatusRead::Complete(GuestTerminalStatus {
157 rootfs_quiesced: true,
158 ..
159 })
160 )
161}
162
163pub fn read_persisted_exit_code(box_dir: &Path) -> Option<i32> {
169 resolve_workload_exit_code(box_dir, None)
170}
171
172pub fn resolve_workload_exit_code(box_dir: &Path, provider_exit_code: Option<i32>) -> Option<i32> {
180 match read_guest_terminal_status(box_dir) {
181 TerminalStatusRead::Complete(status) => return Some(status.exit_code),
182 TerminalStatusRead::PendingOrInvalid => {
185 return provider_exit_code.filter(|exit_code| *exit_code != 0);
186 }
187 TerminalStatusRead::Absent => {}
188 }
189
190 let candidates = [
191 box_dir
192 .join("upper")
193 .join(a3s_box_core::rootfs_metadata::EXIT_CODE_PATH.trim_start_matches('/')),
194 box_dir
195 .join("rootfs")
196 .join(".a3s-rootfs")
197 .join(a3s_box_core::rootfs_metadata::EXIT_CODE_PATH.trim_start_matches('/')),
198 box_dir
199 .join("rootfs")
200 .join(a3s_box_core::rootfs_metadata::EXIT_CODE_PATH.trim_start_matches('/')),
201 ];
202
203 candidates
204 .into_iter()
205 .find_map(|path| {
206 std::fs::read_to_string(path)
207 .ok()
208 .and_then(|contents| contents.trim().parse::<i32>().ok())
209 })
210 .or(provider_exit_code)
211}
212
213pub struct AttachedRootfs {
218 path: std::path::PathBuf,
219 detach_on_drop: bool,
220}
221
222pub fn guest_native_ext4_generation_exists(box_dir: &Path) -> Result<bool> {
228 let path = box_dir.join("rootfs-ext4-v1");
229 match std::fs::symlink_metadata(&path) {
230 Ok(_) => Ok(true),
231 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(false),
232 Err(error) => Err(BoxError::BuildError(format!(
233 "Failed to inspect guest-native rootfs generation {}: {error}",
234 path.display()
235 ))),
236 }
237}
238
239pub fn guest_native_ext4_disk_mib(box_dir: &Path) -> Result<Option<u32>> {
246 if !guest_native_ext4_generation_exists(box_dir)? {
247 return Ok(None);
248 }
249 #[cfg(target_os = "macos")]
250 {
251 let directory = GuestNativeExt4Provider::artifact_directory(box_dir);
252 let (artifact, _) = ext4_artifact::open_ext4_artifact_for_resume(&directory)?;
253 const MIB: u64 = 1024 * 1024;
254 if artifact.manifest.capacity_bytes % MIB != 0 {
255 return Err(BoxError::StateError(format!(
256 "Guest-native rootfs capacity is not MiB-aligned at {}",
257 artifact.disk.display()
258 )));
259 }
260 let disk_mib = u32::try_from(artifact.manifest.capacity_bytes / MIB).map_err(|_| {
261 BoxError::StateError(format!(
262 "Guest-native rootfs capacity exceeds the Box configuration range at {}",
263 artifact.disk.display()
264 ))
265 })?;
266 Ok(Some(disk_mib))
267 }
268 #[cfg(not(target_os = "macos"))]
269 {
270 Err(BoxError::StateError(format!(
271 "Guest-native rootfs state is unsupported on this host: {}",
272 box_dir.join("rootfs-ext4-v1").display()
273 )))
274 }
275}
276
277#[cfg(target_os = "macos")]
283pub(crate) fn open_clean_guest_native_ext4_artifact(
284 artifact_directory: &Path,
285) -> Result<Ext4Artifact> {
286 let (artifact, validation) = ext4_artifact::open_ext4_artifact_for_resume(artifact_directory)?;
287 if validation == ext4::Ext4ResumeValidation::JournalRecoveryRequired {
288 return Err(BoxError::StateError(format!(
289 "Guest-native rootfs at {} needs ext4 journal recovery; start the box and stop it cleanly before creating or restoring a filesystem snapshot",
290 artifact.disk.display()
291 )));
292 }
293 Ok(artifact)
294}
295
296#[cfg(target_os = "macos")]
299pub(crate) fn clone_clean_guest_native_ext4_artifact(
300 artifact_directory: &Path,
301 destination: &Path,
302) -> Result<Ext4Artifact> {
303 let source = open_clean_guest_native_ext4_artifact(artifact_directory)?;
304 let cloned = ext4_cache::clone_artifact(&source, destination)?;
305 let validated = match open_clean_guest_native_ext4_artifact(destination) {
306 Ok(validated) => validated,
307 Err(error) => {
308 let _ = std::fs::remove_dir_all(destination);
309 return Err(error);
310 }
311 };
312 if cloned != validated || source.manifest != validated.manifest {
313 let _ = std::fs::remove_dir_all(destination);
314 return Err(BoxError::StateError(format!(
315 "Cloned guest-native rootfs identity changed at {}",
316 destination.display()
317 )));
318 }
319 Ok(validated)
320}
321
322#[cfg(target_os = "macos")]
323pub(crate) fn guest_native_ext4_sparse_digest(artifact: &Ext4Artifact) -> Result<String> {
324 ext4_cache::sparse_sha256(&artifact.disk, artifact.manifest.capacity_bytes)
325}
326
327#[cfg(target_os = "macos")]
328pub(crate) fn guest_native_ext4_allocated_bytes(artifact_directory: &Path) -> Result<u64> {
329 ext4_cache::allocated_bytes(artifact_directory)
330}
331
332#[cfg(target_os = "macos")]
339pub(crate) fn guest_native_ext4_maintenance_disk(box_dir: &Path) -> Result<PathBuf> {
340 let directory = GuestNativeExt4Provider::artifact_directory(box_dir);
341 let (artifact, validation) = ext4_artifact::open_ext4_artifact_for_resume(&directory)?;
342 if validation == ext4::Ext4ResumeValidation::JournalRecoveryRequired {
343 return Err(BoxError::StateError(format!(
344 "Guest-native rootfs at {} needs ext4 journal recovery; start the box and stop it cleanly before offline diff, export, or commit",
345 artifact.disk.display()
346 )));
347 }
348 Ok(artifact.disk)
349}
350
351impl AttachedRootfs {
352 pub fn path(&self) -> &Path {
353 &self.path
354 }
355}
356
357impl Drop for AttachedRootfs {
358 fn drop(&mut self) {
359 if self.detach_on_drop {
360 unmount_box_rootfs(&self.path);
361 }
362 }
363}
364
365pub fn attach_persistent_rootfs(
371 box_dir: &Path,
372) -> a3s_box_core::error::Result<Option<AttachedRootfs>> {
373 if guest_native_ext4_generation_exists(box_dir)? {
374 return Err(BoxError::StateError(
375 "Guest-native rootfs generations have no host directory attachment; use the trusted maintenance archive path for stopped access"
376 .to_string(),
377 ));
378 }
379
380 #[cfg(target_os = "macos")]
381 {
382 let image = box_dir.join("rootfs-apfs-v2.sparseimage");
383 if !image.is_file() {
384 return Ok(None);
385 }
386 let rootfs = box_dir.join("rootfs");
387 let was_mounted = is_mountpoint(&rootfs);
388 let path = provider::CaseSensitiveApfsProvider.prepare_empty(box_dir)?;
389 Ok(Some(AttachedRootfs {
390 path,
391 detach_on_drop: !was_mounted,
392 }))
393 }
394
395 #[cfg(not(target_os = "macos"))]
396 {
397 let _ = box_dir;
398 Ok(None)
399 }
400}
401
402pub fn stage_box_terminal_rootfs_metadata(box_dir: &Path) -> a3s_box_core::error::Result<()> {
410 if guest_native_ext4_generation_exists(box_dir)? {
411 return Ok(());
414 }
415 let attached = attach_persistent_rootfs(box_dir)?;
416 let mut roots = Vec::<PathBuf>::new();
417 if let Some(rootfs) = attached.as_ref() {
418 roots.push(rootfs.path().to_path_buf());
419 }
420 roots.extend([
421 box_dir.join("rootfs"),
422 box_dir.join("upper"),
423 box_dir.join("merged"),
424 ]);
425 roots.sort();
426 roots.dedup();
427
428 let mut existing_roots = Vec::new();
429 for root in roots {
430 match std::fs::symlink_metadata(&root) {
431 Ok(_) => existing_roots.push(root),
432 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
433 Err(error) => return Err(error.into()),
434 }
435 }
436 stage_metadata_roots(&existing_roots)?;
437 Ok(())
438}
439
440fn stage_metadata_roots(roots: &[PathBuf]) -> std::io::Result<()> {
441 for root in roots {
442 a3s_box_core::rootfs_metadata::stage_terminal_rootfs_metadata_for_boot(root)?;
443 }
444 Ok(())
445}
446
447pub fn unmount_box_overlay(merged: &Path) {
455 for _ in 0..8 {
456 if !is_mountpoint(merged) {
457 break;
458 }
459 if overlay::overlay_unmount(merged).is_err() {
460 break;
461 }
462 }
463}
464
465pub(crate) fn unmount_box_overlay_for_reuse(merged: &Path) -> a3s_box_core::error::Result<()> {
471 for _ in 0..8 {
472 if !is_mountpoint(merged) {
473 return Ok(());
474 }
475 overlay::overlay_unmount_for_reuse(merged)?;
476 }
477
478 if is_mountpoint(merged) {
479 return Err(a3s_box_core::error::BoxError::BuildError(format!(
480 "Overlay at {} remained mounted after synchronous cleanup",
481 merged.display()
482 )));
483 }
484 Ok(())
485}
486
487pub fn unmount_box_rootfs(rootfs: &Path) {
489 #[cfg(target_os = "macos")]
490 {
491 let mountpoint = if rootfs.file_name().is_some_and(|name| name == ".a3s-rootfs") {
495 rootfs.parent().unwrap_or(rootfs)
496 } else {
497 rootfs
498 };
499 if !is_mountpoint(mountpoint) {
500 return;
501 }
502 match std::process::Command::new("hdiutil")
503 .arg("detach")
504 .arg("-quiet")
505 .arg(mountpoint)
506 .status()
507 {
508 Ok(status) if status.success() => {}
509 Ok(status) => tracing::warn!(
510 path = %mountpoint.display(),
511 ?status,
512 "Failed to detach case-sensitive rootfs image"
513 ),
514 Err(error) => tracing::warn!(
515 path = %mountpoint.display(),
516 %error,
517 "Failed to run hdiutil detach"
518 ),
519 }
520 }
521
522 #[cfg(not(target_os = "macos"))]
523 let _ = rootfs;
524}
525
526#[cfg(target_os = "macos")]
531pub(crate) fn unmount_box_rootfs_for_handoff(rootfs: &Path) -> a3s_box_core::error::Result<()> {
532 let mountpoint = if rootfs.file_name().is_some_and(|name| name == ".a3s-rootfs") {
533 rootfs.parent().unwrap_or(rootfs)
534 } else {
535 rootfs
536 };
537 if !is_mountpoint(mountpoint) {
538 return Err(a3s_box_core::error::BoxError::BuildError(format!(
539 "Expected a mounted rootfs staging filesystem at {}",
540 mountpoint.display()
541 )));
542 }
543 let status = std::process::Command::new("hdiutil")
544 .arg("detach")
545 .arg("-quiet")
546 .arg(mountpoint)
547 .status()
548 .map_err(|error| {
549 a3s_box_core::error::BoxError::BuildError(format!(
550 "Failed to run hdiutil detach for {}: {error}",
551 mountpoint.display()
552 ))
553 })?;
554 if !status.success() || is_mountpoint(mountpoint) {
555 return Err(a3s_box_core::error::BoxError::BuildError(format!(
556 "Rootfs staging filesystem remained attached at {} after handoff",
557 mountpoint.display()
558 )));
559 }
560 Ok(())
561}
562
563#[cfg(test)]
564mod tests {
565 use super::*;
566
567 #[test]
568 fn persisted_exit_code_supports_each_rootfs_provider_layout() {
569 for (relative, expected) in [
570 ("upper/.a3s_exit_code", 17),
571 ("rootfs/.a3s_exit_code", 23),
572 ("rootfs/.a3s-rootfs/.a3s_exit_code", 29),
573 ] {
574 let temp = tempfile::tempdir().unwrap();
575 let path = temp.path().join(relative);
576 std::fs::create_dir_all(path.parent().unwrap()).unwrap();
577 std::fs::write(path, format!("{expected}\n")).unwrap();
578
579 assert_eq!(read_persisted_exit_code(temp.path()), Some(expected));
580 }
581 }
582
583 #[test]
584 fn persisted_exit_code_ignores_missing_or_invalid_files() {
585 let temp = tempfile::tempdir().unwrap();
586 assert_eq!(read_persisted_exit_code(temp.path()), None);
587
588 let path = temp.path().join("rootfs/.a3s_exit_code");
589 std::fs::create_dir_all(path.parent().unwrap()).unwrap();
590 std::fs::write(path, "not-an-exit-code").unwrap();
591 assert_eq!(read_persisted_exit_code(temp.path()), None);
592 }
593
594 #[test]
595 fn terminal_status_is_preferred_over_legacy_rootfs_marker() {
596 let temp = tempfile::tempdir().unwrap();
597 let terminal = temp
598 .path()
599 .join("runtime-control")
600 .join(GUEST_TERMINAL_STATUS_FILE_NAME);
601 std::fs::create_dir_all(terminal.parent().unwrap()).unwrap();
602 std::fs::write(
603 &terminal,
604 serde_json::to_vec(&GuestTerminalStatus::new(31)).unwrap(),
605 )
606 .unwrap();
607 let legacy = temp.path().join("rootfs/.a3s_exit_code");
608 std::fs::create_dir_all(legacy.parent().unwrap()).unwrap();
609 std::fs::write(legacy, "7").unwrap();
610
611 assert_eq!(read_persisted_exit_code(temp.path()), Some(31));
612 }
613
614 #[test]
615 fn rootfs_handoff_requires_an_explicit_guest_quiescence_ack() {
616 let temp = tempfile::tempdir().unwrap();
617 let terminal = temp
618 .path()
619 .join("runtime-control")
620 .join(GUEST_TERMINAL_STATUS_FILE_NAME);
621 std::fs::create_dir_all(terminal.parent().unwrap()).unwrap();
622
623 std::fs::write(
624 &terminal,
625 serde_json::to_vec(&GuestTerminalStatus::new(0)).unwrap(),
626 )
627 .unwrap();
628 assert!(!guest_rootfs_handoff_complete(temp.path()));
629
630 std::fs::write(
631 &terminal,
632 serde_json::to_vec(&GuestTerminalStatus::new(0).with_rootfs_quiesced()).unwrap(),
633 )
634 .unwrap();
635 assert!(guest_rootfs_handoff_complete(temp.path()));
636 }
637
638 #[test]
639 fn pending_terminal_status_blocks_stale_rootfs_fallback() {
640 let temp = tempfile::tempdir().unwrap();
641 let terminal = temp
642 .path()
643 .join("runtime-control")
644 .join(GUEST_TERMINAL_STATUS_FILE_NAME);
645 std::fs::create_dir_all(terminal.parent().unwrap()).unwrap();
646 std::fs::write(terminal, []).unwrap();
647 let legacy = temp.path().join("rootfs/.a3s_exit_code");
648 std::fs::create_dir_all(legacy.parent().unwrap()).unwrap();
649 std::fs::write(legacy, "0").unwrap();
650
651 assert_eq!(read_persisted_exit_code(temp.path()), None);
652 assert_eq!(resolve_workload_exit_code(temp.path(), Some(0)), None);
653 assert_eq!(resolve_workload_exit_code(temp.path(), Some(9)), Some(9));
654 }
655
656 #[test]
657 fn missing_path_is_not_mountpoint() {
658 let temp = tempfile::tempdir().unwrap();
659 let missing = temp.path().join("missing");
660
661 assert!(!is_mountpoint(&missing));
662 }
663
664 #[test]
665 fn unmount_overlay_noops_for_non_mountpoint() {
666 let temp = tempfile::tempdir().unwrap();
667 let merged = temp.path().join("merged");
668 std::fs::create_dir(&merged).unwrap();
669
670 unmount_box_overlay(&merged);
671
672 assert!(merged.exists());
673 }
674
675 #[test]
676 fn staging_is_idempotent_until_guest_replay_succeeds() {
677 let root = tempfile::tempdir().unwrap();
678 let terminal = root
679 .path()
680 .join(a3s_box_core::rootfs_metadata::ROOTFS_METADATA_PATH.trim_start_matches('/'));
681 let previous = root.path().join(
682 a3s_box_core::rootfs_metadata::PREVIOUS_ROOTFS_METADATA_PATH.trim_start_matches('/'),
683 );
684 std::fs::write(&terminal, b"clean generation").unwrap();
685
686 stage_metadata_roots(&[root.path().to_path_buf()]).unwrap();
687 stage_metadata_roots(&[root.path().to_path_buf()]).unwrap();
688
689 assert!(!terminal.exists());
690 assert_eq!(std::fs::read(previous).unwrap(), b"clean generation");
691 }
692
693 #[test]
694 fn staging_one_candidate_never_discards_an_alias_replay() {
695 let directory = tempfile::tempdir().unwrap();
696 let merged = directory.path().join("merged");
697 let upper = directory.path().join("upper");
698 std::fs::create_dir_all(&merged).unwrap();
699 std::fs::create_dir_all(&upper).unwrap();
700 let terminal_name =
701 a3s_box_core::rootfs_metadata::ROOTFS_METADATA_PATH.trim_start_matches('/');
702 let previous_name =
703 a3s_box_core::rootfs_metadata::PREVIOUS_ROOTFS_METADATA_PATH.trim_start_matches('/');
704 std::fs::write(merged.join(terminal_name), b"clean generation").unwrap();
705 std::fs::write(upper.join(previous_name), b"clean generation").unwrap();
708
709 stage_metadata_roots(&[merged.clone(), upper.clone()]).unwrap();
710
711 assert!(merged.join(previous_name).is_file());
712 assert!(upper.join(previous_name).is_file());
713 }
714
715 #[test]
716 fn staging_box_roots_clears_every_previous_exit_status() {
717 let directory = tempfile::tempdir().unwrap();
718 let box_dir = directory.path().join("box");
719 for provider_root in ["rootfs", "upper", "merged"] {
720 let root = box_dir.join(provider_root);
721 std::fs::create_dir_all(&root).unwrap();
722 std::fs::write(
723 root.join(a3s_box_core::rootfs_metadata::EXIT_CODE_PATH.trim_start_matches('/')),
724 b"17\n",
725 )
726 .unwrap();
727 }
728
729 stage_box_terminal_rootfs_metadata(&box_dir).unwrap();
730
731 assert_eq!(read_persisted_exit_code(&box_dir), None);
732 for provider_root in ["rootfs", "upper", "merged"] {
733 assert!(!box_dir
734 .join(provider_root)
735 .join(a3s_box_core::rootfs_metadata::EXIT_CODE_PATH.trim_start_matches('/'))
736 .exists());
737 }
738 }
739
740 #[test]
741 fn raw_generation_keeps_terminal_fencing_inside_guest() {
742 let directory = tempfile::tempdir().unwrap();
743 let box_dir = directory.path().join("box");
744 let artifact = box_dir.join("rootfs-ext4-v1");
745 let rootfs = box_dir.join("rootfs");
746 std::fs::create_dir_all(&artifact).unwrap();
747 std::fs::create_dir_all(&rootfs).unwrap();
748 let terminal = rootfs
749 .join(a3s_box_core::rootfs_metadata::ROOTFS_METADATA_PATH.trim_start_matches('/'));
750 std::fs::write(&terminal, b"guest-owned").unwrap();
751
752 assert!(guest_native_ext4_generation_exists(&box_dir).unwrap());
753 stage_box_terminal_rootfs_metadata(&box_dir).unwrap();
754 assert_eq!(std::fs::read(&terminal).unwrap(), b"guest-owned");
755 assert!(attach_persistent_rootfs(&box_dir).is_err());
756 }
757}