Skip to content
Open
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions fuzz/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/uu/mv/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ windows-sys = { workspace = true, features = [
] }

[target.'cfg(unix)'.dependencies]
rustix = { workspace = true, features = ["fs"] }
rustix = { workspace = true, features = ["fs", "process"] }

[[bin]]
name = "mv"
Expand Down
139 changes: 127 additions & 12 deletions src/uu/mv/src/mv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ use uucore::fs::{
target_os = "netbsd"
))]
use uucore::fsxattr;
#[cfg(all(
unix,
not(any(target_os = "aix", target_os = "hurd", target_os = "redox"))
))]
use uucore::safe_traversal::{DirFd, SymlinkBehavior};
#[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))]
use uucore::selinux::set_selinux_security_context;
use uucore::translate;
Expand Down Expand Up @@ -924,13 +929,17 @@ fn is_directory_not_empty_error(err: &io::Error) -> bool {
err.kind() == io::ErrorKind::DirectoryNotEmpty
}

/// Fifos, sockets and device nodes are recreated rather than copied.
#[cfg(unix)]
fn is_fifo(filetype: fs::FileType) -> bool {
fn is_special_file(filetype: fs::FileType) -> bool {
filetype.is_fifo()
|| filetype.is_socket()
|| filetype.is_block_device()
|| filetype.is_char_device()
}

#[cfg(not(unix))]
fn is_fifo(_filetype: fs::FileType) -> bool {
fn is_special_file(_filetype: fs::FileType) -> bool {
false
}

Expand Down Expand Up @@ -983,8 +992,8 @@ fn rename_with_fallback(
{
rename_dir_fallback(from, to, display_manager, verbose)
}
} else if is_fifo(file_type) {
rename_fifo_fallback(from, to)
} else if is_special_file(file_type) {
rename_special_fallback(from, to, &metadata)
} else {
#[cfg(unix)]
{
Expand All @@ -1002,23 +1011,129 @@ fn rename_with_fallback(
})
}

/// Replace the destination with a new pipe with the same name as the source.
/// Replace the destination with a new special file like the source.
#[cfg(unix)]
fn rename_fifo_fallback(from: &Path, to: &Path) -> io::Result<()> {
if to.try_exists()? {
fn rename_special_fallback(from: &Path, to: &Path, metadata: &fs::Metadata) -> io::Result<()> {
copy_special_file(to, metadata)?;
fs::remove_file(from)
}

/// Create the fifo, socket or device node that `metadata` describes at `to`,
/// with its ownership and permissions.
///
/// An entry at `to` is replaced atomically, so it is kept if the node cannot
/// be created.
#[cfg(all(
unix,
not(any(target_os = "aix", target_os = "hurd", target_os = "redox"))
))]
fn copy_special_file(to: &Path, metadata: &fs::Metadata) -> io::Result<()> {
let parent = to
.parent()
.filter(|p| !p.as_os_str().is_empty())
.unwrap_or_else(|| Path::new("."));
let name = to.file_name().ok_or(io::ErrorKind::InvalidInput)?;
// Follows symlinks in `parent`, as creating the node by path would.
let dir = DirFd::open_anchor(parent)?;
create_special_file_at(&dir, name, metadata)
}

/// Create the special file that `metadata` describes as `name` in `dir`, with
/// its ownership and permissions, replacing an entry already there.
///
/// The node gets its ownership and mode inside a new private directory in
/// `dir` and is then renamed into place. In `dir` itself, whoever else can
/// write there could link another file over the name between those calls.
#[cfg(all(
unix,
not(any(target_os = "aix", target_os = "hurd", target_os = "redox"))
))]
fn create_special_file_at(
dir: &DirFd,
name: &std::ffi::OsStr,
metadata: &fs::Metadata,
) -> io::Result<()> {
use rustix::fs::renameat;
use rustix::process::geteuid;
use std::os::unix::fs::MetadataExt;

let (staging, staging_name) = uucore::fs::create_temp_at(dir, |dir, tmp| {
dir.mkdir_at(tmp, 0o700)?;
// Whatever the umask, the owner must be able to create entries inside.
dir.chmod_at(tmp, 0o700, SymlinkBehavior::NoFollow)
.and_then(|()| dir.open_subdir(tmp, SymlinkBehavior::NoFollow))
.and_then(|staging| {
// Another directory may have been moved to `tmp` since.
let stat = staging.metadata()?;
if stat.uid() == geteuid().as_raw() && stat.mode() & 0o777 == 0o700 {
Ok(staging)
} else {
Err(io::ErrorKind::AlreadyExists.into())
}
})
.inspect_err(|_| {
// Removes only an empty directory: the one made above, or at
// worst an empty one moved to `tmp` since.
let _ = dir.unlink_at(tmp, true);
})
})?;

let created = staging
.mknod_at(name, metadata.mode(), metadata.rdev())
.and_then(|()| {
let (uid, gid) = (metadata.uid(), metadata.gid());
let node = staging.metadata_at(name, SymlinkBehavior::NoFollow)?;
// Ownership is best effort for unprivileged callers. As for
// regular files, if it did not take, the node belongs to whoever
// ran mv, so setuid and setgid are dropped. The rest of the mode,
// which the umask reduced, is restored.
let owned = (node.uid(), node.gid()) == (uid, gid)
|| staging
.chown_at(name, Some(uid), Some(gid), SymlinkBehavior::NoFollow)
.is_ok();
let mode = if owned { 0o7777 } else { 0o1777 };
staging.chmod_at(name, metadata.mode() & mode, SymlinkBehavior::NoFollow)
})
.and_then(|()| Ok(renameat(&staging, name, dir, name)?))
.inspect_err(|_| {
let _ = staging.unlink_at(name, false);
});
let _ = dir.unlink_at(&staging_name, true);
created
}

