|
5 | 5 |
|
6 | 6 | //! Common functions to manage permissions |
7 | 7 |
|
8 | | -// spell-checker:ignore (jargon) TOCTOU fchownat |
| 8 | +// spell-checker:ignore (jargon) TOCTOU fchownat fchown |
9 | 9 |
|
10 | 10 | use crate::display::Quotable; |
11 | 11 | use crate::error::{UResult, USimpleError, strip_errno}; |
@@ -307,14 +307,41 @@ impl ChownExecutor { |
307 | 307 | } |
308 | 308 |
|
309 | 309 | 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) => { |
| 317 | + // Don't show error here - let safe_dive_into handle directory traversal errors |
| 318 | + // This prevents duplicate error messages |
| 319 | + Ok(String::new()) |
| 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( |
311 | 336 | path, |
312 | 337 | &meta, |
313 | 338 | self.dest_uid, |
314 | 339 | self.dest_gid, |
315 | 340 | self.dereference, |
316 | 341 | self.verbosity.clone(), |
317 | | - ) { |
| 342 | + ); |
| 343 | + |
| 344 | + match chown_result { |
318 | 345 | Ok(n) => { |
319 | 346 | if !n.is_empty() { |
320 | 347 | show_error!("{n}"); |
@@ -351,6 +378,60 @@ impl ChownExecutor { |
351 | 378 | } |
352 | 379 | } |
353 | 380 |
|
| 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 | + let dest_uid = self.dest_uid.unwrap_or_else(|| meta.uid()); |
| 389 | + let dest_gid = self.dest_gid.unwrap_or_else(|| meta.gid()); |
| 390 | + |
| 391 | + // Use fchown (safe) to change the directory's ownership |
| 392 | + if let Err(e) = dir_fd.fchown(self.dest_uid, self.dest_gid) { |
| 393 | + let mut error_msg = format!( |
| 394 | + "changing {} of {}: {}", |
| 395 | + if self.verbosity.groups_only { |
| 396 | + "group" |
| 397 | + } else { |
| 398 | + "ownership" |
| 399 | + }, |
| 400 | + path.quote(), |
| 401 | + e |
| 402 | + ); |
| 403 | + |
| 404 | + if self.verbosity.level == VerbosityLevel::Verbose { |
| 405 | + error_msg = if self.verbosity.groups_only { |
| 406 | + let gid = meta.gid(); |
| 407 | + format!( |
| 408 | + "{error_msg}\nfailed to change group of {} from {} to {}", |
| 409 | + path.quote(), |
| 410 | + entries::gid2grp(gid).unwrap_or_else(|_| gid.to_string()), |
| 411 | + entries::gid2grp(dest_gid).unwrap_or_else(|_| dest_gid.to_string()) |
| 412 | + ) |
| 413 | + } else { |
| 414 | + let uid = meta.uid(); |
| 415 | + let gid = meta.gid(); |
| 416 | + format!( |
| 417 | + "{error_msg}\nfailed to change ownership of {} from {}:{} to {}:{}", |
| 418 | + path.quote(), |
| 419 | + entries::uid2usr(uid).unwrap_or_else(|_| uid.to_string()), |
| 420 | + entries::gid2grp(gid).unwrap_or_else(|_| gid.to_string()), |
| 421 | + entries::uid2usr(dest_uid).unwrap_or_else(|_| dest_uid.to_string()), |
| 422 | + entries::gid2grp(dest_gid).unwrap_or_else(|_| dest_gid.to_string()) |
| 423 | + ) |
| 424 | + }; |
| 425 | + } |
| 426 | + |
| 427 | + return Err(error_msg); |
| 428 | + } |
| 429 | + |
| 430 | + // Report the change if verbose (similar to wrap_chown) |
| 431 | + self.report_ownership_change_success(path, meta.uid(), meta.gid()); |
| 432 | + Ok(String::new()) |
| 433 | + } |
| 434 | + |
354 | 435 | #[cfg(all(target_os = "linux", feature = "safe-traversal"))] |
355 | 436 | fn safe_dive_into<P: AsRef<Path>>(&self, root: P) -> i32 { |
356 | 437 | let root = root.as_ref(); |
|
0 commit comments