Skip to content

Commit 01a176c

Browse files
committed
chmod: on linux use the safe traversal functions
1 parent 4fe6b36 commit 01a176c

File tree

2 files changed

+93
-11
lines changed

2 files changed

+93
-11
lines changed

src/uu/chmod/src/chmod.rs

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -456,35 +456,69 @@ impl Chmoder {
456456

457457
#[cfg(target_os = "linux")]
458458
fn walk_dir_with_context(&self, file_path: &Path, is_command_line_arg: bool) -> UResult<()> {
459-
let mut r = self.chmod_file(file_path);
460-
461459
// Determine whether to traverse symlinks based on context and traversal mode
462460
let should_follow_symlink = match self.traverse_symlinks {
463461
TraverseSymlinks::All => true,
464462
TraverseSymlinks::First => is_command_line_arg, // Only follow symlinks that are command line args
465463
TraverseSymlinks::None => false,
466464
};
467465

468-
// If the path is a directory (or we should follow symlinks), recurse into it using safe traversal
466+
// Use safe syscalls for the root directory as well to prevent TOCTOU attacks
469467
if (!file_path.is_symlink() || should_follow_symlink) && file_path.is_dir() {
468+
// For directories, use safe traversal from the start
470469
match DirFd::open(file_path) {
471470
Ok(dir_fd) => {
472-
r = self.safe_traverse_dir(&dir_fd, file_path).and(r);
471+
// First chmod the directory itself using fchmod (safe)
472+
let dir_result = self.safe_chmod_dir(&dir_fd, file_path);
473+
// Then traverse its contents
474+
let traverse_result = self.safe_traverse_dir(&dir_fd, file_path);
475+
dir_result.and(traverse_result)
473476
}
474477
Err(err) => {
475478
// Handle permission denied errors with proper file path context
476479
if err.kind() == std::io::ErrorKind::PermissionDenied {
477-
r = r.and(Err(ChmodError::PermissionDenied(
478-
file_path.to_string_lossy().to_string(),
480+
Err(
481+
ChmodError::PermissionDenied(file_path.to_string_lossy().to_string())
482+
.into(),
479483
)
480-
.into()));
481484
} else {
482-
r = r.and(Err(err.into()));
485+
Err(err.into())
483486
}
484487
}
485488
}
489+
} else {
490+
// For non-directories (files, symlinks), use the regular chmod_file method
491+
self.chmod_file(file_path)
486492
}
487-
r
493+
}
494+
495+
#[cfg(target_os = "linux")]
496+
fn safe_chmod_dir(&self, dir_fd: &DirFd, file_path: &Path) -> UResult<()> {
497+
// Get the current mode using fstat (safe)
498+
let stat = dir_fd
499+
.fstat()
500+
.map_err(|_e| ChmodError::PermissionDenied(file_path.to_string_lossy().to_string()))?;
501+
502+
let current_mode = stat.st_mode;
503+
let (new_mode, _) = self.calculate_new_mode(current_mode, true)?; // true = is_dir
504+
505+
// Use fchmod (safe) to change the directory's mode
506+
if let Err(_e) = dir_fd.fchmod(new_mode) {
507+
if self.verbose {
508+
println!(
509+
"failed to change mode of {} to {:o}",
510+
file_path.quote(),
511+
new_mode
512+
);
513+
}
514+
return Err(
515+
ChmodError::PermissionDenied(file_path.to_string_lossy().to_string()).into(),
516+
);
517+
}
518+
519+
// Report the change if verbose
520+
self.report_permission_change(file_path, current_mode, new_mode);
521+
Ok(())
488522
}
489523

490524
#[cfg(target_os = "linux")]

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

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -307,14 +307,41 @@ impl ChownExecutor {
307307
}
308308

309309
let ret = if self.matched(meta.uid(), meta.gid()) {
310-
match wrap_chown(
310+
// Use safe syscalls for root directory to prevent TOCTOU attacks
311+
#[cfg(all(target_os = "linux", feature = "safe-traversal"))]
312+
let chown_result = if path.is_dir() {
313+
// For directories, use safe traversal from the start
314+
match DirFd::open(path) {
315+
Ok(dir_fd) => self.safe_chown_dir(&dir_fd, path, &meta),
316+
Err(e) => Err(format!(
317+
"cannot access '{}': {}",
318+
path.display(),
319+
strip_errno(&e)
320+
)),
321+
}
322+
} else {
323+
// For non-directories (files, symlinks), use the regular wrap_chown method
324+
wrap_chown(
325+
path,
326+
&meta,
327+
self.dest_uid,
328+
self.dest_gid,
329+
self.dereference,
330+
self.verbosity.clone(),
331+
)
332+
};
333+
334+
#[cfg(not(all(target_os = "linux", feature = "safe-traversal")))]
335+
let chown_result = wrap_chown(
311336
path,
312337
&meta,
313338
self.dest_uid,
314339
self.dest_gid,
315340
self.dereference,
316341
self.verbosity.clone(),
317-
) {
342+
);
343+
344+
match chown_result {
318345
Ok(n) => {
319346
if !n.is_empty() {
320347
show_error!("{n}");
@@ -351,6 +378,27 @@ impl ChownExecutor {
351378
}
352379
}
353380

381+
#[cfg(all(target_os = "linux", feature = "safe-traversal"))]
382+
fn safe_chown_dir(
383+
&self,
384+
dir_fd: &DirFd,
385+
path: &Path,
386+
meta: &Metadata,
387+
) -> Result<String, String> {
388+
// Use fchown (safe) to change the directory's ownership
389+
if let Err(e) = dir_fd.fchown(self.dest_uid, self.dest_gid) {
390+
return Err(format!(
391+
"cannot change ownership of '{}': {}",
392+
path.display(),
393+
strip_errno(&e)
394+
));
395+
}
396+
397+
// Report the change if verbose (similar to wrap_chown)
398+
self.report_ownership_change_success(path, meta.uid(), meta.gid());
399+
Ok(String::new())
400+
}
401+
354402
#[cfg(all(target_os = "linux", feature = "safe-traversal"))]
355403
fn safe_dive_into<P: AsRef<Path>>(&self, root: P) -> i32 {
356404
let root = root.as_ref();

0 commit comments

Comments
 (0)