/// Without `safe_traversal`, the node is created by path after removing the
/// destination, as `uucore::fs::replace_link` does on Redox.
#[cfg(any(target_os = "aix", target_os = "hurd", target_os = "redox"))]
fn copy_special_file(to: &Path, metadata: &fs::Metadata) -> io::Result<()> {
use nix::sys::stat::{Mode, SFlag, mknod};
use std::os::unix::fs::MetadataExt;

if to.symlink_metadata().is_ok() {
fs::remove_file(to)?;
}
// rustix::fs::mkfifoat is linux only
nix::unistd::mkfifo(to, nix::sys::stat::Mode::from_bits_truncate(0o666))?;
fs::remove_file(from)
let mode = metadata.mode() as nix::libc::mode_t;
mknod(
to,
SFlag::from_bits_truncate(mode & nix::libc::S_IFMT),
Mode::from_bits_truncate(mode),
metadata.rdev() as nix::libc::dev_t,
)?;
let (uid, gid) = (metadata.uid(), metadata.gid());
let node = to.symlink_metadata()?;
// Setuid and setgid are dropped if the ownership did not take.
let owned = (node.uid(), node.gid()) == (uid, gid)
|| unix::fs::lchown(to, Some(uid), Some(gid)).is_ok();
let mode = if owned { 0o7777 } else { 0o1777 };
fs::set_permissions(to, fs::Permissions::from_mode(metadata.mode() & mode))
}

#[cfg(not(unix))]
#[expect(
clippy::unnecessary_wraps,
reason = "fn sig must match on all platforms"
)]
fn rename_fifo_fallback(_from: &Path, _to: &Path) -> io::Result<()> {
fn rename_special_fallback(_from: &Path, _to: &Path, _metadata: &fs::Metadata) -> io::Result<()> {
Ok(())
}

Expand Down Expand Up @@ -1369,7 +1484,7 @@ fn copy_file_with_hardlinks_helper(
// Copy a symlink file (no-follow).
// rename_symlink_fallback already preserves ownership and removes the source.
rename_symlink_fallback(from, to)?;
} else if is_fifo(from.symlink_metadata()?.file_type()) {
} else if from.symlink_metadata()?.file_type().is_fifo() {
// rustix::fs::mkfifoat is linux only
nix::unistd::mkfifo(to, nix::sys::stat::Mode::from_bits_truncate(0o666))?;
// Preserve ownership (uid/gid) from the source
Expand Down
12 changes: 11 additions & 1 deletion src/uucore/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ fluent-syntax = { workspace = true }
unic-langid = { workspace = true }
thiserror = { workspace = true }

[build-dependencies]
cfg_aliases = { workspace = true }

[dev-dependencies]
tempfile = { workspace = true }

Expand Down Expand Up @@ -156,7 +159,14 @@ encoding = ["data-encoding", "data-encoding-macro", "z85", "base64-simd"]
entries = ["libc", "rustix/fs", "rustix/process"]
extendedbigdecimal = ["bigdecimal", "num-traits"]
fast-inc = []
fs = ["dunce", "libc", "rustix/fs", "rustix/std", "windows-sys"]
fs = [
"dunce",
"libc",
"rustix/fs",
"rustix/std",
"safe-traversal",
"windows-sys",
]
fsext = ["libc", "windows-sys", "bstr", "wide"]
fsxattr = ["xattr", "itertools", "libc"]
hardware = []
Expand Down
19 changes: 19 additions & 0 deletions src/uucore/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,31 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.

// spell-checker:ignore tvos watchos visionos

use cfg_aliases::cfg_aliases;
use std::env;
use std::fs::File;
use std::io::Write;
use std::path::{Path, PathBuf};

pub fn main() -> Result<(), Box<dyn std::error::Error>> {
cfg_aliases! {
// Directory open flags that grant search but not read access.
has_o_path: { any(target_os = "linux", target_os = "android") },
has_o_search: { any(
target_os = "macos",
target_os = "ios",
target_os = "tvos",
target_os = "watchos",
target_os = "visionos",
target_os = "freebsd",
target_os = "netbsd",
target_os = "illumos",
target_os = "solaris"
) },
}

let out_dir = env::var("OUT_DIR")?;

let mut embedded_file = File::create(Path::new(&out_dir).join("embedded_locales.rs"))?;
Expand Down
1 change: 1 addition & 0 deletions src/uucore/locales/en-US.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ error-invalid-argument = Invalid argument
error-is-a-directory-text = Is a directory
error-is-a-directory = { $file }: { error-is-a-directory-text }
error-too-many-symlink-levels = Too many levels of symbolic links
error-no-unique-temp-name = no unique temporary name available in the destination directory
# Common actions
action-copying = copying
Expand Down
1 change: 1 addition & 0 deletions src/uucore/locales/fr-FR.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ error-invalid-argument = Argument invalide
error-is-a-directory-text = Est un répertoire
error-is-a-directory = { $file }: { error-is-a-directory-text }
error-too-many-symlink-levels = Trop de niveaux de liens symboliques
error-no-unique-temp-name = aucun nom temporaire unique disponible dans le répertoire de destination

# Actions communes
action-copying = copie
Expand Down
Loading
Loading