Skip to content

Commit 4a92c9b

Browse files
sylvestrecakebaker
andauthored
safe traversal: adjust chmod & chgrp to use it (#8632)
* chown: implement safe traversal * uucore/safe_traversal: add secure chmod functions * safe traversal: adjust chmod & chgrp to use it * chmod dedup some code Co-authored-by: Daniel Hofstetter <daniel.hofstetter@42dh.com> * address review comments --------- Co-authored-by: Daniel Hofstetter <daniel.hofstetter@42dh.com>
1 parent 91160bb commit 4a92c9b

File tree

8 files changed

+568
-64
lines changed

8 files changed

+568
-64
lines changed

src/uu/chmod/Cargo.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,13 @@ path = "src/chmod.rs"
2020
[dependencies]
2121
clap = { workspace = true }
2222
thiserror = { workspace = true }
23-
uucore = { workspace = true, features = ["entries", "fs", "mode", "perms"] }
23+
uucore = { workspace = true, features = [
24+
"entries",
25+
"fs",
26+
"mode",
27+
"perms",
28+
"safe-traversal",
29+
] }
2430
fluent = { workspace = true }
2531

2632
[[bin]]

src/uu/chmod/src/chmod.rs

Lines changed: 237 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ use uucore::fs::display_permissions_unix;
1717
use uucore::libc::mode_t;
1818
use uucore::mode;
1919
use uucore::perms::{TraverseSymlinks, configure_symlink_and_recursion};
20+
21+
#[cfg(target_os = "linux")]
22+
use uucore::safe_traversal::DirFd;
2023
use uucore::{format_usage, show, show_error};
2124

2225
use uucore::translate;
@@ -266,6 +269,104 @@ struct Chmoder {
266269
}
267270

268271
impl Chmoder {
272+
/// Calculate the new mode based on the current mode and the chmod specification.
273+
/// Returns (`new_mode`, `naively_expected_new_mode`) for symbolic modes, or (`new_mode`, `new_mode`) for numeric/reference modes.
274+
fn calculate_new_mode(&self, current_mode: u32, is_dir: bool) -> UResult<(u32, u32)> {
275+
match self.fmode {
276+
Some(mode) => Ok((mode, mode)),
277+
None => {
278+
let cmode_unwrapped = self.cmode.clone().unwrap();
279+
let mut new_mode = current_mode;
280+
let mut naively_expected_new_mode = current_mode;
281+
282+
for mode in cmode_unwrapped.split(',') {
283+
let result = if mode.chars().any(|c| c.is_ascii_digit()) {
284+
mode::parse_numeric(new_mode, mode, is_dir).map(|v| (v, v))
285+
} else {
286+
mode::parse_symbolic(new_mode, mode, mode::get_umask(), is_dir).map(|m| {
287+
// calculate the new mode as if umask was 0
288+
let naive_mode =
289+
mode::parse_symbolic(naively_expected_new_mode, mode, 0, is_dir)
290+
.unwrap(); // we know that mode must be valid, so this cannot fail
291+
(m, naive_mode)
292+
})
293+
};
294+
295+
match result {
296+
Ok((mode, naive_mode)) => {
297+
new_mode = mode;
298+
naively_expected_new_mode = naive_mode;
299+
}
300+
Err(f) => {
301+
return if self.quiet {
302+
Err(ExitCode::new(1))
303+
} else {
304+
Err(USimpleError::new(1, f))
305+
};
306+
}
307+
}
308+
}
309+
Ok((new_mode, naively_expected_new_mode))
310+
}
311+
}
312+
}
313+
314+
/// Report permission changes based on verbose and changes flags
315+
fn report_permission_change(&self, file_path: &Path, old_mode: u32, new_mode: u32) {
316+
if self.verbose || self.changes {
317+
let current_permissions = display_permissions_unix(old_mode as mode_t, false);
318+
let new_permissions = display_permissions_unix(new_mode as mode_t, false);
319+
320+
if new_mode != old_mode {
321+
println!(
322+
"mode of {} changed from {:04o} ({}) to {:04o} ({})",
323+
file_path.quote(),
324+
old_mode,
325+
current_permissions,
326+
new_mode,
327+
new_permissions
328+
);
329+
} else if self.verbose {
330+
println!(
331+
"mode of {} retained as {:04o} ({})",
332+
file_path.quote(),
333+
old_mode,
334+
current_permissions
335+
);
336+
}
337+
}
338+
}
339+
340+
/// Handle symlinks during directory traversal based on traversal mode
341+
#[cfg(not(target_os = "linux"))]
342+
fn handle_symlink_during_traversal(
343+
&self,
344+
path: &Path,
345+
is_command_line_arg: bool,
346+
) -> UResult<()> {
347+
let should_follow_symlink = match self.traverse_symlinks {
348+
TraverseSymlinks::All => true,
349+
TraverseSymlinks::First => is_command_line_arg,
350+
TraverseSymlinks::None => false,
351+
};
352+
353+
if !should_follow_symlink {
354+
return self.chmod_file_internal(path, false);
355+
}
356+
357+
match fs::metadata(path) {
358+
Ok(meta) if meta.is_dir() => self.walk_dir_with_context(path, false),
359+
Ok(_) => {
360+
// It's a file symlink, chmod it
361+
self.chmod_file(path)
362+
}
363+
Err(_) => {
364+
// Dangling symlink, chmod it without dereferencing
365+
self.chmod_file_internal(path, false)
366+
}
367+
}
368+
}
369+
269370
fn chmod(&self, files: &[OsString]) -> UResult<()> {
270371
let mut r = Ok(());
271372

@@ -322,6 +423,7 @@ impl Chmoder {
322423
r
323424
}
324425

426+
#[cfg(not(target_os = "linux"))]
325427
fn walk_dir_with_context(&self, file_path: &Path, is_command_line_arg: bool) -> UResult<()> {
326428
let mut r = self.chmod_file(file_path);
327429

@@ -352,17 +454,100 @@ impl Chmoder {
352454
r
353455
}
354456

355-
fn handle_symlink_during_recursion(&self, path: &Path) -> UResult<()> {
457+
#[cfg(target_os = "linux")]
458+
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+
461+
// Determine whether to traverse symlinks based on context and traversal mode
462+
let should_follow_symlink = match self.traverse_symlinks {
463+
TraverseSymlinks::All => true,
464+
TraverseSymlinks::First => is_command_line_arg, // Only follow symlinks that are command line args
465+
TraverseSymlinks::None => false,
466+
};
467+
468+
// If the path is a directory (or we should follow symlinks), recurse into it using safe traversal
469+
if (!file_path.is_symlink() || should_follow_symlink) && file_path.is_dir() {
470+
match DirFd::open(file_path) {
471+
Ok(dir_fd) => {
472+
r = self.safe_traverse_dir(&dir_fd, file_path).and(r);
473+
}
474+
Err(err) => {
475+
// Handle permission denied errors with proper file path context
476+
if err.kind() == std::io::ErrorKind::PermissionDenied {
477+
r = r.and(Err(ChmodError::PermissionDenied(
478+
file_path.to_string_lossy().to_string(),
479+
)
480+
.into()));
481+
} else {
482+
r = r.and(Err(err.into()));
483+
}
484+
}
485+
}
486+
}
487+
r
488+
}
489+
490+
#[cfg(target_os = "linux")]
491+
fn safe_traverse_dir(&self, dir_fd: &DirFd, dir_path: &Path) -> UResult<()> {
492+
let mut r = Ok(());
493+
494+
let entries = dir_fd.read_dir()?;
495+
496+
// Determine if we should follow symlinks (doesn't depend on entry_name)
497+
let should_follow_symlink = self.traverse_symlinks == TraverseSymlinks::All;
498+
499+
for entry_name in entries {
500+
let entry_path = dir_path.join(&entry_name);
501+
502+
let dir_meta = dir_fd.metadata_at(&entry_name, should_follow_symlink);
503+
let Ok(meta) = dir_meta else {
504+
// Handle permission denied with proper file path context
505+
let e = dir_meta.unwrap_err();
506+
let error = if e.kind() == std::io::ErrorKind::PermissionDenied {
507+
ChmodError::PermissionDenied(entry_path.to_string_lossy().to_string()).into()
508+
} else {
509+
e.into()
510+
};
511+
r = r.and(Err(error));
512+
continue;
513+
};
514+
515+
if entry_path.is_symlink() {
516+
r = self
517+
.handle_symlink_during_safe_recursion(&entry_path, dir_fd, &entry_name)
518+
.and(r);
519+
} else {
520+
// For regular files and directories, chmod them
521+
r = self
522+
.safe_chmod_file(&entry_path, dir_fd, &entry_name, meta.mode() & 0o7777)
523+
.and(r);
524+
525+
// Recurse into subdirectories
526+
if meta.is_dir() {
527+
r = self.walk_dir_with_context(&entry_path, false).and(r);
528+
}
529+
}
530+
}
531+
r
532+
}
533+
534+
#[cfg(target_os = "linux")]
535+
fn handle_symlink_during_safe_recursion(
536+
&self,
537+
path: &Path,
538+
dir_fd: &DirFd,
539+
entry_name: &std::ffi::OsStr,
540+
) -> UResult<()> {
356541
// During recursion, determine behavior based on traversal mode
357542
match self.traverse_symlinks {
358543
TraverseSymlinks::All => {
359544
// Follow all symlinks during recursion
360545
// Check if the symlink target is a directory, but handle dangling symlinks gracefully
361546
match fs::metadata(path) {
362547
Ok(meta) if meta.is_dir() => self.walk_dir_with_context(path, false),
363-
Ok(_) => {
364-
// It's a file symlink, chmod it
365-
self.chmod_file(path)
548+
Ok(meta) => {
549+
// It's a file symlink, chmod it using safe traversal
550+
self.safe_chmod_file(path, dir_fd, entry_name, meta.mode() & 0o7777)
366551
}
367552
Err(_) => {
368553
// Dangling symlink, chmod it without dereferencing
@@ -378,12 +563,50 @@ impl Chmoder {
378563
}
379564
}
380565

566+
#[cfg(target_os = "linux")]
567+
fn safe_chmod_file(
568+
&self,
569+
file_path: &Path,
570+
dir_fd: &DirFd,
571+
entry_name: &std::ffi::OsStr,
572+
current_mode: u32,
573+
) -> UResult<()> {
574+
// Calculate the new mode using the helper method
575+
let (new_mode, _) = self.calculate_new_mode(current_mode, file_path.is_dir())?;
576+
577+
// Use safe traversal to change the mode
578+
let follow_symlinks = self.dereference;
579+
if let Err(_e) = dir_fd.chmod_at(entry_name, new_mode, follow_symlinks) {
580+
if self.verbose {
581+
println!(
582+
"failed to change mode of {} to {:o}",
583+
file_path.quote(),
584+
new_mode
585+
);
586+
}
587+
return Err(
588+
ChmodError::PermissionDenied(file_path.to_string_lossy().to_string()).into(),
589+
);
590+
}
591+
592+
// Report the change using the helper method
593+
self.report_permission_change(file_path, current_mode, new_mode);
594+
595+
Ok(())
596+
}
597+
598+
#[cfg(not(target_os = "linux"))]
599+
fn handle_symlink_during_recursion(&self, path: &Path) -> UResult<()> {
600+
// Use the common symlink handling logic
601+
self.handle_symlink_during_traversal(path, false)
602+
}
603+
381604
fn chmod_file(&self, file: &Path) -> UResult<()> {
382605
self.chmod_file_internal(file, self.dereference)
383606
}
384607

385608
fn chmod_file_internal(&self, file: &Path, dereference: bool) -> UResult<()> {
386-
use uucore::{mode::get_umask, perms::get_metadata};
609+
use uucore::perms::get_metadata;
387610

388611
let metadata = get_metadata(file, dereference);
389612

@@ -409,45 +632,14 @@ impl Chmoder {
409632
}
410633
};
411634

412-
// Determine the new permissions to apply
635+
// Calculate the new mode using the helper method
636+
let (new_mode, naively_expected_new_mode) =
637+
self.calculate_new_mode(fperm, file.is_dir())?;
638+
639+
// Determine how to apply the permissions
413640
match self.fmode {
414641
Some(mode) => self.change_file(fperm, mode, file)?,
415642
None => {
416-
let cmode_unwrapped = self.cmode.clone().unwrap();
417-
let mut new_mode = fperm;
418-
let mut naively_expected_new_mode = new_mode;
419-
for mode in cmode_unwrapped.split(',') {
420-
let result = if mode.chars().any(|c| c.is_ascii_digit()) {
421-
mode::parse_numeric(new_mode, mode, file.is_dir()).map(|v| (v, v))
422-
} else {
423-
mode::parse_symbolic(new_mode, mode, get_umask(), file.is_dir()).map(|m| {
424-
// calculate the new mode as if umask was 0
425-
let naive_mode = mode::parse_symbolic(
426-
naively_expected_new_mode,
427-
mode,
428-
0,
429-
file.is_dir(),
430-
)
431-
.unwrap(); // we know that mode must be valid, so this cannot fail
432-
(m, naive_mode)
433-
})
434-
};
435-
436-
match result {
437-
Ok((mode, naive_mode)) => {
438-
new_mode = mode;
439-
naively_expected_new_mode = naive_mode;
440-
}
441-
Err(f) => {
442-
return if self.quiet {
443-
Err(ExitCode::new(1))
444-
} else {
445-
Err(USimpleError::new(1, f))
446-
};
447-
}
448-
}
449-
}
450-
451643
// Special handling for symlinks when not dereferencing
452644
if file.is_symlink() && !dereference {
453645
// TODO: On most Unix systems, symlink permissions are ignored by the kernel,
@@ -479,13 +671,8 @@ impl Chmoder {
479671

480672
fn change_file(&self, fperm: u32, mode: u32, file: &Path) -> Result<(), i32> {
481673
if fperm == mode {
482-
if self.verbose && !self.changes {
483-
println!(
484-
"mode of {} retained as {fperm:04o} ({})",
485-
file.quote(),
486-
display_permissions_unix(fperm as mode_t, false),
487-
);
488-
}
674+
// Use the helper method for consistent reporting
675+
self.report_permission_change(file, fperm, mode);
489676
Ok(())
490677
} else if let Err(err) = fs::set_permissions(file, fs::Permissions::from_mode(mode)) {
491678
if !self.quiet {
@@ -501,14 +688,8 @@ impl Chmoder {
501688
}
502689
Err(1)
503690
} else {
504-
if self.verbose || self.changes {
505-
println!(
506-
"mode of {} changed from {fperm:04o} ({}) to {mode:04o} ({})",
507-
file.quote(),
508-
display_permissions_unix(fperm as mode_t, false),
509-
display_permissions_unix(mode as mode_t, false)
510-
);
511-
}
691+
// Use the helper method for consistent reporting
692+
self.report_permission_change(file, fperm, mode);
512693
Ok(())
513694
}
514695
}

src/uu/chown/Cargo.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,12 @@ path = "src/chown.rs"
1919

2020
[dependencies]
2121
clap = { workspace = true }
22-
uucore = { workspace = true, features = ["entries", "fs", "perms"] }
22+
uucore = { workspace = true, features = [
23+
"entries",
24+
"fs",
25+
"perms",
26+
"safe-traversal",
27+
] }
2328
fluent = { workspace = true }
2429

2530
[[bin]]

0 commit comments

Comments
 (0)