Skip to content

Commit d138468

Browse files
committed
fix(install): address PR review comments for symlink race condition fix
- Replace std::io::copy() with copy_stream() for consistency and splice optimization - Consolidate DirFd::open() and open_subdir() to take follow_symlinks parameter - Extract finalize_installed_file() helper to reduce code duplication - Add documentation for security behavior (symlink removal) and mode handling - Clean up verbose comments
1 parent f0a874b commit d138468

6 files changed

Lines changed: 91 additions & 138 deletions

File tree

src/uu/chmod/src/chmod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -480,7 +480,7 @@ impl Chmoder {
480480

481481
// If the path is a directory (or we should follow symlinks), recurse into it using safe traversal
482482
if (!file_path.is_symlink() || should_follow_symlink) && file_path.is_dir() {
483-
match DirFd::open(file_path) {
483+
match DirFd::open(file_path, true) {
484484
Ok(dir_fd) => {
485485
r = self.safe_traverse_dir(&dir_fd, file_path).and(r);
486486
}
@@ -534,7 +534,7 @@ impl Chmoder {
534534

535535
// Recurse into subdirectories using the existing directory fd
536536
if meta.is_dir() {
537-
match dir_fd.open_subdir(&entry_name) {
537+
match dir_fd.open_subdir(&entry_name, true) {
538538
Ok(child_dir_fd) => {
539539
r = self.safe_traverse_dir(&child_dir_fd, &entry_path).and(r);
540540
}

src/uu/du/src/du.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,7 @@ fn safe_du(
358358
Ok(s) => s,
359359
Err(_e) => {
360360
// Try using our new DirFd method for the root directory
361-
match DirFd::open(path) {
361+
match DirFd::open(path, true) {
362362
Ok(dir_fd) => match Stat::new_from_dirfd(&dir_fd, path) {
363363
Ok(s) => s,
364364
Err(e) => {
@@ -396,8 +396,8 @@ fn safe_du(
396396

397397
// Open the directory using DirFd
398398
let open_result = match parent_fd {
399-
Some(parent) => parent.open_subdir(path.file_name().unwrap_or(path.as_os_str())),
400-
None => DirFd::open(path),
399+
Some(parent) => parent.open_subdir(path.file_name().unwrap_or(path.as_os_str()), true),
400+
None => DirFd::open(path, true),
401401
};
402402

403403
let dir_fd = match open_result {

src/uu/install/src/install.rs

Lines changed: 38 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -646,7 +646,7 @@ fn standard(mut paths: Vec<OsString>, b: &Behavior) -> UResult<()> {
646646
&& sources.len() == 1
647647
&& !is_potential_directory_path(&target)
648648
{
649-
if let Ok(dir_fd) = DirFd::open_no_follow(to_create) {
649+
if let Ok(dir_fd) = DirFd::open(to_create, false) {
650650
if let Some(filename) = target.file_name() {
651651
target_parent_fd = Some(dir_fd);
652652
target_filename = Some(filename.to_os_string());
@@ -672,6 +672,8 @@ fn standard(mut paths: Vec<OsString>, b: &Behavior) -> UResult<()> {
672672

673673
#[cfg(unix)]
674674
{
675+
// Use DEFAULT_MODE (0o755) for created directories - this matches GNU install
676+
// behavior. The actual mode will be modified by umask at the kernel level.
675677
match create_dir_all_safe(to_create, DEFAULT_MODE) {
676678
Ok(dir_fd) => {
677679
if b.target_dir.is_none()
@@ -768,47 +770,7 @@ fn standard(mut paths: Vec<OsString>, b: &Behavior) -> UResult<()> {
768770

769771
copy_file_safe(source, parent_fd, filename.as_os_str())?;
770772

771-
#[cfg(not(windows))]
772-
if b.strip {
773-
strip_file(&target, b)?;
774-
}
775-
776-
set_ownership_and_permissions(&target, b)?;
777-
778-
if b.preserve_timestamps {
779-
preserve_timestamps(source, &target)?;
780-
}
781-
782-
#[cfg(all(feature = "selinux", target_os = "linux"))]
783-
if !b.unprivileged {
784-
if b.preserve_context {
785-
uucore::selinux::preserve_security_context(source, &target)
786-
.map_err(|e| InstallError::SelinuxContextFailed(e.to_string()))?;
787-
} else if b.default_context {
788-
set_selinux_default_context(&target)
789-
.map_err(|e| InstallError::SelinuxContextFailed(e.to_string()))?;
790-
} else if b.context.is_some() {
791-
let context = get_context_for_selinux(b);
792-
set_selinux_security_context(&target, context)
793-
.map_err(|e| InstallError::SelinuxContextFailed(e.to_string()))?;
794-
}
795-
}
796-
797-
if b.verbose {
798-
print!(
799-
"{}",
800-
translate!("install-verbose-copy", "from" => source.quote(), "to" => target.quote())
801-
);
802-
match backup_path {
803-
Some(path) => println!(
804-
" {}",
805-
translate!("install-verbose-backup", "backup" => path.quote())
806-
),
807-
None => println!(),
808-
}
809-
}
810-
811-
Ok(())
773+
finalize_installed_file(source, &target, b, backup_path)
812774
} else {
813775
copy(source, &target, b)
814776
}
@@ -964,8 +926,7 @@ fn copy_file_safe(from: &Path, to_parent_fd: &DirFd, to_filename: &std::ffi::OsS
964926

965927
let mut src = File::open(from)?;
966928
let mut dst = to_parent_fd.open_file_at(to_filename)?;
967-
std::io::copy(&mut src, &mut dst)?;
968-
dst.sync_all()?;
929+
copy_stream(&mut src, &mut dst)?;
969930

970931
Ok(())
971932
}
@@ -997,9 +958,7 @@ fn copy_file(from: &Path, to: &Path) -> UResult<()> {
997958
.into());
998959
}
999960

1000-
// Remove existing file at destination to allow overwriting
1001-
// Note: create_new() below provides TOCTOU protection; if something
1002-
// appears at this path between the remove and create, it will fail safely
961+
// Remove existing file (create_new below provides TOCTOU protection)
1003962
if let Err(e) = fs::remove_file(to) {
1004963
if e.kind() != std::io::ErrorKind::NotFound {
1005964
show_error!(
@@ -1010,7 +969,6 @@ fn copy_file(from: &Path, to: &Path) -> UResult<()> {
1010969
}
1011970

1012971
let mut handle = File::open(from)?;
1013-
// create_new provides TOCTOU protection
1014972
let mut dest = OpenOptions::new().write(true).create_new(true).open(to)?;
1015973

1016974
copy_stream(&mut handle, &mut dest).map_err(|err| {
@@ -1113,28 +1071,13 @@ fn preserve_timestamps(from: &Path, to: &Path) -> UResult<()> {
11131071
Ok(())
11141072
}
11151073

1116-
/// Copy one file to a new location, changing metadata.
1117-
///
1118-
/// Returns a Result type with the Err variant containing the error message.
1119-
///
1120-
/// # Parameters
1121-
///
1122-
/// _from_ must exist as a non-directory.
1123-
/// _to_ must be a non-existent file, whose parent directory exists.
1124-
///
1125-
/// # Errors
1126-
///
1127-
/// If the copy system call fails, we print a verbose error and return an empty error value.
1128-
///
1129-
fn copy(from: &Path, to: &Path, b: &Behavior) -> UResult<()> {
1130-
if b.compare && !need_copy(from, to, b) {
1131-
return Ok(());
1132-
}
1133-
// Declare the path here as we may need it for the verbose output below.
1134-
let backup_path = perform_backup(to, b)?;
1135-
1136-
copy_file(from, to)?;
1137-
1074+
/// Apply post-copy operations: strip, ownership, permissions, timestamps, SELinux, and verbose output.
1075+
fn finalize_installed_file(
1076+
from: &Path,
1077+
to: &Path,
1078+
b: &Behavior,
1079+
backup_path: Option<PathBuf>,
1080+
) -> UResult<()> {
11381081
#[cfg(not(windows))]
11391082
if b.strip {
11401083
strip_file(to, b)?;
@@ -1178,6 +1121,31 @@ fn copy(from: &Path, to: &Path, b: &Behavior) -> UResult<()> {
11781121
Ok(())
11791122
}
11801123

1124+
/// Copy one file to a new location, changing metadata.
1125+
///
1126+
/// Returns a Result type with the Err variant containing the error message.
1127+
///
1128+
/// # Parameters
1129+
///
1130+
/// _from_ must exist as a non-directory.
1131+
/// _to_ must be a non-existent file, whose parent directory exists.
1132+
///
1133+
/// # Errors
1134+
///
1135+
/// If the copy system call fails, we print a verbose error and return an empty error value.
1136+
///
1137+
fn copy(from: &Path, to: &Path, b: &Behavior) -> UResult<()> {
1138+
if b.compare && !need_copy(from, to, b) {
1139+
return Ok(());
1140+
}
1141+
// Declare the path here as we may need it for the verbose output below.
1142+
let backup_path = perform_backup(to, b)?;
1143+
1144+
copy_file(from, to)?;
1145+
1146+
finalize_installed_file(from, to, b, backup_path)
1147+
}
1148+
11811149
#[cfg(all(feature = "selinux", target_os = "linux"))]
11821150
fn get_context_for_selinux(b: &Behavior) -> Option<&String> {
11831151
if b.default_context {

src/uu/rm/src/platform/unix.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ pub fn safe_remove_file(
120120
let parent = path.parent().unwrap_or(Path::new("."));
121121
let file_name = path.file_name()?;
122122

123-
let dir_fd = DirFd::open(parent).ok()?;
123+
let dir_fd = DirFd::open(parent, true).ok()?;
124124

125125
match dir_fd.unlink_at(file_name, false) {
126126
Ok(_) => {
@@ -151,7 +151,7 @@ pub fn safe_remove_empty_dir(
151151
let parent = path.parent().unwrap_or(Path::new("."));
152152
let dir_name = path.file_name()?;
153153

154-
let dir_fd = DirFd::open(parent).ok()?;
154+
let dir_fd = DirFd::open(parent, true).ok()?;
155155

156156
match dir_fd.unlink_at(dir_name, true) {
157157
Ok(_) => {
@@ -290,7 +290,7 @@ pub fn safe_remove_dir_recursive(
290290
};
291291

292292
// Try to open the directory using DirFd for secure traversal
293-
let dir_fd = match DirFd::open(path) {
293+
let dir_fd = match DirFd::open(path, true) {
294294
Ok(fd) => fd,
295295
Err(e) => {
296296
// If we can't open the directory for safe traversal,
@@ -389,7 +389,7 @@ pub fn safe_remove_dir_recursive_impl(path: &Path, dir_fd: &DirFd, options: &Opt
389389
}
390390

391391
// Recursively remove subdirectory using safe traversal
392-
let child_dir_fd = match dir_fd.open_subdir(&entry_name) {
392+
let child_dir_fd = match dir_fd.open_subdir(&entry_name, true) {
393393
Ok(fd) => fd,
394394
Err(e) => {
395395
// If we can't open the subdirectory for safe traversal,

src/uucore/src/lib/features/perms.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,7 @@ impl ChownExecutor {
311311
#[cfg(target_os = "linux")]
312312
let chown_result = if path.is_dir() {
313313
// For directories on Linux, use safe traversal from the start
314-
match DirFd::open(path) {
314+
match DirFd::open(path, true) {
315315
Ok(dir_fd) => self
316316
.safe_chown_dir(&dir_fd, path, &meta)
317317
.map(|_| String::new()),
@@ -537,7 +537,7 @@ impl ChownExecutor {
537537

538538
// Recurse into subdirectories
539539
if meta.is_dir() && (follow || !meta.file_type().is_symlink()) {
540-
match dir_fd.open_subdir(&entry_name) {
540+
match dir_fd.open_subdir(&entry_name, true) {
541541
Ok(subdir_fd) => {
542542
self.safe_traverse_dir(&subdir_fd, &entry_path, ret);
543543
}
@@ -695,7 +695,7 @@ impl ChownExecutor {
695695
/// Try to open directory with error reporting
696696
#[cfg(target_os = "linux")]
697697
fn try_open_dir(&self, path: &Path) -> Option<DirFd> {
698-
DirFd::open(path)
698+
DirFd::open(path, true)
699699
.map_err(|e| {
700700
if self.verbosity.level != VerbosityLevel::Silent {
701701
show_error!("cannot access {}: {}", path.quote(), strip_errno(&e));

0 commit comments

Comments
 (0)