1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-28 11:37:44 +00:00

uucore::entries: Make Passwd::locate and Group::locate thread-safe

This commit is contained in:
Jan Verbeek 2021-08-27 14:21:33 +02:00
parent fe286fa8c8
commit f2ddae93fa
6 changed files with 131 additions and 145 deletions

View file

@ -182,6 +182,7 @@ getgrgid
getgrnam getgrnam
getgrouplist getgrouplist
getgroups getgroups
getpwent
getpwnam getpwnam
getpwuid getpwuid
getuid getuid

View file

@ -183,7 +183,7 @@ fn parse_spec(spec: &str, sep: char) -> UResult<(Option<u32>, Option<u32>)> {
let uid = if !user.is_empty() { let uid = if !user.is_empty() {
Some(match Passwd::locate(user) { Some(match Passwd::locate(user) {
Ok(u) => u.uid(), // We have been able to get the uid Ok(u) => u.uid, // We have been able to get the uid
Err(_) => Err(_) =>
// we have NOT been able to find the uid // we have NOT been able to find the uid
// but we could be in the case where we have user.group // but we could be in the case where we have user.group
@ -208,7 +208,7 @@ fn parse_spec(spec: &str, sep: char) -> UResult<(Option<u32>, Option<u32>)> {
Some( Some(
Group::locate(group) Group::locate(group)
.map_err(|_| USimpleError::new(1, format!("invalid group: {}", spec.quote())))? .map_err(|_| USimpleError::new(1, format!("invalid group: {}", spec.quote())))?
.gid(), .gid,
) )
} else { } else {
None None

View file

@ -245,7 +245,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
// GNU's `id` does not support the flags: -p/-P/-A. // GNU's `id` does not support the flags: -p/-P/-A.
if matches.is_present(options::OPT_PASSWORD) { if matches.is_present(options::OPT_PASSWORD) {
// BSD's `id` ignores all but the first specified user // BSD's `id` ignores all but the first specified user
pline(possible_pw.map(|v| v.uid())); pline(possible_pw.as_ref().map(|v| v.uid));
return Ok(()); return Ok(());
}; };
if matches.is_present(options::OPT_HUMAN_READABLE) { if matches.is_present(options::OPT_HUMAN_READABLE) {
@ -259,7 +259,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
return Ok(()); return Ok(());
} }
let (uid, gid) = possible_pw.map(|p| (p.uid(), p.gid())).unwrap_or(( let (uid, gid) = possible_pw.as_ref().map(|p| (p.uid, p.gid)).unwrap_or((
if state.rflag { getuid() } else { geteuid() }, if state.rflag { getuid() } else { geteuid() },
if state.rflag { getgid() } else { getegid() }, if state.rflag { getgid() } else { getegid() },
)); ));
@ -302,7 +302,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let groups = entries::get_groups_gnu(Some(gid)).unwrap(); let groups = entries::get_groups_gnu(Some(gid)).unwrap();
let groups = if state.user_specified { let groups = if state.user_specified {
possible_pw.map(|p| p.belongs_to()).unwrap() possible_pw.as_ref().map(|p| p.belongs_to()).unwrap()
} else { } else {
groups.clone() groups.clone()
}; };
@ -453,7 +453,7 @@ pub fn uu_app() -> App<'static, 'static> {
fn pretty(possible_pw: Option<Passwd>) { fn pretty(possible_pw: Option<Passwd>) {
if let Some(p) = possible_pw { if let Some(p) = possible_pw {
print!("uid\t{}\ngroups\t", p.name()); print!("uid\t{}\ngroups\t", p.name);
println!( println!(
"{}", "{}",
p.belongs_to() p.belongs_to()
@ -466,10 +466,10 @@ fn pretty(possible_pw: Option<Passwd>) {
let login = cstr2cow!(getlogin() as *const _); let login = cstr2cow!(getlogin() as *const _);
let rid = getuid(); let rid = getuid();
if let Ok(p) = Passwd::locate(rid) { if let Ok(p) = Passwd::locate(rid) {
if login == p.name() { if login == p.name {
println!("login\t{}", login); println!("login\t{}", login);
} }
println!("uid\t{}", p.name()); println!("uid\t{}", p.name);
} else { } else {
println!("uid\t{}", rid); println!("uid\t{}", rid);
} }
@ -477,7 +477,7 @@ fn pretty(possible_pw: Option<Passwd>) {
let eid = getegid(); let eid = getegid();
if eid == rid { if eid == rid {
if let Ok(p) = Passwd::locate(eid) { if let Ok(p) = Passwd::locate(eid) {
println!("euid\t{}", p.name()); println!("euid\t{}", p.name);
} else { } else {
println!("euid\t{}", eid); println!("euid\t{}", eid);
} }
@ -486,7 +486,7 @@ fn pretty(possible_pw: Option<Passwd>) {
let rid = getgid(); let rid = getgid();
if rid != eid { if rid != eid {
if let Ok(g) = Group::locate(rid) { if let Ok(g) = Group::locate(rid) {
println!("euid\t{}", g.name()); println!("euid\t{}", g.name);
} else { } else {
println!("euid\t{}", rid); println!("euid\t{}", rid);
} }
@ -511,16 +511,16 @@ fn pline(possible_uid: Option<uid_t>) {
println!( println!(
"{}:{}:{}:{}:{}:{}:{}:{}:{}:{}", "{}:{}:{}:{}:{}:{}:{}:{}:{}:{}",
pw.name(), pw.name,
pw.user_passwd(), pw.user_passwd,
pw.uid(), pw.uid,
pw.gid(), pw.gid,
pw.user_access_class(), pw.user_access_class,
pw.passwd_change_time(), pw.passwd_change_time,
pw.expiration(), pw.expiration,
pw.user_info(), pw.user_info,
pw.user_dir(), pw.user_dir,
pw.user_shell() pw.user_shell
); );
} }
@ -531,13 +531,7 @@ fn pline(possible_uid: Option<uid_t>) {
println!( println!(
"{}:{}:{}:{}:{}:{}:{}", "{}:{}:{}:{}:{}:{}:{}",
pw.name(), pw.name, pw.user_passwd, pw.uid, pw.gid, pw.user_info, pw.user_dir, pw.user_shell
pw.user_passwd(),
pw.uid(),
pw.gid(),
pw.user_info(),
pw.user_dir(),
pw.user_shell()
); );
} }

View file

@ -267,11 +267,11 @@ impl Pinky {
if self.include_fullname { if self.include_fullname {
if let Ok(pw) = Passwd::locate(ut.user().as_ref()) { if let Ok(pw) = Passwd::locate(ut.user().as_ref()) {
let mut gecos = pw.user_info().into_owned(); let mut gecos = pw.user_info;
if let Some(n) = gecos.find(',') { if let Some(n) = gecos.find(',') {
gecos.truncate(n + 1); gecos.truncate(n + 1);
} }
print!(" {:<19.19}", gecos.replace("&", &pw.name().capitalize())); print!(" {:<19.19}", gecos.replace("&", &pw.name.capitalize()));
} else { } else {
print!(" {:19}", " ???"); print!(" {:19}", " ???");
} }
@ -333,13 +333,13 @@ impl Pinky {
for u in &self.names { for u in &self.names {
print!("Login name: {:<28}In real life: ", u); print!("Login name: {:<28}In real life: ", u);
if let Ok(pw) = Passwd::locate(u.as_str()) { if let Ok(pw) = Passwd::locate(u.as_str()) {
println!(" {}", pw.user_info().replace("&", &pw.name().capitalize())); println!(" {}", pw.user_info.replace("&", &pw.name.capitalize()));
if self.include_home_and_shell { if self.include_home_and_shell {
print!("Directory: {:<29}", pw.user_dir()); print!("Directory: {:<29}", pw.user_dir);
println!("Shell: {}", pw.user_shell()); println!("Shell: {}", pw.user_shell);
} }
if self.include_project { if self.include_project {
let mut p = PathBuf::from(pw.user_dir().as_ref()); let mut p = PathBuf::from(&pw.user_dir);
p.push(".project"); p.push(".project");
if let Ok(f) = File::open(p) { if let Ok(f) = File::open(p) {
print!("Project: "); print!("Project: ");
@ -347,7 +347,7 @@ impl Pinky {
} }
} }
if self.include_plan { if self.include_plan {
let mut p = PathBuf::from(pw.user_dir().as_ref()); let mut p = PathBuf::from(&pw.user_dir);
p.push(".plan"); p.push(".plan");
if let Ok(f) = File::open(p) { if let Ok(f) = File::open(p) {
println!("Plan:"); println!("Plan:");

View file

@ -41,12 +41,14 @@ use libc::{c_char, c_int, gid_t, uid_t};
use libc::{getgrgid, getgrnam, getgroups}; use libc::{getgrgid, getgrnam, getgroups};
use libc::{getpwnam, getpwuid, group, passwd}; use libc::{getpwnam, getpwuid, group, passwd};
use std::borrow::Cow;
use std::ffi::{CStr, CString}; use std::ffi::{CStr, CString};
use std::io::Error as IOError; use std::io::Error as IOError;
use std::io::ErrorKind; use std::io::ErrorKind;
use std::io::Result as IOResult; use std::io::Result as IOResult;
use std::ptr; use std::ptr;
use std::sync::Mutex;
use once_cell::sync::Lazy;
extern "C" { extern "C" {
/// From: https://man7.org/linux/man-pages/man3/getgrouplist.3.html /// From: https://man7.org/linux/man-pages/man3/getgrouplist.3.html
@ -124,77 +126,57 @@ fn sort_groups(mut groups: Vec<gid_t>, egid: gid_t) -> Vec<gid_t> {
groups groups
} }
#[derive(Copy, Clone)] #[derive(Clone, Debug)]
pub struct Passwd { pub struct Passwd {
inner: passwd, /// AKA passwd.pw_name
pub name: String,
/// AKA passwd.pw_uid
pub uid: uid_t,
/// AKA passwd.pw_gid
pub gid: gid_t,
/// AKA passwd.pw_gecos
pub user_info: String,
/// AKA passwd.pw_shell
pub user_shell: String,
/// AKA passwd.pw_dir
pub user_dir: String,
/// AKA passwd.pw_passwd
pub user_passwd: String,
/// AKA passwd.pw_class
#[cfg(any(target_os = "freebsd", target_vendor = "apple"))]
pub user_access_class: String,
/// AKA passwd.pw_change
#[cfg(any(target_os = "freebsd", target_vendor = "apple"))]
pub passwd_change_time: time_t,
/// AKA passwd.pw_expire
#[cfg(any(target_os = "freebsd", target_vendor = "apple"))]
pub expiration: time_t,
} }
macro_rules! cstr2cow { /// SAFETY: ptr must point to a valid C string.
($v:expr) => { unsafe fn cstr2string(ptr: *const c_char) -> String {
unsafe { CStr::from_ptr($v).to_string_lossy() } CStr::from_ptr(ptr).to_string_lossy().into_owned()
};
} }
impl Passwd { impl Passwd {
/// AKA passwd.pw_name /// SAFETY: All the pointed-to strings must be valid and not change while
pub fn name(&self) -> Cow<str> { /// the function runs. That means PW_LOCK must be held.
cstr2cow!(self.inner.pw_name) unsafe fn from_raw(raw: passwd) -> Self {
} Passwd {
name: cstr2string(raw.pw_name),
/// AKA passwd.pw_uid uid: raw.pw_uid,
pub fn uid(&self) -> uid_t { gid: raw.pw_gid,
self.inner.pw_uid user_info: cstr2string(raw.pw_gecos),
} user_shell: cstr2string(raw.pw_shell),
user_dir: cstr2string(raw.pw_dir),
/// AKA passwd.pw_gid user_passwd: cstr2string(raw.pw_passwd),
pub fn gid(&self) -> gid_t { #[cfg(any(target_os = "freebsd", target_vendor = "apple"))]
self.inner.pw_gid user_access_class: cstr2string(raw.pw_class),
} #[cfg(any(target_os = "freebsd", target_vendor = "apple"))]
passwd_change_time: raw.pw_change,
/// AKA passwd.pw_gecos #[cfg(any(target_os = "freebsd", target_vendor = "apple"))]
pub fn user_info(&self) -> Cow<str> { expiration: raw.pw_expire,
cstr2cow!(self.inner.pw_gecos) }
}
/// AKA passwd.pw_shell
pub fn user_shell(&self) -> Cow<str> {
cstr2cow!(self.inner.pw_shell)
}
/// AKA passwd.pw_dir
pub fn user_dir(&self) -> Cow<str> {
cstr2cow!(self.inner.pw_dir)
}
/// AKA passwd.pw_passwd
pub fn user_passwd(&self) -> Cow<str> {
cstr2cow!(self.inner.pw_passwd)
}
/// AKA passwd.pw_class
#[cfg(any(target_os = "freebsd", target_vendor = "apple"))]
pub fn user_access_class(&self) -> Cow<str> {
cstr2cow!(self.inner.pw_class)
}
/// AKA passwd.pw_change
#[cfg(any(target_os = "freebsd", target_vendor = "apple"))]
pub fn passwd_change_time(&self) -> time_t {
self.inner.pw_change
}
/// AKA passwd.pw_expire
#[cfg(any(target_os = "freebsd", target_vendor = "apple"))]
pub fn expiration(&self) -> time_t {
self.inner.pw_expire
}
pub fn as_inner(&self) -> &passwd {
&self.inner
}
pub fn into_inner(self) -> passwd {
self.inner
} }
/// This is a wrapper function for `libc::getgrouplist`. /// This is a wrapper function for `libc::getgrouplist`.
@ -215,11 +197,12 @@ impl Passwd {
let mut ngroups: c_int = 8; let mut ngroups: c_int = 8;
let mut ngroups_old: c_int; let mut ngroups_old: c_int;
let mut groups = Vec::with_capacity(ngroups as usize); let mut groups = Vec::with_capacity(ngroups as usize);
let gid = self.inner.pw_gid; let name = CString::new(self.name.clone()).unwrap();
let name = self.inner.pw_name;
loop { loop {
ngroups_old = ngroups; ngroups_old = ngroups;
if unsafe { getgrouplist(name, gid, groups.as_mut_ptr(), &mut ngroups) } == -1 { if unsafe { getgrouplist(name.as_ptr(), self.gid, groups.as_mut_ptr(), &mut ngroups) }
== -1
{
if ngroups == ngroups_old { if ngroups == ngroups_old {
ngroups *= 2; ngroups *= 2;
} }
@ -236,27 +219,22 @@ impl Passwd {
} }
} }
#[derive(Clone, Debug)]
pub struct Group { pub struct Group {
inner: group, /// AKA group.gr_name
pub name: String,
/// AKA group.gr_gid
pub gid: gid_t,
} }
impl Group { impl Group {
/// AKA group.gr_name /// SAFETY: gr_name must be valid and not change while
pub fn name(&self) -> Cow<str> { /// the function runs. That means PW_LOCK must be held.
cstr2cow!(self.inner.gr_name) unsafe fn from_raw(raw: group) -> Self {
} Group {
name: cstr2string(raw.gr_name),
/// AKA group.gr_gid gid: raw.gr_gid,
pub fn gid(&self) -> gid_t { }
self.inner.gr_gid
}
pub fn as_inner(&self) -> &group {
&self.inner
}
pub fn into_inner(self) -> group {
self.inner
} }
} }
@ -267,17 +245,32 @@ pub trait Locate<K> {
Self: ::std::marker::Sized; Self: ::std::marker::Sized;
} }
// These functions are not thread-safe:
// > The return value may point to a static area, and may be
// > overwritten by subsequent calls to getpwent(3), getpwnam(),
// > or getpwuid().
// This applies not just to the struct but also the strings it points
// to, so we must copy all the data we want before releasing the lock.
// (Technically we must also ensure that the raw functions aren't being called
// anywhere else in the program.)
static PW_LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
macro_rules! f { macro_rules! f {
($fnam:ident, $fid:ident, $t:ident, $st:ident) => { ($fnam:ident, $fid:ident, $t:ident, $st:ident) => {
impl Locate<$t> for $st { impl Locate<$t> for $st {
fn locate(k: $t) -> IOResult<Self> { fn locate(k: $t) -> IOResult<Self> {
let _guard = PW_LOCK.lock();
// SAFETY: We're holding PW_LOCK.
unsafe { unsafe {
let data = $fid(k); let data = $fid(k);
if !data.is_null() { if !data.is_null() {
Ok($st { Ok($st::from_raw(ptr::read(data as *const _)))
inner: ptr::read(data as *const _),
})
} else { } else {
// FIXME: Resource limits, signals and I/O failure may
// cause this too. See getpwnam(3).
// errno must be set to zero before the call. We can
// use libc::__errno_location() on some platforms.
// The same applies for the two cases below.
Err(IOError::new( Err(IOError::new(
ErrorKind::NotFound, ErrorKind::NotFound,
format!("No such id: {}", k), format!("No such id: {}", k),
@ -289,25 +282,26 @@ macro_rules! f {
impl<'a> Locate<&'a str> for $st { impl<'a> Locate<&'a str> for $st {
fn locate(k: &'a str) -> IOResult<Self> { fn locate(k: &'a str) -> IOResult<Self> {
let _guard = PW_LOCK.lock();
if let Ok(id) = k.parse::<$t>() { if let Ok(id) = k.parse::<$t>() {
let data = unsafe { $fid(id) }; // SAFETY: We're holding PW_LOCK.
if !data.is_null() { unsafe {
Ok($st { let data = $fid(id);
inner: unsafe { ptr::read(data as *const _) }, if !data.is_null() {
}) Ok($st::from_raw(ptr::read(data as *const _)))
} else { } else {
Err(IOError::new( Err(IOError::new(
ErrorKind::NotFound, ErrorKind::NotFound,
format!("No such id: {}", id), format!("No such id: {}", id),
)) ))
}
} }
} else { } else {
// SAFETY: We're holding PW_LOCK.
unsafe { unsafe {
let data = $fnam(CString::new(k).unwrap().as_ptr()); let data = $fnam(CString::new(k).unwrap().as_ptr());
if !data.is_null() { if !data.is_null() {
Ok($st { Ok($st::from_raw(ptr::read(data as *const _)))
inner: ptr::read(data as *const _),
})
} else { } else {
Err(IOError::new( Err(IOError::new(
ErrorKind::NotFound, ErrorKind::NotFound,
@ -327,24 +321,24 @@ f!(getgrnam, getgrgid, gid_t, Group);
#[inline] #[inline]
pub fn uid2usr(id: uid_t) -> IOResult<String> { pub fn uid2usr(id: uid_t) -> IOResult<String> {
Passwd::locate(id).map(|p| p.name().into_owned()) Passwd::locate(id).map(|p| p.name)
} }
#[cfg(not(target_os = "redox"))] #[cfg(not(target_os = "redox"))]
#[inline] #[inline]
pub fn gid2grp(id: gid_t) -> IOResult<String> { pub fn gid2grp(id: gid_t) -> IOResult<String> {
Group::locate(id).map(|p| p.name().into_owned()) Group::locate(id).map(|p| p.name)
} }
#[inline] #[inline]
pub fn usr2uid(name: &str) -> IOResult<uid_t> { pub fn usr2uid(name: &str) -> IOResult<uid_t> {
Passwd::locate(name).map(|p| p.uid()) Passwd::locate(name).map(|p| p.uid)
} }
#[cfg(not(target_os = "redox"))] #[cfg(not(target_os = "redox"))]
#[inline] #[inline]
pub fn grp2gid(name: &str) -> IOResult<gid_t> { pub fn grp2gid(name: &str) -> IOResult<gid_t> {
Group::locate(name).map(|p| p.gid()) Group::locate(name).map(|p| p.gid)
} }
#[cfg(test)] #[cfg(test)]

View file

@ -24,14 +24,11 @@ fn test_capitalize() {
fn test_long_format() { fn test_long_format() {
let login = "root"; let login = "root";
let pw: Passwd = Passwd::locate(login).unwrap(); let pw: Passwd = Passwd::locate(login).unwrap();
let real_name = pw.user_info().replace("&", &pw.name().capitalize()); let real_name = pw.user_info.replace("&", &pw.name.capitalize());
let ts = TestScenario::new(util_name!()); let ts = TestScenario::new(util_name!());
ts.ucmd().arg("-l").arg(login).succeeds().stdout_is(format!( ts.ucmd().arg("-l").arg(login).succeeds().stdout_is(format!(
"Login name: {:<28}In real life: {}\nDirectory: {:<29}Shell: {}\n\n", "Login name: {:<28}In real life: {}\nDirectory: {:<29}Shell: {}\n\n",
login, login, real_name, pw.user_dir, pw.user_shell
real_name,
pw.user_dir(),
pw.user_shell()
)); ));
ts.ucmd() ts.ucmd()