From deaaf2aaef0c0f9b00fb316757abc9a60d4c8422 Mon Sep 17 00:00:00 2001 From: Zhang Wen Date: Mon, 28 Apr 2025 11:29:13 +0800 Subject: [PATCH 1/2] treat invalid user or group as uid or gid if it's an integer --- src/uu/install/src/install.rs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/uu/install/src/install.rs b/src/uu/install/src/install.rs index e128470fcc8..3d66d66ac98 100644 --- a/src/uu/install/src/install.rs +++ b/src/uu/install/src/install.rs @@ -405,7 +405,14 @@ fn behavior(matches: &ArgMatches) -> UResult { } else { match usr2uid(&owner) { Ok(u) => Some(u), - Err(_) => return Err(InstallError::InvalidUser(owner.clone()).into()), + // When using -o500 option and there's no user with uid 500 on the system + // usr2uid returns an Err value and the whole install operation fails. + // GNU coreutils installs a file with uid 500 in the same situation + // so just return the supplied owner as uid if it's an integer value + Err(_) => match owner.parse::() { + Ok(u) => Some(u), + Err(_) => return Err(InstallError::InvalidUser(owner.clone()).into()), + }, } }; @@ -419,7 +426,14 @@ fn behavior(matches: &ArgMatches) -> UResult { } else { match grp2gid(&group) { Ok(g) => Some(g), - Err(_) => return Err(InstallError::InvalidGroup(group.clone()).into()), + // When using -g500 option and there's no group with gid 500 on the system + // grp2gid returns an Err value and the whole install operation fails. + // GNU coreutils installs a file with gid 500 in the same situation + // so just return the supplied group as gid if it's an integer value + Err(_) => match group.parse::() { + Ok(g) => Some(g), + Err(_) => return Err(InstallError::InvalidGroup(group.clone()).into()), + }, } }; From 8a043915c6fb1eccd24f4ec92396f7364c880f71 Mon Sep 17 00:00:00 2001 From: Zhang Wen Date: Mon, 28 Apr 2025 11:30:39 +0800 Subject: [PATCH 2/2] install: add test for install with invalid user or group --- tests/by-util/test_install.rs | 86 +++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/tests/by-util/test_install.rs b/tests/by-util/test_install.rs index 7a2ccb87595..1bcc20ea11e 100644 --- a/tests/by-util/test_install.rs +++ b/tests/by-util/test_install.rs @@ -8,6 +8,10 @@ use filetime::FileTime; use std::fs; #[cfg(target_os = "linux")] +use std::fs::File; +#[cfg(target_os = "linux")] +use std::io::{BufRead, BufReader}; +#[cfg(target_os = "linux")] use std::os::unix::ffi::OsStringExt; use std::os::unix::fs::{MetadataExt, PermissionsExt}; #[cfg(not(windows))] @@ -2567,3 +2571,85 @@ fn test_install_normal_file_replaces_symlink() { // Verify sensitive file was NOT modified assert_eq!(at.read("sensitive"), "important data"); } +#[test] +#[cfg(target_os = "linux")] +fn test_install_set_owner_nonexistent_uid_and_gid() { + let file = File::open("/etc/login.defs").unwrap(); + let reader = BufReader::new(file); + let mut uid_min: u32 = 0; + let mut uid_max: u32 = 0; + let mut gid_min: u32 = 0; + let mut gid_max: u32 = 0; + for line in reader.lines() { + let line = line.unwrap(); + if line.starts_with("UID_MIN") { + let tokens: Vec<&str> = line.split_whitespace().collect(); + uid_min = tokens[1].parse().unwrap(); + } + if line.starts_with("UID_MAX") { + let tokens: Vec<&str> = line.split_whitespace().collect(); + uid_max = tokens[1].parse().unwrap(); + } + if line.starts_with("GID_MIN") { + let tokens: Vec<&str> = line.split_whitespace().collect(); + gid_min = tokens[1].parse().unwrap(); + } + if line.starts_with("GID_MAX") { + let tokens: Vec<&str> = line.split_whitespace().collect(); + gid_max = tokens[1].parse().unwrap(); + } + } + let file = File::open("/etc/passwd").unwrap(); + let reader = BufReader::new(file); + + let mut uids: Vec = vec![]; + let mut gids: Vec = vec![]; + for line in reader.lines() { + let line = line.unwrap(); + let tokens: Vec<&str> = line.split(':').collect(); + let uid: u32 = tokens[2].parse().unwrap(); + if (uid_min..=uid_max).contains(&uid) { + uids.push(uid); + } + let gid: u32 = tokens[3].parse().unwrap(); + if (gid_min..=gid_max).contains(&gid) { + gids.push(gid); + } + } + uids.sort_unstable(); + + let next_uid = if let Some(uid) = uids.last() { + *uid + 1 + } else { + uid_min + }; + + let next_gid = if let Some(gid) = gids.last() { + *gid + 1 + } else { + gid_min + }; + + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + at.touch("a"); + + if let Ok(result) = run_ucmd_as_root( + &ts, + &[ + format!("-o{next_uid}").as_str(), + format!("-g{next_gid}").as_str(), + "a", + "b", + ], + ) { + result.success(); + assert!(at.file_exists("b")); + + let metadata = fs::metadata(at.plus("b")).unwrap(); + assert_eq!(metadata.uid(), next_uid); + assert_eq!(metadata.gid(), next_gid); + } else { + println!("Test skipped; requires root user"); + } +}