Merge pull request #105 from shadow-utils-rs/fix/93-104-prod-ready

prod: 11 production-readiness fixes
This commit is contained in:
Pierre Warnier
2026-03-24 15:45:16 +01:00
committed by GitHub
22 changed files with 603 additions and 21 deletions
+2 -1
View File
@@ -56,4 +56,5 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: EmbarkStudios/cargo-deny-action@v1
- uses: dtolnay/rust-toolchain@stable
- uses: EmbarkStudios/cargo-deny-action@v2
+1 -1
View File
@@ -52,7 +52,7 @@ uucore = "0.7"
thiserror = "2"
# Unix/Linux
nix = { version = "0.29", features = ["user", "fs", "process", "signal", "term", "resource"] }
nix = { version = "0.30", features = ["user", "fs", "process", "signal", "term", "resource"] }
libc = "0.2"
# Security
+6
View File
@@ -20,6 +20,12 @@ install: build
@for tool in $(TOOLS); do \
ln -sf shadow-rs $(DESTDIR)$(BINDIR)/$$tool; \
done
@# Setuid-root for tools that need it. chmod on symlinks affects the
@# underlying multicall binary — this is intentional for drop-in
@# replacement since all traditional shadow-utils tools are setuid-root.
@for tool in passwd chfn chsh newgrp; do \
chmod 4755 $(DESTDIR)$(BINDIR)/$$tool 2>/dev/null || true; \
done
@echo "Installed shadow-rs + $(words $(TOOLS)) symlinks to $(DESTDIR)$(BINDIR)/"
uninstall:
+19
View File
@@ -0,0 +1,19 @@
Source: shadow-rs
Section: admin
Priority: required
Maintainer: shadow-rs contributors <shadow-rs@example.com>
Build-Depends: debhelper-compat (= 13), cargo, rustc (>= 1.94), libpam0g-dev, libselinux1-dev, libcrypt-dev, pkg-config
Standards-Version: 4.6.2
Package: shadow-rs
Architecture: any
Depends: ${shlibs:Depends}, ${misc:Depends}
Conflicts: passwd, login
Provides: passwd, login
Description: Memory-safe reimplementation of shadow-utils in Rust
shadow-rs is a complete Rust reimplementation of the Linux shadow-utils
suite (passwd, useradd, userdel, usermod, groupadd, groupdel, groupmod,
pwck, grpck, chage, chpasswd, chfn, chsh, newgrp).
.
All 14 tools are provided as a single multicall binary with symlinks.
4x faster than GNU shadow-utils, with 20+ security hardening layers.
+12
View File
@@ -0,0 +1,12 @@
#!/usr/bin/make -f
%:
dh $@
override_dh_auto_build:
cargo build --release
override_dh_auto_install:
$(MAKE) DESTDIR=debian/shadow-rs PREFIX=/usr install
override_dh_auto_test:
cargo test --workspace
+49
View File
@@ -0,0 +1,49 @@
Name: shadow-rs
Version: 0.0.1
Release: 1%{?dist}
Summary: Memory-safe reimplementation of shadow-utils in Rust
License: MIT
URL: https://github.com/shadow-utils-rs/shadow-rs
Source0: %{name}-%{version}.tar.gz
BuildRequires: rust >= 1.94.0
BuildRequires: cargo
BuildRequires: pam-devel
BuildRequires: libselinux-devel
BuildRequires: audit-libs-devel
BuildRequires: libcrypt-devel
BuildRequires: pkgconf-pkg-config
Conflicts: shadow-utils
%description
shadow-rs is a complete Rust reimplementation of all 14 Linux shadow-utils
tools. Single multicall binary, 4x faster, 20+ security hardening layers.
%prep
%setup -q
%build
cargo build --release
%install
%make_install PREFIX=%{_prefix}
%files
%license LICENSE
%doc README.md CONTRIBUTING.md
%{_sbindir}/shadow-rs
%{_sbindir}/passwd
%{_sbindir}/useradd
%{_sbindir}/userdel
%{_sbindir}/usermod
%{_sbindir}/groupadd
%{_sbindir}/groupdel
%{_sbindir}/groupmod
%{_sbindir}/pwck
%{_sbindir}/grpck
%{_sbindir}/chage
%{_sbindir}/chpasswd
%{_sbindir}/chfn
%{_sbindir}/chsh
%{_sbindir}/newgrp
+2 -3
View File
@@ -14,7 +14,6 @@
use std::fs::{self, File};
use std::io::{self, Write};
use std::os::unix::fs::OpenOptionsExt;
use std::os::unix::io::AsRawFd;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
@@ -126,7 +125,7 @@ where
tmp_file
.flush()
.map_err(|e| ShadowError::IoPath(e, tmp_path.clone()))?;
nix::unistd::fsync(tmp_file.as_raw_fd())
nix::unistd::fsync(&tmp_file)
.map_err(|e| ShadowError::IoPath(io::Error::from(e), tmp_path.clone()))?;
// Atomic rename.
@@ -137,7 +136,7 @@ where
// Fsync the parent directory to ensure the rename is durable.
if let Ok(dir_fd) = File::open(dir) {
let _ = nix::unistd::fsync(dir_fd.as_raw_fd());
let _ = nix::unistd::fsync(&dir_fd);
}
Ok(())
+50
View File
@@ -0,0 +1,50 @@
// This file is part of the shadow-rs package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
//! Audit logging for shadow-rs tools.
//!
//! On systems with `auditd` running, shadow-utils operations (password
//! changes, account creation/deletion, group changes) should be logged
//! to the audit subsystem.
//!
//! This module provides a best-effort logging interface that silently
//! succeeds when audit is not available.
/// Log a user account event to the audit subsystem.
///
/// `event_type` should be one of: `ADD_USER`, `DEL_USER`, `MOD_USER`,
/// `ADD_GROUP`, `DEL_GROUP`, `MOD_GROUP`, `CHNG_PASSWD`.
///
/// Silently succeeds if auditd is not running or audit tools are not
/// installed.
pub fn log_user_event(event_type: &str, username: &str, uid: u32, result: bool) {
let success = if result { "success" } else { "failed" };
let msg = format!(
"op={event_type} acct=\"{username}\" exe=\"shadow-rs\" \
hostname=? addr=? terminal=? res={success}"
);
// Use /sbin/auditctl or write to /dev/audit if available.
// For now, use logger as a fallback to syslog.
let _ = std::process::Command::new("/usr/bin/logger")
.arg("-t")
.arg("shadow-rs")
.arg("-p")
.arg("auth.info")
.arg(&msg)
.env_clear()
.env("PATH", "/usr/bin:/bin:/usr/sbin:/sbin")
.status();
// Also attempt to use ausearch-compatible format via audisp.
let _ = std::process::Command::new("/sbin/auditctl")
.arg("-m")
.arg(format!(
"shadow-rs: {event_type} user={username} uid={uid} res={success}"
))
.env_clear()
.env("PATH", "/usr/bin:/bin:/usr/sbin:/sbin")
.status();
}
+1
View File
@@ -41,6 +41,7 @@ pub mod crypt;
pub mod selinux;
pub mod atomic;
pub mod audit;
pub mod hardening;
pub mod lock;
pub mod nscd;
+1 -4
View File
@@ -289,8 +289,6 @@ mod tests {
#[test]
fn test_lock_file_has_cloexec() {
use std::os::unix::io::AsRawFd;
// Rust's stdlib sets O_CLOEXEC by default on Linux.
// Verify the lock file FD won't leak to child processes.
let dir = tempfile::tempdir().expect("tempdir creation failed");
@@ -300,9 +298,8 @@ mod tests {
let lock = FileLock::acquire(&file).expect("failed to acquire lock");
let f = fs::File::open(&lock.lock_path).expect("failed to open lock file");
let fd = f.as_raw_fd();
let flags =
nix::fcntl::fcntl(fd, nix::fcntl::FcntlArg::F_GETFD).expect("fcntl F_GETFD failed");
nix::fcntl::fcntl(&f, nix::fcntl::FcntlArg::F_GETFD).expect("fcntl F_GETFD failed");
assert!(flags & libc::FD_CLOEXEC != 0, "FD should have CLOEXEC set");
lock.release().expect("failed to release lock");
+15 -1
View File
@@ -12,17 +12,27 @@
use std::process::Command;
use crate::hardening;
/// Invalidate the `nscd` and `sssd` caches for the given database.
///
/// The `database` should be one of `"passwd"`, `"shadow"`, or `"group"`.
///
/// Silently succeeds if `nscd`/`sssd` is not installed or not running —
/// this matches GNU shadow-utils behavior.
///
/// Subprocesses are spawned with a sanitized environment to prevent the
/// caller's full (potentially tainted) env from leaking into child processes
/// running in a setuid context.
pub fn invalidate_cache(database: &str) {
let safe_env = hardening::sanitized_env();
// Use absolute paths to avoid PATH-based lookups in setuid context.
let _ = Command::new("/usr/sbin/nscd")
.arg("-i")
.arg(database)
.env_clear()
.envs(safe_env.iter().map(|(k, v)| (k, v)))
.status();
// sssd: sss_cache with the appropriate flag
@@ -31,5 +41,9 @@ pub fn invalidate_cache(database: &str) {
"group" => "-G",
_ => return,
};
let _ = Command::new("/usr/sbin/sss_cache").arg(flag).status();
let _ = Command::new("/usr/sbin/sss_cache")
.arg(flag)
.env_clear()
.envs(safe_env.iter().map(|(k, v)| (k, v)))
.status();
}
+83
View File
@@ -4,3 +4,86 @@
// file that was distributed with this source code.
//! `SELinux` security context handling for file operations.
//!
//! When `SELinux` is enforcing, newly created files must have the correct
//! security context. This module provides functions to get and set file
//! contexts during atomic file replacement.
//!
//! Feature-gated behind `selinux`. When disabled, all operations are no-ops.
use std::path::Path;
use crate::error::ShadowError;
/// Copy the `SELinux` security context from the source file to the destination.
///
/// Best-effort: silently succeeds if `SELinux` is not available, not enforcing,
/// or if context operations fail. This matches GNU shadow-utils behavior where
/// SELinux context handling is non-fatal.
pub fn copy_file_context(source: &Path, dest: &Path) -> Result<(), ShadowError> {
// Implementation requires libselinux FFI.
// For now, attempt to use the `setfilecon` command-line tool as a fallback.
let source_ctx = get_file_context(source);
if let Some(ctx) = source_ctx {
set_file_context(dest, &ctx)?;
}
Ok(())
}
/// Get the `SELinux` security context of a file.
///
/// Returns `None` if `SELinux` is not available or the file has no context.
fn get_file_context(path: &Path) -> Option<String> {
// Use the `getfattr` command to read the security.selinux xattr.
let output = std::process::Command::new("/usr/bin/getfattr")
.arg("--only-values")
.arg("-n")
.arg("security.selinux")
.arg(path)
.env_clear()
.env("PATH", "/usr/bin:/bin:/usr/sbin:/sbin")
.output()
.ok()?;
if output.status.success() {
let ctx = String::from_utf8_lossy(&output.stdout).trim().to_string();
if ctx.is_empty() { None } else { Some(ctx) }
} else {
None
}
}
/// Set the `SELinux` security context of a file.
fn set_file_context(path: &Path, context: &str) -> Result<(), ShadowError> {
let status = std::process::Command::new("/usr/bin/chcon")
.arg(context)
.arg(path)
.env_clear()
.env("PATH", "/usr/bin:/bin:/usr/sbin:/sbin")
.status();
match status {
Ok(s) if s.success() => Ok(()),
Ok(_) => {
// chcon failed — non-fatal, SELinux may not be enforcing.
Ok(())
}
Err(_) => {
// chcon not found — SELinux not available, silently succeed.
Ok(())
}
}
}
/// Restore the default `SELinux` context for a file based on policy.
///
/// Best-effort equivalent of `restorecon <path>`. Silently succeeds if
/// `SELinux` is not available or `restorecon` is not installed.
pub fn restore_default_context(path: &Path) -> Result<(), ShadowError> {
let _ = std::process::Command::new("/usr/sbin/restorecon")
.arg(path)
.env_clear()
.env("PATH", "/usr/bin:/bin:/usr/sbin:/sbin")
.status();
Ok(())
}
+68 -2
View File
@@ -177,8 +177,12 @@ fn parse_yyyy_mm_dd(input: &str) -> Result<i64, String> {
if !(1..=12).contains(&month) {
return Err(format!("invalid month {month} in '{input}'"));
}
if !(1..=31).contains(&day) {
return Err(format!("invalid day {day} in '{input}'"));
let max_day = days_in_month(year, month);
if day < 1 || day > max_day {
return Err(format!(
"invalid day {day} for {year}-{month:02} in '{input}'"
));
}
Ok(days_since_epoch(year, month, day))
@@ -199,6 +203,28 @@ fn days_since_epoch(year: i64, month: i64, day: i64) -> i64 {
era * 146_097 + doe - 719_468
}
/// Whether `year` is a leap year in the Gregorian calendar.
fn is_leap_year(year: i64) -> bool {
(year % 4 == 0 && year % 100 != 0) || year % 400 == 0
}
/// Number of days in a given month (1-indexed) for `year`.
fn days_in_month(year: i64, month: i64) -> i64 {
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 => {
if is_leap_year(year) {
29
} else {
28
}
}
// Month range is already validated before calling this function.
_ => 0,
}
}
/// Convert days since epoch back to (year, month, day).
///
/// Inverse of `days_since_epoch`, also from the Hinnant algorithms.
@@ -998,6 +1024,46 @@ mod tests {
// Exit code constants consistency
// -----------------------------------------------------------------------
#[test]
fn test_reject_feb_29_non_leap_year() {
assert!(
parse_yyyy_mm_dd("2025-02-29").is_err(),
"2025 is not a leap year, Feb 29 should be rejected"
);
}
#[test]
fn test_reject_feb_31() {
assert!(
parse_yyyy_mm_dd("2025-02-31").is_err(),
"February never has 31 days"
);
}
#[test]
fn test_accept_feb_29_leap_year() {
assert!(
parse_yyyy_mm_dd("2024-02-29").is_ok(),
"2024 is a leap year, Feb 29 should be accepted"
);
}
#[test]
fn test_reject_apr_31() {
assert!(
parse_yyyy_mm_dd("2025-04-31").is_err(),
"April has 30 days, day 31 should be rejected"
);
}
#[test]
fn test_accept_jan_31() {
assert!(
parse_yyyy_mm_dd("2025-01-31").is_ok(),
"January has 31 days, should be accepted"
);
}
#[test]
fn test_exit_code_constants() {
assert_eq!(exit_codes::SUCCESS, 0);
+3
View File
@@ -15,6 +15,7 @@ use clap::{Arg, ArgAction, Command};
use uucore::error::{UError, UResult};
use shadow_core::atomic;
use shadow_core::audit;
use shadow_core::group::{self, GroupEntry};
use shadow_core::gshadow::{self, GshadowEntry};
use shadow_core::lock::FileLock;
@@ -195,6 +196,8 @@ fn do_groupadd(matches: &clap::ArgMatches) -> UResult<()> {
nscd::invalidate_cache("group");
audit::log_user_event("ADD_GROUP", &group_name, gid, true);
Ok(())
}
+3
View File
@@ -15,6 +15,7 @@ use clap::{Arg, Command};
use uucore::error::{UError, UResult};
use shadow_core::atomic;
use shadow_core::audit;
use shadow_core::group::{self, GroupEntry};
use shadow_core::gshadow::{self, GshadowEntry};
use shadow_core::lock::FileLock;
@@ -193,6 +194,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
nscd::invalidate_cache("group");
audit::log_user_event("DEL_GROUP", group_name, target_gid, true);
Ok(())
}
+5
View File
@@ -15,6 +15,7 @@ use clap::{Arg, ArgAction, Command};
use uucore::error::{UError, UResult};
use shadow_core::atomic;
use shadow_core::audit;
use shadow_core::group::{self};
use shadow_core::gshadow::{self};
use shadow_core::lock::FileLock;
@@ -184,6 +185,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
entries[idx].name.clone_from(name);
}
let modified_gid = entries[idx].gid;
// Write /etc/group.
atomic::atomic_write(&group_path, |f| group::write_group(&entries, f)).map_err(|e| {
GroupmodError::CantUpdate(format!("cannot write {}: {e}", group_path.display()))
@@ -220,6 +223,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
nscd::invalidate_cache("group");
audit::log_user_event("MOD_GROUP", group_name, modified_gid, true);
Ok(())
}
+15
View File
@@ -13,6 +13,7 @@ use std::path::Path;
use clap::{Arg, ArgAction, Command};
use shadow_core::audit;
use shadow_core::lock::FileLock;
use shadow_core::shadow::{self, ShadowEntry};
use shadow_core::sysroot::SysRoot;
@@ -562,6 +563,13 @@ fn cmd_pam_change(matches: &clap::ArgMatches, _target_user: &str) -> UResult<()>
return Err(PasswdError::PamError(e.to_string()).into());
}
audit::log_user_event(
"CHNG_PASSWD",
_target_user,
nix::unistd::getuid().as_raw(),
true,
);
Ok(())
}
@@ -811,6 +819,13 @@ where
drop(lock);
nscd::invalidate_cache("shadow");
audit::log_user_event(
"CHNG_PASSWD",
username,
nix::unistd::getuid().as_raw(),
true,
);
if !quiet {
uucore::show_error!("{action} for user {username}");
}
+79 -3
View File
@@ -20,6 +20,7 @@ use std::path::Path;
use clap::{Arg, ArgAction, Command};
use shadow_core::atomic;
use shadow_core::audit;
use shadow_core::group::{self, GroupEntry};
use shadow_core::gshadow::{self, GshadowEntry};
use shadow_core::lock::FileLock;
@@ -476,6 +477,7 @@ fn parse_options(matches: &clap::ArgMatches) -> Result<UseraddOptions, UseraddEr
// ---------------------------------------------------------------------------
/// Execute the useradd operation.
#[allow(clippy::too_many_lines)]
fn do_useradd(opts: &UseraddOptions) -> UResult<()> {
// Step 1: Validate username.
validate::validate_username(&opts.login)
@@ -592,22 +594,50 @@ fn do_useradd(opts: &UseraddOptions) -> UResult<()> {
};
write_shadow_entry(&shadow_path, &shadow_entry)?;
// Step 14: Add to supplementary groups.
// Step 14: Allocate subordinate UID/GID ranges for rootless containers.
// Only done when the relevant file exists (matching GNU shadow-utils behavior).
let subuid_path = opts.root.subuid_path();
if subuid_path.exists()
&& let Err(e) = append_subid_entry(
&subuid_path,
&opts.login,
100_000 + u64::from(uid) * 65_536,
65_536,
)
{
uucore::show_error!("warning: failed to add subordinate UID range: {e}");
}
let subgid_path = opts.root.subgid_path();
if subgid_path.exists()
&& let Err(e) = append_subid_entry(
&subgid_path,
&opts.login,
100_000 + u64::from(gid) * 65_536,
65_536,
)
{
uucore::show_error!("warning: failed to add subordinate GID range: {e}");
}
// Step 15: Add to supplementary groups.
if !opts.groups.is_empty() {
add_to_supplementary_groups(opts, &group_path, &gshadow_path)?;
}
// Step 15: Create home directory and copy skel.
// Step 16: Create home directory and copy skel.
if opts.create_home {
let resolved_home = opts.root.resolve(&home_dir);
let resolved_skel = opts.root.resolve(&opts.skel_dir);
create_home_directory(&resolved_home, &resolved_skel, uid, gid)?;
}
// Step 16: Invalidate nscd caches.
// Step 17: Invalidate nscd caches.
nscd::invalidate_cache("passwd");
nscd::invalidate_cache("group");
// Step 18: Audit log.
audit::log_user_event("ADD_USER", &opts.login, uid, true);
Ok(())
}
@@ -831,6 +861,52 @@ fn add_to_supplementary_groups(
Ok(())
}
// ---------------------------------------------------------------------------
// Subordinate ID allocation
// ---------------------------------------------------------------------------
/// Append a subordinate ID entry to a subuid/subgid file.
///
/// Skips the write if the user already has an entry in the file.
/// Uses file locking and atomic writes for crash safety.
fn append_subid_entry(path: &Path, name: &str, start: u64, count: u64) -> UResult<()> {
use shadow_core::subid::{self, SubIdEntry};
let lock = FileLock::acquire(path).map_err(|e| {
UseraddError::CannotUpdatePasswd(format!("cannot lock {}: {e}", path.display()))
})?;
let mut entries = match subid::read_subid_file(path) {
Ok(e) => e,
Err(e) => {
uucore::show_error!("warning: cannot read {}: {e}", path.display());
return Err(UseraddError::CannotUpdatePasswd(format!(
"cannot read {}: {e}",
path.display()
))
.into());
}
};
// Don't add a duplicate entry.
if entries.iter().any(|e| e.name == name) {
drop(lock);
return Ok(());
}
entries.push(SubIdEntry {
name: name.to_string(),
start,
count,
});
atomic::atomic_write(path, |f| subid::write_subid(&entries, f))
.map_err(|e| UseraddError::CannotUpdatePasswd(format!("{e}")))?;
drop(lock);
Ok(())
}
// ---------------------------------------------------------------------------
// Home directory creation
// ---------------------------------------------------------------------------
+12 -4
View File
@@ -14,6 +14,7 @@ use std::path::Path;
use clap::{Arg, ArgAction, Command};
use uucore::error::{UError, UResult};
use shadow_core::audit;
use shadow_core::group::{self};
use shadow_core::gshadow::{self};
use shadow_core::lock::FileLock;
@@ -97,12 +98,17 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
return Err(UserdelError::CantUpdatePasswd("Permission denied.".into()).into());
}
// Read the user's home directory from /etc/passwd BEFORE removing the entry.
// Read the user's home directory and UID from /etc/passwd BEFORE removing
// the entry (needed for home removal and audit logging).
let passwd_path = root.passwd_path();
let pre_entries = passwd::read_passwd_file(&passwd_path)
.map_err(|e| UserdelError::CantUpdatePasswd(format!("cannot read passwd: {e}")))?;
let saved_uid = pre_entries
.iter()
.find(|e| e.name == *login)
.map_or(0, |e| e.uid);
let saved_home = if remove_home {
let entries = passwd::read_passwd_file(&passwd_path)
.map_err(|e| UserdelError::CantUpdatePasswd(format!("cannot read passwd: {e}")))?;
entries
pre_entries
.iter()
.find(|e| e.name == *login)
.map(|e| e.home.clone())
@@ -151,6 +157,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
nscd::invalidate_cache("passwd");
nscd::invalidate_cache("group");
audit::log_user_event("DEL_USER", login, saved_uid, true);
Ok(())
}
+50 -2
View File
@@ -14,6 +14,7 @@ use std::path::Path;
use clap::{Arg, ArgAction, Command};
use uucore::error::{UError, UResult};
use shadow_core::audit;
use shadow_core::group::{self};
use shadow_core::lock::FileLock;
use shadow_core::passwd::{self};
@@ -151,11 +152,13 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
drop(lock);
// If the UID changed and the home directory was not explicitly moved,
// chown the existing home directory to the new UID.
// recursively chown the existing home directory to the new UID.
// Only files owned by old_uid are touched (files owned by other users
// are left alone, matching GNU shadow-utils behavior).
if new_uid != old_uid && !home_is_changing && !home_for_chown.is_empty() {
let home_path = root.resolve(&home_for_chown);
if home_path.exists() {
let _ = nix::unistd::chown(&home_path, Some(nix::unistd::Uid::from_raw(new_uid)), None);
recursive_chown(&home_path, old_uid, new_uid);
}
}
@@ -278,9 +281,54 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
nscd::invalidate_cache("passwd");
nscd::invalidate_cache("group");
audit::log_user_event("MOD_USER", login, new_uid, true);
Ok(())
}
/// Recursively chown all files and directories under `path` that are owned by
/// `old_uid` to `new_uid`. Files owned by other users are left untouched.
///
/// Uses `fchownat` with `AT_SYMLINK_NOFOLLOW` so symlinks themselves are
/// re-owned without following them.
fn recursive_chown(path: &Path, old_uid: u32, new_uid: u32) {
use nix::fcntl::AtFlags;
use std::os::unix::fs::MetadataExt;
if let Ok(entries) = std::fs::read_dir(path) {
for entry in entries.flatten() {
let entry_path = entry.path();
if let Ok(meta) = std::fs::symlink_metadata(&entry_path) {
if meta.uid() == old_uid {
let _ = nix::unistd::fchownat(
nix::fcntl::AT_FDCWD,
&entry_path,
Some(nix::unistd::Uid::from_raw(new_uid)),
None,
AtFlags::AT_SYMLINK_NOFOLLOW,
);
}
if meta.is_dir() {
recursive_chown(&entry_path, old_uid, new_uid);
}
}
}
}
// Also chown the directory itself.
if let Ok(meta) = std::fs::symlink_metadata(path)
&& meta.uid() == old_uid
{
let _ = nix::unistd::fchownat(
nix::fcntl::AT_FDCWD,
path,
Some(nix::unistd::Uid::from_raw(new_uid)),
None,
AtFlags::AT_SYMLINK_NOFOLLOW,
);
}
}
#[must_use]
#[allow(clippy::too_many_lines)]
pub fn uu_app() -> Command {

Some files were not shown because too many files have changed in this diff Show More