Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions crates/engine/src/local_copy/executor/file/guard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,9 @@ impl DestinationWriteGuard {
/// kernel lacks the opcode.
///
/// upstream: `util1.c:robust_rename()` - retry up to 4 times on `ETXTBSY`.
// TODO: SEC-1.j receiver wiring - DirSandbox not in scope, defer pending
// `DestinationWriteGuard` carrying a sandbox handle threaded through the
// local-copy executor and its callers; cross-crate API change.
fn commit_named_temp_file(&self, temp_path: PathBuf) -> Result<(), LocalCopyError> {
let mut tries = 4u32;
loop {
Expand Down
7 changes: 7 additions & 0 deletions crates/transfer/src/disk_commit/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,10 @@ fn commit_file(
///
/// This mirrors the pattern used in `engine::local_copy::executor::file::guard`
/// for local-copy temp-file commits.
// TODO: SEC-1.j receiver wiring - DirSandbox not in scope, defer pending
// `DiskCommitConfig` carrying an `Arc<DirSandbox>` across the cross-thread
// message boundary; `BackupConfig::make_backup` needs the same plumbing
// for the backup rename hardening.
fn rename_with_io_uring_fallback(old_path: &Path, new_path: &Path) -> io::Result<()> {
if let Some(result) = fast_io::try_rename_via_io_uring(old_path, new_path) {
return result;
Expand Down Expand Up @@ -476,6 +480,9 @@ fn make_backup(file_path: &Path, config: &BackupConfig) -> io::Result<()> {
}
}

// TODO: SEC-1.j receiver wiring - DirSandbox not in scope, defer pending
// `BackupConfig` carrying an `Arc<DirSandbox>` across the cross-thread
// message boundary into the disk-commit thread.
fs::rename(file_path, &backup_path)?;
// upstream: backup.c:216-217 - DEBUG_GTE(BACKUP, 1) on the RENAME success
// branch of link_or_rename. disk_commit always uses rename here.
Expand Down
2 changes: 2 additions & 0 deletions crates/transfer/src/receiver/transfer/pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,8 @@ impl ReceiverContext {
config: &request_config,
#[cfg(unix)]
sandbox: setup.sandbox.as_deref(),
#[cfg(unix)]
dest_dir: Some(setup.dest_dir.as_path()),
};

let xattr_list = self.resolve_xattr_list(file_entry);
Expand Down
13 changes: 11 additions & 2 deletions crates/transfer/src/transfer_ops/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,10 +181,19 @@ pub struct ResponseContext<'a> {
/// SEC-1.f-j cutover sites can resolve relative names against a
/// sandboxed dirfd via `*at` syscalls instead of re-walking paths
/// through the kernel. `None` when the receiver could not open the
/// destination root (e.g. it does not exist yet). This PR (SEC-1.e)
/// only wires the carrier; no syscalls are migrated yet.
/// destination root (e.g. it does not exist yet).
#[cfg(unix)]
pub sandbox: Option<&'a fast_io::DirSandbox>,
/// Destination tree root anchor for the SEC-1.j leaf-rename detector.
///
/// `process_file_response` uses this together with `sandbox` to route
/// the temp -> final rename through `renameat(dirfd, leaf, dirfd,
/// leaf)` when both the temp and final names are single-component
/// leaves beneath this root, so a TOCTOU symlink swap on either leaf
/// cannot redirect the commit. Multi-component / cross-tree cases
/// keep the path-based fallback. `None` when no anchor is available.
#[cfg(unix)]
pub dest_dir: Option<&'a std::path::Path>,
}

/// Reads and validates the echoed NDX and sum_head from the sender response.
Expand Down
45 changes: 44 additions & 1 deletion crates/transfer/src/transfer_ops/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -281,10 +281,53 @@ pub fn process_file_response<R: Read>(
// On Linux 5.11+ with io_uring, submits IORING_OP_RENAMEAT instead of
// synchronous rename(2). Falls back to std::fs::rename on all other
// platforms or when the kernel lacks the opcode.
//
// SEC-1.j: when the sandbox is plumbed and both temp leaf + final
// leaf are single components beneath `dest_dir`, route through
// `renameat(dirfd, leaf, dirfd, leaf)` so a TOCTOU swap on either
// leaf cannot redirect the commit. The io_uring fast path is
// preserved by trying it first; the sandbox routing is the SEC-1.j
// hardening for the synchronous fallback. Multi-component /
// cross-tree cases keep the path-based `std::fs::rename`.
if let Some(result) = fast_io::try_rename_via_io_uring(cleanup_guard.path(), &file_path) {
result?;
} else {
fs::rename(cleanup_guard.path(), &file_path)?;
#[cfg(unix)]
{
let temp_path = cleanup_guard.path();
let (sandbox_dest_dir, temp_rel, final_rel) = match ctx.dest_dir {
Some(dest_dir) => {
let temp_rel = temp_path
.strip_prefix(dest_dir)
.map(std::path::Path::to_path_buf)
.unwrap_or_else(|_| temp_path.to_path_buf());
let final_rel = file_path
.strip_prefix(dest_dir)
.map(std::path::Path::to_path_buf)
.unwrap_or_else(|_| file_path.clone());
(dest_dir, temp_rel, final_rel)
}
None => (
std::path::Path::new(""),
temp_path.to_path_buf(),
file_path.clone(),
),
};
fast_io::renameat_via_sandbox_or_fallback(
ctx.sandbox,
sandbox_dest_dir,
&temp_rel,
temp_path,
sandbox_dest_dir,
&final_rel,
&file_path,
true,
)?;
}
#[cfg(not(unix))]
{
fs::rename(cleanup_guard.path(), &file_path)?;
}
}
} else if ctx.config.inplace {
// Inplace: truncate to final size.
Expand Down
Loading