Skip to content

Commit fe4816e

Browse files
committed
Stage snapshot files via hard links for jailed sources
When the source sandbox is jailed, replace the previous copy-then-chown flow with a hard-link staging approach. Introduce a linked_snapshot_files flag and always create the fork snapshot/mem files in the fork dir, chown them to the jailer, then hard-link them into the source chroot so Firecracker can write within its chroot. Add error handling and cleanup on failures (including removing temporary snapshot/mem files and aborting the fork), and remove the old code path that copied snapshot files from the source chroot after disk cloning. Also ensure temporary hard-linked files are removed after resume/failure.
1 parent 532684a commit fe4816e

1 file changed

Lines changed: 102 additions & 78 deletions

File tree

crates/sandchest-node/src/sandbox.rs

Lines changed: 102 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -839,22 +839,101 @@ impl SandboxManager {
839839
)));
840840
}
841841

842-
// Compute snapshot paths. When source is jailed, snapshot files must go
843-
// into the source's chroot (Firecracker can only write within its chroot).
844-
let (api_snapshot_path, api_mem_path, host_snapshot_path, host_mem_path) =
845-
if let Some(ref src_chroot) = source_chroot_root {
846-
// Jailed source: write to source chroot, then copy later
847-
let api_snap = "/fork_snapshot_file".to_string();
848-
let api_mem = "/fork_mem_file".to_string();
849-
let host_snap = format!("{}/fork_snapshot_file", src_chroot);
850-
let host_mem = format!("{}/fork_mem_file", src_chroot);
851-
(api_snap, api_mem, host_snap, host_mem)
852-
} else {
853-
// Non-jailed: write directly to fork's sandbox dir
854-
let snap = format!("{}/snapshot_file", fork_sandbox_dir);
855-
let mem = format!("{}/mem_file", fork_sandbox_dir);
856-
(snap.clone(), mem.clone(), snap, mem)
857-
};
842+
let fork_snap = format!("{}/snapshot_file", fork_sandbox_dir);
843+
let fork_mem = format!("{}/mem_file", fork_sandbox_dir);
844+
845+
// Compute snapshot paths. When source is jailed, Firecracker can only
846+
// write within the source chroot, so we expose the fork snapshot files
847+
// there via hard links instead of copying them afterward.
848+
let (
849+
api_snapshot_path,
850+
api_mem_path,
851+
host_snapshot_path,
852+
host_mem_path,
853+
linked_snapshot_files,
854+
) = if let Some(ref src_chroot) = source_chroot_root {
855+
let api_snap = "/fork_snapshot_file".to_string();
856+
let api_mem = "/fork_mem_file".to_string();
857+
let host_snap = format!("{}/fork_snapshot_file", src_chroot);
858+
let host_mem = format!("{}/fork_mem_file", src_chroot);
859+
(api_snap, api_mem, host_snap, host_mem, true)
860+
} else {
861+
(
862+
fork_snap.clone(),
863+
fork_mem.clone(),
864+
fork_snap.clone(),
865+
fork_mem.clone(),
866+
false,
867+
)
868+
};
869+
870+
if linked_snapshot_files {
871+
for path in [&fork_snap, &fork_mem, &host_snapshot_path, &host_mem_path] {
872+
let _ = tokio::fs::remove_file(path).await;
873+
}
874+
875+
for path in [&fork_snap, &fork_mem] {
876+
if let Err(e) = tokio::fs::File::create(path).await {
877+
self.cleanup_fork_failure(
878+
new_sandbox_id,
879+
slot,
880+
None,
881+
&format!("snapshot staging failed: {}", e),
882+
)
883+
.await;
884+
return Err(SandboxError::ForkFailed(format!(
885+
"failed to stage fork snapshot file {}: {}",
886+
path, e
887+
)));
888+
}
889+
890+
if let Err(e) =
891+
jailer::chown_to_jailer(&self.node_config.jailer, std::path::Path::new(path))
892+
.await
893+
{
894+
self.cleanup_fork_failure(
895+
new_sandbox_id,
896+
slot,
897+
None,
898+
&format!("snapshot staging ownership failed: {}", e),
899+
)
900+
.await;
901+
return Err(SandboxError::ForkFailed(format!(
902+
"failed to set fork snapshot ownership for {}: {}",
903+
path, e
904+
)));
905+
}
906+
}
907+
908+
if let Err(e) = tokio::fs::hard_link(&fork_snap, &host_snapshot_path).await {
909+
self.cleanup_fork_failure(
910+
new_sandbox_id,
911+
slot,
912+
None,
913+
&format!("snapshot hard-link setup failed: {}", e),
914+
)
915+
.await;
916+
return Err(SandboxError::ForkFailed(format!(
917+
"failed to hard-link snapshot file into source chroot: {}",
918+
e
919+
)));
920+
}
921+
922+
if let Err(e) = tokio::fs::hard_link(&fork_mem, &host_mem_path).await {
923+
let _ = tokio::fs::remove_file(&host_snapshot_path).await;
924+
self.cleanup_fork_failure(
925+
new_sandbox_id,
926+
slot,
927+
None,
928+
&format!("mem hard-link setup failed: {}", e),
929+
)
930+
.await;
931+
return Err(SandboxError::ForkFailed(format!(
932+
"failed to hard-link memory snapshot file into source chroot: {}",
933+
e
934+
)));
935+
}
936+
}
858937
let source_rootfs = if source_chroot_root.is_some() {
859938
let src_paths = self.sandbox_paths(source_sandbox_id);
860939
format!("{}/rootfs.ext4", src_paths.dir)
@@ -887,6 +966,8 @@ impl SandboxManager {
887966
.await
888967
{
889968
let _ = fc_api.resume_vm().await; // Best-effort resume on failure
969+
let _ = tokio::fs::remove_file(&host_snapshot_path).await;
970+
let _ = tokio::fs::remove_file(&host_mem_path).await;
890971
self.cleanup_fork_failure(
891972
new_sandbox_id,
892973
slot,
@@ -900,6 +981,11 @@ impl SandboxManager {
900981
)));
901982
}
902983

984+
if linked_snapshot_files {
985+
let _ = tokio::fs::remove_file(&host_snapshot_path).await;
986+
let _ = tokio::fs::remove_file(&host_mem_path).await;
987+
}
988+
903989
// --- Step 3: Clone disk (while source is paused for consistency) ---
904990
let disk_result = if self.node_config.jailer.enabled {
905991
match disk::clone_disk_to(
@@ -950,68 +1036,6 @@ impl SandboxManager {
9501036
)));
9511037
}
9521038

