Skip to content

Commit c1871ba

Browse files
authored
Better handling when /etc/group misses an entry for the primary group of the user (#950)
2 parents 6461b9b + 994da7e commit c1871ba

File tree

6 files changed

+52
-16
lines changed

6 files changed

+52
-16
lines changed

src/common/resolve.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ pub(crate) fn resolve_target_user_and_group(
112112
// when -u is specified but -g is not specified, default -g to the primary group of the specified user
113113
(Some(_), None) => {
114114
if let Some(user) = &target_user {
115-
target_group = Group::from_gid(user.gid)?;
115+
target_group = Some(user.primary_group()?);
116116
}
117117
}
118118
// when no -u or -g is specified, default to root:root
@@ -279,7 +279,7 @@ mod tests {
279279
// fallback to root
280280
let (user, group) = resolve_target_user_and_group(&None, &None, &current_user).unwrap();
281281
assert_eq!(user.name, "root");
282-
assert_eq!(group.name, ROOT_GROUP_NAME);
282+
assert_eq!(group.name.unwrap(), ROOT_GROUP_NAME);
283283

284284
// unknown user
285285
let result =
@@ -296,7 +296,7 @@ mod tests {
296296
resolve_target_user_and_group(&None, &Some(ROOT_GROUP_NAME.into()), &current_user)
297297
.unwrap();
298298
assert_eq!(user.name, current_user.name);
299-
assert_eq!(group.name, ROOT_GROUP_NAME);
299+
assert_eq!(group.name.unwrap(), ROOT_GROUP_NAME);
300300

301301
// fallback to current users group when no group specified
302302
let (user, group) =

src/su/context.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,7 @@ impl SuContext {
9999
}
100100

101101
// resolve target group
102-
let mut group =
103-
Group::from_gid(user.gid)?.ok_or_else(|| Error::GroupNotFound(user.gid.to_string()))?;
102+
let mut group = user.primary_group()?;
104103

105104
if !options.supp_group.is_empty() || !options.group.is_empty() {
106105
user.groups.clear();

src/sudo/env/tests.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ fn create_test_context(sudo_options: &SudoRunOptions) -> Context {
9696

9797
let current_group = Group {
9898
gid: GroupId::new(1000),
99-
name: "test".to_string(),
99+
name: Some("test".to_string()),
100100
};
101101

102102
let root_user = User {
@@ -112,7 +112,7 @@ fn create_test_context(sudo_options: &SudoRunOptions) -> Context {
112112

113113
let root_group = Group {
114114
gid: GroupId::new(0),
115-
name: "root".to_string(),
115+
name: Some("root".to_string()),
116116
};
117117

118118
Context {

src/system/interface.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ impl UnixGroup for super::Group {
160160
self.gid
161161
}
162162
fn try_as_name(&self) -> Option<&str> {
163-
Some(&self.name)
163+
self.name.as_deref()
164164
}
165165
}
166166

src/system/mod.rs

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,12 @@ impl User {
479479
Self::from_uid(Self::real_uid())
480480
}
481481

482+
pub fn primary_group(&self) -> std::io::Result<Group> {
483+
// Use from_gid_unchecked here to ensure that we can still resolve when
484+
// the /etc/group entry for the primary group is missing.
485+
Group::from_gid_unchecked(self.gid)
486+
}
487+
482488
pub fn from_name(name_c: &CStr) -> Result<Option<User>, Error> {
483489
let max_pw_size = sysconf(libc::_SC_GETPW_R_SIZE_MAX).unwrap_or(16_384);
484490
let mut buf = vec![0; max_pw_size as usize];
@@ -511,7 +517,7 @@ impl User {
511517
#[cfg_attr(test, derive(PartialEq))]
512518
pub struct Group {
513519
pub gid: GroupId,
514-
pub name: String,
520+
pub name: Option<String>,
515521
}
516522

517523
impl Group {
@@ -525,11 +531,12 @@ impl Group {
525531
let name = unsafe { string_from_ptr(grp.gr_name) };
526532
Group {
527533
gid: GroupId::new(grp.gr_gid),
528-
name,
534+
name: Some(name),
529535
}
530536
}
531537

532-
pub fn from_gid(gid: GroupId) -> std::io::Result<Option<Group>> {
538+
/// Lookup group for gid without returning an error when a /etc/group entry is missing.
539+
fn from_gid_unchecked(gid: GroupId) -> std::io::Result<Group> {
533540
let max_gr_size = sysconf(libc::_SC_GETGR_R_SIZE_MAX).unwrap_or(16_384);
534541
let mut buf = vec![0; max_gr_size as usize];
535542
let mut grp = MaybeUninit::uninit();
@@ -545,13 +552,23 @@ impl Group {
545552
)
546553
})?;
547554
if grp_ptr.is_null() {
548-
Ok(None)
555+
Ok(Group { gid, name: None })
549556
} else {
550557
// SAFETY: grp_ptr was not null, and getgrgid_r succeeded, so we have assurances that
551558
// the `grp` structure was written to by getgrgid_r
552559
let grp = unsafe { grp.assume_init() };
553560
// SAFETY: `pwd` was obtained by a call to getgrXXX_r, as required.
554-
Ok(Some(unsafe { Group::from_libc(&grp) }))
561+
Ok(unsafe { Group::from_libc(&grp) })
562+
}
563+
}
564+
565+
pub fn from_gid(gid: GroupId) -> std::io::Result<Option<Group>> {
566+
let group = Self::from_gid_unchecked(gid)?;
567+
if group.name.is_none() {
568+
// No entry in /etc/group
569+
Ok(None)
570+
} else {
571+
Ok(Some(group))
555572
}
556573
}
557574

@@ -947,7 +964,7 @@ mod tests {
947964
for &(id, name) in fixed_groups {
948965
let root = Group::from_gid(id).unwrap().unwrap();
949966
assert_eq!(root.gid, id);
950-
assert_eq!(root.name, name);
967+
assert_eq!(root.name.unwrap(), name);
951968
}
952969
}
953970

@@ -978,7 +995,7 @@ mod tests {
978995
}
979996
},
980997
Group {
981-
name: name.to_string(),
998+
name: Some(name.to_string()),
982999
gid: GroupId::new(gid),
9831000
}
9841001
)

test-framework/sudo-compliance-tests/src/sudo/misc.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,13 +217,33 @@ fn does_not_panic_on_io_errors_cli_error() -> Result<()> {
217217
}
218218

219219
#[test]
220-
#[ignore = "gh771"]
221220
fn long_username() -> Result<()> {
222221
// `useradd` limits usernames to 32 characters
223222
// directly write to `/etc/passwd` to work around this limitation
224223
let username = "a".repeat(33);
225224
let env = Env(SUDOERS_ALL_ALL_NOPASSWD).build()?;
226225

226+
Command::new("sh")
227+
.arg("-c")
228+
.arg(format!(
229+
"echo {username}:x:1000:1000::/tmp:/bin/sh >> /etc/passwd && echo {username}:x:1000: >> /etc/group"
230+
))
231+
.output(&env)?
232+
.assert_success()?;
233+
234+
Command::new("sudo")
235+
.arg("-u")
236+
.arg(username)
237+
.arg("true")
238+
.output(&env)?
239+
.assert_success()
240+
}
241+
242+
#[test]
243+
fn missing_primary_group() -> Result<()> {
244+
let username = "user";
245+
let env = Env(SUDOERS_ALL_ALL_NOPASSWD).build()?;
246+
227247
Command::new("sh")
228248
.arg("-c")
229249
.arg(format!(

0 commit comments

Comments
 (0)