diff --git a/fuse-pipe/src/protocol/request.rs b/fuse-pipe/src/protocol/request.rs index 85e2a7d1..43f88af1 100644 --- a/fuse-pipe/src/protocol/request.rs +++ b/fuse-pipe/src/protocol/request.rs @@ -463,6 +463,38 @@ impl VolumeRequest { cloned } + /// Extract the output file handle (fh_out) for operations that have one. + pub fn fh_out(&self) -> Option { + match self { + Self::CopyFileRange { fh_out, .. } | Self::RemapFileRange { fh_out, .. } => { + Some(*fh_out) + } + _ => None, + } + } + + /// Extract the output inode (ino_out) for operations that have one. + pub fn ino_out(&self) -> Option { + match self { + Self::CopyFileRange { ino_out, .. } | Self::RemapFileRange { ino_out, .. } => { + Some(*ino_out) + } + _ => None, + } + } + + /// Clone this request with a different output file handle. + pub fn with_fh_out(&self, new_fh_out: u64) -> Self { + let mut cloned = self.clone(); + match &mut cloned { + Self::CopyFileRange { fh_out, .. } | Self::RemapFileRange { fh_out, .. } => { + *fh_out = new_fh_out + } + _ => {} + } + cloned + } + /// Check if this is a directory handle operation. pub fn is_dir_handle_op(&self) -> bool { matches!( diff --git a/fuse-pipe/src/server/remap.rs b/fuse-pipe/src/server/remap.rs index a771110a..d1e98c79 100644 --- a/fuse-pipe/src/server/remap.rs +++ b/fuse-pipe/src/server/remap.rs @@ -661,10 +661,28 @@ impl RemapFs { _ => return None, }; - // Atomic insert — handles concurrent reopen races + // Atomic insert — handles concurrent reopen races. + // If another thread already inserted a mapping for this old_fh, + // release the fd we just opened to avoid leaking it. use dashmap::mapref::entry::Entry; match self.handle_remap.entry(old_fh) { - Entry::Occupied(e) => Some(*e.get()), + Entry::Occupied(e) => { + let winner_fh = *e.get(); + // Release the fd we opened — another thread won the race + let release_req = if is_dir { + VolumeRequest::Releasedir { + ino: inner_ino, + fh: new_fh, + } + } else { + VolumeRequest::Release { + ino: inner_ino, + fh: new_fh, + } + }; + self.inner.handle_request(&release_req); + Some(winner_fh) + } Entry::Vacant(e) => { debug!( old_fh, @@ -696,6 +714,12 @@ impl FilesystemHandler for RemapFs { remapped = remapped.with_fh(*new_fh); } } + // Also remap fh_out for CopyFileRange/RemapFileRange + if let Some(old_fh_out) = remapped.fh_out() { + if let Some(new_fh_out) = self.handle_remap.get(&old_fh_out) { + remapped = remapped.with_fh_out(*new_fh_out); + } + } // Delegate to inner handler let response = self @@ -704,20 +728,39 @@ impl FilesystemHandler for RemapFs { // If EBADF and this request uses a file handle, try lazy re-open if response.is_ebadf() { + let mut retry = remapped.clone(); + let mut reopened = false; + + // Try reopening fh (fh_in for CopyFileRange/RemapFileRange) if let Some(old_fh) = request.fh() { if let Some(stable_ino) = request.ino() { if let Some(new_fh) = self.try_reopen_handle(stable_ino, old_fh, request.is_dir_handle_op()) { - // Retry with the reopened handle - let retry = remapped.with_fh(new_fh); - let retry_resp = self - .inner - .handle_request_with_groups(&retry, supplementary_groups); - return self.remap_response(request, retry_resp); + retry = retry.with_fh(new_fh); + reopened = true; + } + } + } + + // Try reopening fh_out for CopyFileRange/RemapFileRange + if let Some(old_fh_out) = request.fh_out() { + if let Some(stable_ino_out) = request.ino_out() { + if let Some(new_fh_out) = + self.try_reopen_handle(stable_ino_out, old_fh_out, false) + { + retry = retry.with_fh_out(new_fh_out); + reopened = true; } } } + + if reopened { + let retry_resp = self + .inner + .handle_request_with_groups(&retry, supplementary_groups); + return self.remap_response(request, retry_resp); + } } // Handle Release/Releasedir: clean up handle_remap entry diff --git a/src/commands/podman/snapshot.rs b/src/commands/podman/snapshot.rs index cad8d5db..fa6f4bf5 100644 --- a/src/commands/podman/snapshot.rs +++ b/src/commands/podman/snapshot.rs @@ -137,7 +137,7 @@ pub async fn create_podman_snapshot(snap: &CreateSnapshotParams<'_>) -> Result<( } else { tracing::info!( port, - entries = json.len(), + bytes = json.len(), "serialized inode table to snapshot" ); } diff --git a/tests/test_portable_volumes.rs b/tests/test_portable_volumes.rs index 9a489c7b..a8318ffe 100644 --- a/tests/test_portable_volumes.rs +++ b/tests/test_portable_volumes.rs @@ -592,8 +592,8 @@ async fn test_remap_fs_snapshot_file_replace() -> Result<()> { // Test 18: Open file handles survive snapshot/clone restore // ============================================================================= -/// Verify that --non-blocking-output delivers lines best-effort through -/// the portable volumes snapshot/clone pipeline. +/// Verify that file handles survive snapshot/clone restore through +/// the portable volumes pipeline. /// /// Tests: /// 1. Clone can read files that existed at snapshot time