953-
// For jailed source: copy snapshot files from source chroot to fork's dir
954-
if source_chroot_root.is_some() {
955-
let fork_snap = format!("{}/snapshot_file", fork_sandbox_dir);
956-
let fork_mem = format!("{}/mem_file", fork_sandbox_dir);
957-
if let Err(e) = tokio::fs::copy(&host_snapshot_path, &fork_snap).await {
958-
self.cleanup_fork_failure(
959-
new_sandbox_id,
960-
slot,
961-
None,
962-
&format!("copy snapshot to fork failed: {}", e),
963-
)
964-
.await;
965-
// Clean up temp files from source chroot
966-
let _ = tokio::fs::remove_file(&host_snapshot_path).await;
967-
let _ = tokio::fs::remove_file(&host_mem_path).await;
968-
return Err(SandboxError::ForkFailed(format!(
969-
"failed to copy snapshot to fork: {}",
970-
e
971-
)));
972-
}
973-
if let Err(e) = tokio::fs::copy(&host_mem_path, &fork_mem).await {
974-
self.cleanup_fork_failure(
975-
new_sandbox_id,
976-
slot,
977-
None,
978-
&format!("copy mem to fork failed: {}", e),
979-
)
980-
.await;
981-
let _ = tokio::fs::remove_file(&host_snapshot_path).await;
982-
let _ = tokio::fs::remove_file(&host_mem_path).await;
983-
return Err(SandboxError::ForkFailed(format!(
984-
"failed to copy mem to fork: {}",
985-
e
986-
)));
987-
}
988-
// Clean up temp files from source chroot
989-
let _ = tokio::fs::remove_file(&host_snapshot_path).await;
990-
let _ = tokio::fs::remove_file(&host_mem_path).await;
991-
if self.node_config.jailer.enabled {
992-
for path in [&fork_snap, &fork_mem] {
993-
if let Err(e) = jailer::chown_to_jailer(
994-
&self.node_config.jailer,
995-
std::path::Path::new(path),
996-
)
997-
.await
998-
{
999-
self.cleanup_fork_failure(
1000-
new_sandbox_id,
1001-
slot,
1002-
None,
1003-
&format!("snapshot artifact ownership setup failed: {}", e),
1004-
)
1005-
.await;
1006-
return Err(SandboxError::ForkFailed(format!(
1007-
"snapshot artifact ownership setup failed: {}",
1008-
e
1009-
)));
1010-
}
1011-
}
1012-
}
1013-
}
1014-
10151039
// --- Step 5: Boot fork from snapshot ---
10161040
let api_socket_path = fork_paths.api_socket.clone();
10171041

0 commit comments

Comments
 (0)