diff --git a/crates/engine/src/local_copy/executor/special/device.rs b/crates/engine/src/local_copy/executor/special/device.rs index 5f19e2fb6..885d594f8 100644 --- a/crates/engine/src/local_copy/executor/special/device.rs +++ b/crates/engine/src/local_copy/executor/special/device.rs @@ -21,7 +21,7 @@ use crate::local_copy::{ remove_source_entry_if_requested, }; #[cfg(unix)] -use ::metadata::create_device_node; +use ::metadata::create_device_node_with_fake_super; use ::metadata::{MetadataOptions, apply_file_metadata_with_options}; /// Copies a device node (block or character) from source to destination. @@ -208,10 +208,13 @@ pub(crate) fn copy_device( } } - // create the actual device node + // create the actual device node, or a 0600 placeholder when --fake-super + // is active (mirrors upstream syscall.c:do_mknod()'s am_root < 0 branch). #[cfg(unix)] { - create_device_node(destination, metadata).map_err(map_metadata_error)?; + let fake_super = metadata_options.fake_super_enabled(); + create_device_node_with_fake_super(destination, metadata, fake_super) + .map_err(map_metadata_error)?; } #[cfg(not(unix))] { @@ -239,6 +242,17 @@ pub(crate) fn copy_device( #[cfg(all(unix, feature = "acl"))] sync_acls_if_requested(preserve_acls, mode, source, destination, true)?; + // Under fake-super, capture the would-be device's mode/uid/gid/rdev in + // the rsync.%stat xattr so the destination can be restored later. This + // is the read-side complement of `apply_ownership_via_fake_super` for + // the local-copy path, where we have a full `fs::Metadata` rather than + // a wire-protocol `FileEntry`. + // upstream: xattrs.c:set_stat_xattr() under am_root < 0 + #[cfg(all(unix, feature = "xattr"))] + if metadata_options.fake_super_enabled() { + store_fake_super_for_local_metadata(destination, metadata)?; + } + context.record_hard_link(metadata, destination); context.summary_mut().record_device(); @@ -259,3 +273,25 @@ pub(crate) fn copy_device( remove_source_entry_if_requested(context, source, record_path.as_deref(), file_type)?; Ok(()) } + +/// Stores the would-be device/special metadata in the `rsync.%stat` xattr. +/// +/// Encodes mode (with `S_IFMT` bits), uid, gid, and rdev so a later +/// fake-super read can faithfully reconstruct the original node. +// upstream: xattrs.c:set_stat_xattr() +#[cfg(all(unix, feature = "xattr"))] +fn store_fake_super_for_local_metadata( + destination: &Path, + metadata: &fs::Metadata, +) -> Result<(), LocalCopyError> { + use ::metadata::{FakeSuperStat, store_fake_super}; + + let stat = FakeSuperStat::from_metadata(metadata); + store_fake_super(destination, &stat).map_err(|error| { + LocalCopyError::io( + "store fake-super metadata", + destination.to_path_buf(), + error, + ) + }) +} diff --git a/crates/engine/src/local_copy/executor/special/fifo.rs b/crates/engine/src/local_copy/executor/special/fifo.rs index 202bc39ea..deadaa2ce 100644 --- a/crates/engine/src/local_copy/executor/special/fifo.rs +++ b/crates/engine/src/local_copy/executor/special/fifo.rs @@ -21,7 +21,7 @@ use crate::local_copy::{ remove_source_entry_if_requested, }; #[cfg(unix)] -use ::metadata::create_fifo; +use ::metadata::create_fifo_with_fake_super; use ::metadata::{MetadataOptions, apply_file_metadata_with_options}; /// Copies a FIFO (named pipe) from source to destination. @@ -212,10 +212,13 @@ pub(crate) fn copy_fifo( } } - // actually create a FIFO + // actually create a FIFO, or a 0600 placeholder when --fake-super is + // active (mirrors upstream syscall.c:do_mknod()'s am_root < 0 branch). #[cfg(unix)] { - create_fifo(destination, metadata).map_err(map_metadata_error)?; + let fake_super = metadata_options.fake_super_enabled(); + create_fifo_with_fake_super(destination, metadata, fake_super) + .map_err(map_metadata_error)?; } #[cfg(not(unix))] { @@ -247,6 +250,14 @@ pub(crate) fn copy_fifo( #[cfg(all(unix, feature = "acl"))] sync_acls_if_requested(preserve_acls, mode, source, destination, true)?; + // Under fake-super, capture the would-be FIFO/socket's mode/uid/gid in + // the rsync.%stat xattr so the destination can be restored later. + // upstream: xattrs.c:set_stat_xattr() under am_root < 0 + #[cfg(all(unix, feature = "xattr"))] + if metadata_options.fake_super_enabled() { + store_fake_super_for_local_metadata(destination, metadata)?; + } + context.record_hard_link(metadata, destination); context.summary_mut().record_fifo(); @@ -267,3 +278,25 @@ pub(crate) fn copy_fifo( remove_source_entry_if_requested(context, source, record_path.as_deref(), file_type)?; Ok(()) } + +/// Stores the would-be FIFO/socket metadata in the `rsync.%stat` xattr. +/// +/// Encodes mode (with `S_IFMT` bits), uid, and gid so a later fake-super +/// read can faithfully reconstruct the original node. +// upstream: xattrs.c:set_stat_xattr() +#[cfg(all(unix, feature = "xattr"))] +fn store_fake_super_for_local_metadata( + destination: &Path, + metadata: &fs::Metadata, +) -> Result<(), LocalCopyError> { + use ::metadata::{FakeSuperStat, store_fake_super}; + + let stat = FakeSuperStat::from_metadata(metadata); + store_fake_super(destination, &stat).map_err(|error| { + LocalCopyError::io( + "store fake-super metadata", + destination.to_path_buf(), + error, + ) + }) +} diff --git a/crates/metadata/src/apply/ownership.rs b/crates/metadata/src/apply/ownership.rs index 14fa7cae8..ea4cb14c0 100644 --- a/crates/metadata/src/apply/ownership.rs +++ b/crates/metadata/src/apply/ownership.rs @@ -292,9 +292,11 @@ fn apply_ownership_via_fake_super( uid: Option, gid: Option, ) -> Result<(), MetadataError> { - use crate::fake_super::{FakeSuperStat, store_fake_super}; + use crate::fake_super::{FakeSuperStat, load_fake_super, store_fake_super}; - let mode = entry.permissions(); + // upstream: xattrs.c:set_stat_xattr() encodes the full mode (S_IFMT + perms) + // so a later read can rebuild the file type, not just the permission bits. + let mode = entry.mode(); let uid = uid.unwrap_or(0); let gid = gid.unwrap_or(0); @@ -314,6 +316,15 @@ fn apply_ownership_via_fake_super( rdev, }; + // upstream: xattrs.c:read_stat_xattr() consults the existing xattr so an + // unchanged stat skips the rewrite. Mirrors `set_file_attrs()`'s "no-op + // when current state already matches" fast path. + if let Ok(Some(existing)) = load_fake_super(destination) + && existing == stat + { + return Ok(()); + } + store_fake_super(destination, &stat) .map_err(|error| MetadataError::new("store fake-super metadata", destination, error)) } diff --git a/crates/metadata/src/apply/tests.rs b/crates/metadata/src/apply/tests.rs index 50910a0ea..7fad3443d 100644 --- a/crates/metadata/src/apply/tests.rs +++ b/crates/metadata/src/apply/tests.rs @@ -931,3 +931,127 @@ fn attrs_flags_skip_mtime_does_not_affect_permissions() { let dest_meta = fs::metadata(&dest).expect("dest metadata"); assert_eq!(dest_meta.permissions().mode() & 0o777, 0o755); } + +#[cfg(all(unix, feature = "xattr"))] +#[test] +fn fake_super_writes_rsync_stat_xattr_for_regular_file() { + use crate::fake_super::{FAKE_SUPER_XATTR, FakeSuperStat}; + use protocol::flist::FileEntry; + + let temp = tempdir().expect("tempdir"); + let dest = temp.path().join("fakesuper-regular.txt"); + fs::write(&dest, b"data").expect("write dest"); + + let mut entry = FileEntry::new_file("fakesuper-regular.txt".into(), 4, 0o100_644); + entry.set_uid(4242); + entry.set_gid(4343); + + let opts = MetadataOptions::new() + .fake_super(true) + .preserve_owner(true) + .preserve_group(true); + + apply_metadata_from_file_entry(&dest, &entry, &opts).expect("apply with fake-super"); + + let raw = match xattr::get(&dest, FAKE_SUPER_XATTR) { + Ok(Some(value)) => value, + Ok(None) => { + // Filesystem without xattr support (e.g. tmpfs without user_xattr). + return; + } + Err(_) => return, + }; + let decoded = + FakeSuperStat::decode(std::str::from_utf8(&raw).expect("xattr utf-8")).expect("decode"); + + assert_eq!(decoded.mode, 0o100_644); + assert_eq!(decoded.uid, 4242); + assert_eq!(decoded.gid, 4343); + assert_eq!(decoded.rdev, None); +} + +#[cfg(all(unix, feature = "xattr"))] +#[test] +fn fake_super_does_not_chown_destination() { + use crate::fake_super::FAKE_SUPER_XATTR; + use protocol::flist::FileEntry; + use std::os::unix::fs::MetadataExt; + + let temp = tempdir().expect("tempdir"); + let dest = temp.path().join("fakesuper-nochown.txt"); + fs::write(&dest, b"data").expect("write dest"); + + let original_uid = fs::metadata(&dest).expect("metadata").uid(); + let original_gid = fs::metadata(&dest).expect("metadata").gid(); + + let mut entry = FileEntry::new_file("fakesuper-nochown.txt".into(), 4, 0o100_644); + // Use a uid/gid the unprivileged test process cannot assume directly. + entry.set_uid(original_uid + 1000); + entry.set_gid(original_gid + 1000); + + let opts = MetadataOptions::new() + .fake_super(true) + .preserve_owner(true) + .preserve_group(true); + + apply_metadata_from_file_entry(&dest, &entry, &opts) + .expect("fake-super apply must not fail without root"); + + let after = fs::metadata(&dest).expect("metadata"); + assert_eq!( + after.uid(), + original_uid, + "fake-super must not invoke chown on the inode" + ); + assert_eq!( + after.gid(), + original_gid, + "fake-super must not invoke chown on the inode" + ); + + // Sanity: the xattr was written when the filesystem supports it. + if let Ok(Some(_)) = xattr::get(&dest, FAKE_SUPER_XATTR) { + // Nothing else to assert; existence proves the wire-up. + } +} + +#[cfg(all(unix, feature = "xattr"))] +#[test] +fn fake_super_skips_rewrite_when_xattr_already_matches() { + use crate::fake_super::{FAKE_SUPER_XATTR, FakeSuperStat, store_fake_super}; + use protocol::flist::FileEntry; + + let temp = tempdir().expect("tempdir"); + let dest = temp.path().join("fakesuper-skip.txt"); + fs::write(&dest, b"data").expect("write dest"); + + let stat = FakeSuperStat { + mode: 0o100_640, + uid: 7777, + gid: 8888, + rdev: None, + }; + if store_fake_super(&dest, &stat).is_err() { + // Filesystem without xattr support; skip silently. + return; + } + let raw_before = xattr::get(&dest, FAKE_SUPER_XATTR) + .expect("xattr get") + .expect("xattr present"); + + let mut entry = FileEntry::new_file("fakesuper-skip.txt".into(), 4, 0o100_640); + entry.set_uid(7777); + entry.set_gid(8888); + + let opts = MetadataOptions::new() + .fake_super(true) + .preserve_owner(true) + .preserve_group(true); + + apply_metadata_from_file_entry(&dest, &entry, &opts).expect("apply with fake-super"); + + let raw_after = xattr::get(&dest, FAKE_SUPER_XATTR) + .expect("xattr get") + .expect("xattr present"); + assert_eq!(raw_before, raw_after, "xattr must remain byte-identical"); +} diff --git a/crates/metadata/src/lib.rs b/crates/metadata/src/lib.rs index 3b405c028..b1c921718 100644 --- a/crates/metadata/src/lib.rs +++ b/crates/metadata/src/lib.rs @@ -190,7 +190,10 @@ pub use mapping_win::{GroupMapping, MappingKind, MappingParseError, NameMapping, pub use options::{AttrsFlags, MetadataOptions}; -pub use special::{create_device_node, create_fifo}; +pub use special::{ + create_device_node, create_device_node_with_fake_super, create_fifo, + create_fifo_with_fake_super, +}; #[cfg(all(unix, feature = "xattr"))] pub use xattr::{apply_xattrs_from_list, read_xattrs_for_wire, sync_xattrs}; diff --git a/crates/metadata/src/special.rs b/crates/metadata/src/special.rs index e1bfc373e..2a988f173 100644 --- a/crates/metadata/src/special.rs +++ b/crates/metadata/src/special.rs @@ -1,6 +1,5 @@ use crate::error::MetadataError; use std::fs; -#[cfg(unix)] use std::io; use std::path::Path; @@ -23,6 +22,98 @@ pub fn create_device_node( create_device_node_inner(destination, metadata) } +/// Creates a FIFO or socket at `destination`, honouring `--fake-super`. +/// +/// When `fake_super` is `true`, mirrors upstream `syscall.c:do_mknod()`'s +/// `am_root < 0` branch: instead of issuing `mknod(2)`, a regular `0600` +/// placeholder file is created so an unprivileged process can preserve the +/// node's metadata in `user.rsync.%stat` (written separately by +/// `store_fake_super`). When `fake_super` is `false`, behaviour matches +/// [`create_fifo`]. +/// +/// # Errors +/// +/// Returns [`MetadataError`] if the placeholder cannot be created or if the +/// underlying mknod call fails. +// upstream: syscall.c:do_mknod() - placeholder substitution when am_root < 0 +pub fn create_fifo_with_fake_super( + destination: &Path, + metadata: &fs::Metadata, + fake_super: bool, +) -> Result<(), MetadataError> { + if fake_super { + create_fake_super_placeholder(destination, "create fifo") + } else { + create_fifo_inner(destination, metadata) + } +} + +/// Creates a device node at `destination`, honouring `--fake-super`. +/// +/// When `fake_super` is `true`, mirrors upstream `syscall.c:do_mknod()`'s +/// `am_root < 0` branch: instead of issuing `mknod(2)` (which requires +/// `CAP_MKNOD`), a regular `0600` placeholder file is created so an +/// unprivileged process can preserve the device's privileged metadata +/// (mode, uid, gid, rdev) in `user.rsync.%stat` (written separately by +/// `store_fake_super`). When `fake_super` is `false`, behaviour matches +/// [`create_device_node`]. +/// +/// # Errors +/// +/// Returns [`MetadataError`] if the placeholder cannot be created or if the +/// underlying mknod call fails. +// upstream: syscall.c:do_mknod() - placeholder substitution when am_root < 0 +pub fn create_device_node_with_fake_super( + destination: &Path, + metadata: &fs::Metadata, + fake_super: bool, +) -> Result<(), MetadataError> { + if fake_super { + create_fake_super_placeholder(destination, "create device") + } else { + create_device_node_inner(destination, metadata) + } +} + +/// Creates an empty 0600 regular file used as a fake-super placeholder. +/// +/// Upstream `do_mknod()` performs the equivalent substitution by routing the +/// call through `do_open` with `O_CREAT|O_WRONLY|O_EXCL` and mode `0600`. Any +/// pre-existing entry at `destination` is removed first to mirror the +/// `unlink + create` semantics used by upstream when overwriting an existing +/// special-file destination. +// upstream: syscall.c:90-174 - do_mknod() routes to do_open when am_root < 0 +fn create_fake_super_placeholder( + destination: &Path, + context: &'static str, +) -> Result<(), MetadataError> { + if let Err(error) = fs::remove_file(destination) + && error.kind() != io::ErrorKind::NotFound + { + return Err(MetadataError::new(context, destination, error)); + } + + let mut open_options = fs::OpenOptions::new(); + open_options.write(true).create_new(true); + apply_placeholder_mode(&mut open_options); + open_options + .open(destination) + .map(drop) + .map_err(|error| MetadataError::new(context, destination, error)) +} + +#[cfg(unix)] +fn apply_placeholder_mode(open_options: &mut fs::OpenOptions) { + use std::os::unix::fs::OpenOptionsExt; + open_options.mode(0o600); +} + +#[cfg(not(unix))] +fn apply_placeholder_mode(_open_options: &mut fs::OpenOptions) { + // On non-Unix targets there is no POSIX mode to apply; the placeholder is + // a regular file with platform-default permissions. +} + #[cfg(all( unix, not(any( @@ -372,4 +463,89 @@ mod tests { let result = create_device_node(&device_path, &metadata); assert!(result.is_ok()); } + + /// Under `--fake-super`, `create_fifo_with_fake_super` must never call + /// `mknod(2)`. Instead it creates a regular 0600 placeholder so the + /// destination's would-be metadata can be captured separately in the + /// `user.rsync.%stat` xattr. + /// // upstream: syscall.c:do_mknod() under am_root < 0 + #[cfg(unix)] + #[test] + fn fake_super_replaces_mkfifo_with_regular_placeholder() { + use std::os::unix::fs::{FileTypeExt, PermissionsExt}; + + let temp = tempdir().expect("create tempdir"); + let source_path = temp.path().join("source"); + fs::File::create(&source_path).expect("create source"); + let metadata = fs::metadata(&source_path).expect("metadata"); + + let dest = temp.path().join("placeholder.fifo"); + create_fifo_with_fake_super(&dest, &metadata, true).expect("placeholder created"); + + let dest_meta = fs::symlink_metadata(&dest).expect("placeholder metadata"); + assert!( + dest_meta.file_type().is_file(), + "fake-super must create a regular file, not a fifo" + ); + assert!(!dest_meta.file_type().is_fifo()); + assert_eq!(dest_meta.permissions().mode() & 0o777, 0o600); + } + + /// Same invariant for `create_device_node_with_fake_super`: never + /// invoke `mknod(2)` for a device, fall back to a 0600 placeholder. + /// // upstream: syscall.c:do_mknod() under am_root < 0 + #[cfg(unix)] + #[test] + fn fake_super_replaces_mknod_with_regular_placeholder() { + use std::os::unix::fs::{FileTypeExt, PermissionsExt}; + + let temp = tempdir().expect("create tempdir"); + let source_path = temp.path().join("source"); + fs::File::create(&source_path).expect("create source"); + let metadata = fs::metadata(&source_path).expect("metadata"); + + let dest = temp.path().join("placeholder.dev"); + create_device_node_with_fake_super(&dest, &metadata, true) + .expect("placeholder created without CAP_MKNOD"); + + let dest_meta = fs::symlink_metadata(&dest).expect("placeholder metadata"); + assert!( + dest_meta.file_type().is_file(), + "fake-super must create a regular file, not a device node" + ); + assert!(!dest_meta.file_type().is_block_device()); + assert!(!dest_meta.file_type().is_char_device()); + assert_eq!(dest_meta.permissions().mode() & 0o777, 0o600); + } + + /// When fake-super is disabled, `create_fifo_with_fake_super` falls + /// through to the real mknod path (subject to the same platform + /// availability as `create_fifo`). + #[cfg(all( + unix, + not(any( + target_os = "ios", + target_os = "macos", + target_os = "tvos", + target_os = "watchos" + )) + ))] + #[test] + fn create_fifo_with_fake_super_disabled_creates_real_fifo() { + use std::os::unix::fs::{FileTypeExt, PermissionsExt}; + + let temp = tempdir().expect("create tempdir"); + let source_path = temp.path().join("source"); + fs::File::create(&source_path).expect("create source"); + let mut permissions = fs::metadata(&source_path).expect("metadata").permissions(); + permissions.set_mode(0o640); + fs::set_permissions(&source_path, permissions).expect("set permissions"); + let metadata = fs::metadata(&source_path).expect("metadata after permissions"); + + let dest = temp.path().join("real.fifo"); + create_fifo_with_fake_super(&dest, &metadata, false).expect("real fifo created"); + + let dest_meta = fs::symlink_metadata(&dest).expect("fifo metadata"); + assert!(dest_meta.file_type().is_fifo()); + } }