Skip to content

Commit 7d7e392

Browse files
committed
Merge #733
733: unistd: Add getgroups, setgroups, getgrouplist, initgroups r=Susurrus a=JayH5
2 parents 41c9e1b + 0c786df commit 7d7e392

File tree

4 files changed

+279
-12
lines changed

4 files changed

+279
-12
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
4040
([#771](https://github.com/nix-rust/nix/pull/771))
4141
- Added `nix::sys::uio::{process_vm_readv, process_vm_writev}` on Linux
4242
([#568](https://github.com/nix-rust/nix/pull/568))
43+
- Added `nix::unistd::{getgroups, setgroups, getgrouplist, initgroups}`. ([#733](https://github.com/nix-rust/nix/pull/733))
4344

4445
### Changed
4546
- Renamed existing `ptrace` wrappers to encourage namespacing ([#692](https://github.com/nix-rust/nix/pull/692))

src/unistd.rs

Lines changed: 207 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,13 @@ use fcntl::{fcntl, OFlag, O_CLOEXEC, FD_CLOEXEC};
66
use fcntl::FcntlArg::F_SETFD;
77
use libc::{self, c_char, c_void, c_int, c_long, c_uint, size_t, pid_t, off_t,
88
uid_t, gid_t, mode_t};
9-
use std::mem;
9+
use std::{fmt, mem, ptr};
1010
use std::ffi::{CString, CStr, OsString, OsStr};
1111
use std::os::unix::ffi::{OsStringExt, OsStrExt};
1212
use std::os::unix::io::RawFd;
1313
use std::path::{PathBuf};
1414
use void::Void;
1515
use sys::stat::Mode;
16-
use std::fmt;
1716

1817
#[cfg(any(target_os = "android", target_os = "linux"))]
1918
pub use self::pivot_root::*;
@@ -464,7 +463,7 @@ pub fn mkdir<P: ?Sized + NixPath>(path: &P, mode: Mode) -> Result<()> {
464463
/// fn main() {
465464
/// let tmp_dir = TempDir::new("test_fifo").unwrap();
466465
/// let fifo_path = tmp_dir.path().join("foo.pipe");
467-
///
466+
///
468467
/// // create new fifo and give read, write and execute rights to the owner
469468
/// match unistd::mkfifo(&fifo_path, stat::S_IRWXU) {
470469
/// Ok(_) => println!("created {:?}", fifo_path),
@@ -554,9 +553,6 @@ pub fn chown<P: ?Sized + NixPath>(path: &P, owner: Option<Uid>, group: Option<Gi
554553
}
555554

556555
fn to_exec_array(args: &[CString]) -> Vec<*const c_char> {
557-
use std::ptr;
558-
use libc::c_char;
559-
560556
let mut args_p: Vec<*const c_char> = args.iter().map(|s| s.as_ptr()).collect();
561557
args_p.push(ptr::null());
562558
args_p
@@ -804,7 +800,7 @@ pub enum Whence {
804800
SeekCur = libc::SEEK_CUR,
805801
/// Specify an offset relative to the end of the file.
806802
SeekEnd = libc::SEEK_END,
807-
/// Specify an offset relative to the next location in the file greater than or
803+
/// Specify an offset relative to the next location in the file greater than or
808804
/// equal to offset that contains some data. If offset points to
809805
/// some data, then the file offset is set to offset.
810806
#[cfg(any(target_os = "dragonfly", target_os = "freebsd",
@@ -813,7 +809,7 @@ pub enum Whence {
813809
target_arch = "mips64")))))]
814810
SeekData = libc::SEEK_DATA,
815811
/// Specify an offset relative to the next hole in the file greater than
816-
/// or equal to offset. If offset points into the middle of a hole, then
812+
/// or equal to offset. If offset points into the middle of a hole, then
817813
/// the file offset should be set to offset. If there is no hole past offset,
818814
/// then the file offset should be adjusted to the end of the file (i.e., there
819815
/// is an implicit hole at the end of any file).
@@ -1047,6 +1043,206 @@ pub fn setgid(gid: Gid) -> Result<()> {
10471043
Errno::result(res).map(drop)
10481044
}
10491045

1046+
/// Get the list of supplementary group IDs of the calling process.
1047+
///
1048+
/// [Further reading](http://pubs.opengroup.org/onlinepubs/009695399/functions/getgroups.html)
1049+
///
1050+
/// **Note:** This function is not available for Apple platforms. On those
1051+
/// platforms, checking group membership should be achieved via communication
1052+
/// with the `opendirectoryd` service.
1053+
#[cfg(not(any(target_os = "ios", target_os = "macos")))]
1054+
pub fn getgroups() -> Result<Vec<Gid>> {
1055+
// First get the number of groups so we can size our Vec
1056+
let ret = unsafe { libc::getgroups(0, ptr::null_mut()) };
1057+
1058+
// Now actually get the groups. We try multiple times in case the number of
1059+
// groups has changed since the first call to getgroups() and the buffer is
1060+
// now too small.
1061+
let mut groups = Vec::<Gid>::with_capacity(Errno::result(ret)? as usize);
1062+
loop {
1063+
// FIXME: On the platforms we currently support, the `Gid` struct has
1064+
// the same representation in memory as a bare `gid_t`. This is not
1065+
// necessarily the case on all Rust platforms, though. See RFC 1785.
1066+
let ret = unsafe {
1067+
libc::getgroups(groups.capacity() as c_int, groups.as_mut_ptr() as *mut gid_t)
1068+
};
1069+
1070+
match Errno::result(ret) {
1071+
Ok(s) => {
1072+
unsafe { groups.set_len(s as usize) };
1073+
return Ok(groups);
1074+
},
1075+
Err(Error::Sys(Errno::EINVAL)) => {
1076+
// EINVAL indicates that the buffer size was too small. Trigger
1077+
// the internal buffer resizing logic of `Vec` by requiring
1078+
// more space than the current capacity.
1079+
let cap = groups.capacity();
1080+
unsafe { groups.set_len(cap) };
1081+
groups.reserve(1);
1082+
},
1083+
Err(e) => return Err(e)
1084+
}
1085+
}
1086+
}
1087+
1088+
/// Set the list of supplementary group IDs for the calling process.
1089+
///
1090+
/// [Further reading](http://man7.org/linux/man-pages/man2/getgroups.2.html)
1091+
///
1092+
/// **Note:** This function is not available for Apple platforms. On those
1093+
/// platforms, group membership management should be achieved via communication
1094+
/// with the `opendirectoryd` service.
1095+
///
1096+
/// # Examples
1097+
///
1098+
/// `setgroups` can be used when dropping privileges from the root user to a
1099+
/// specific user and group. For example, given the user `www-data` with UID
1100+
/// `33` and the group `backup` with the GID `34`, one could switch the user as
1101+
/// follows:
1102+
/// ```
1103+
/// let uid = Uid::from_raw(33);
1104+
/// let gid = Gid::from_raw(34);
1105+
/// setgroups(&[gid])?;
1106+
/// setgid(gid)?;
1107+
/// setuid(uid)?;
1108+
/// ```
1109+
#[cfg(not(any(target_os = "ios", target_os = "macos")))]
1110+
pub fn setgroups(groups: &[Gid]) -> Result<()> {
1111+
cfg_if! {
1112+
if #[cfg(any(target_os = "dragonfly",
1113+
target_os = "freebsd",
1114+
target_os = "ios",
1115+
target_os = "macos",
1116+
target_os = "netbsd",
1117+
target_os = "openbsd"))] {
1118+
type setgroups_ngroups_t = c_int;
1119+
} else {
1120+
type setgroups_ngroups_t = size_t;
1121+
}
1122+
}
1123+
// FIXME: On the platforms we currently support, the `Gid` struct has the
1124+
// same representation in memory as a bare `gid_t`. This is not necessarily
1125+
// the case on all Rust platforms, though. See RFC 1785.
1126+
let res = unsafe {
1127+
libc::setgroups(groups.len() as setgroups_ngroups_t, groups.as_ptr() as *const gid_t)
1128+
};
1129+
1130+
Errno::result(res).map(|_| ())
1131+
}
1132+
1133+
/// Calculate the supplementary group access list.
1134+
///
1135+
/// Gets the group IDs of all groups that `user` is a member of. The additional
1136+
/// group `group` is also added to the list.
1137+
///
1138+
/// [Further reading](http://man7.org/linux/man-pages/man3/getgrouplist.3.html)
1139+
///
1140+
/// **Note:** This function is not available for Apple platforms. On those
1141+
/// platforms, checking group membership should be achieved via communication
1142+
/// with the `opendirectoryd` service.
1143+
///
1144+
/// # Errors
1145+
///
1146+
/// Although the `getgrouplist()` call does not return any specific
1147+
/// errors on any known platforms, this implementation will return a system
1148+
/// error of `EINVAL` if the number of groups to be fetched exceeds the
1149+
/// `NGROUPS_MAX` sysconf value. This mimics the behaviour of `getgroups()`
1150+
/// and `setgroups()`. Additionally, while some implementations will return a
1151+
/// partial list of groups when `NGROUPS_MAX` is exceeded, this implementation
1152+
/// will only ever return the complete list or else an error.
1153+
#[cfg(not(any(target_os = "ios", target_os = "macos")))]
1154+
pub fn getgrouplist(user: &CStr, group: Gid) -> Result<Vec<Gid>> {
1155+
let ngroups_max = match sysconf(SysconfVar::NGROUPS_MAX) {
1156+
Ok(Some(n)) => n as c_int,
1157+
Ok(None) | Err(_) => <c_int>::max_value(),
1158+
};
1159+
use std::cmp::min;
1160+
let mut ngroups = min(ngroups_max, 8);
1161+
let mut groups = Vec::<Gid>::with_capacity(ngroups as usize);
1162+
cfg_if! {
1163+
if #[cfg(any(target_os = "ios", target_os = "macos"))] {
1164+
type getgrouplist_group_t = c_int;
1165+
} else {
1166+
type getgrouplist_group_t = gid_t;
1167+
}
1168+
}
1169+
let gid: gid_t = group.into();
1170+
loop {
1171+
let ret = unsafe {
1172+
libc::getgrouplist(user.as_ptr(),
1173+
gid as getgrouplist_group_t,
1174+
groups.as_mut_ptr() as *mut getgrouplist_group_t,
1175+
&mut ngroups)
1176+
};
1177+
1178+
// BSD systems only return 0 or -1, Linux returns ngroups on success.
1179+
if ret >= 0 {
1180+
unsafe { groups.set_len(ngroups as usize) };
1181+
return Ok(groups);
1182+
} else if ret == -1 {
1183+
// Returns -1 if ngroups is too small, but does not set errno.
1184+
// BSD systems will still fill the groups buffer with as many
1185+
// groups as possible, but Linux manpages do not mention this
1186+
// behavior.
1187+
1188+
let cap = groups.capacity();
1189+
if cap >= ngroups_max as usize {
1190+
// We already have the largest capacity we can, give up
1191+
return Err(Error::invalid_argument());
1192+
}
1193+
1194+
// Reserve space for at least ngroups
1195+
groups.reserve(ngroups as usize);
1196+
1197+
// Even if the buffer gets resized to bigger than ngroups_max,
1198+
// don't ever ask for more than ngroups_max groups
1199+
ngroups = min(ngroups_max, groups.capacity() as c_int);
1200+
}
1201+
}
1202+
}
1203+
1204+
/// Initialize the supplementary group access list.
1205+
///
1206+
/// Sets the supplementary group IDs for the calling process using all groups
1207+
/// that `user` is a member of. The additional group `group` is also added to
1208+
/// the list.
1209+
///
1210+
/// [Further reading](http://man7.org/linux/man-pages/man3/initgroups.3.html)
1211+
///
1212+
/// **Note:** This function is not available for Apple platforms. On those
1213+
/// platforms, group membership management should be achieved via communication
1214+
/// with the `opendirectoryd` service.
1215+
///
1216+
/// # Examples
1217+
///
1218+
/// `initgroups` can be used when dropping privileges from the root user to
1219+
/// another user. For example, given the user `www-data`, we could look up the
1220+
/// UID and GID for the user in the system's password database (usually found
1221+
/// in `/etc/passwd`). If the `www-data` user's UID and GID were `33` and `33`,
1222+
/// respectively, one could switch the user as follows:
1223+
/// ```
1224+
/// let user = CString::new("www-data").unwrap();
1225+
/// let uid = Uid::from_raw(33);
1226+
/// let gid = Gid::from_raw(33);
1227+
/// initgroups(&user, gid)?;
1228+
/// setgid(gid)?;
1229+
/// setuid(uid)?;
1230+
/// ```
1231+
#[cfg(not(any(target_os = "ios", target_os = "macos")))]
1232+
pub fn initgroups(user: &CStr, group: Gid) -> Result<()> {
1233+
cfg_if! {
1234+
if #[cfg(any(target_os = "ios", target_os = "macos"))] {
1235+
type initgroups_group_t = c_int;
1236+
} else {
1237+
type initgroups_group_t = gid_t;
1238+
}
1239+
}
1240+
let gid: gid_t = group.into();
1241+
let res = unsafe { libc::initgroups(user.as_ptr(), gid as initgroups_group_t) };
1242+
1243+
Errno::result(res).map(|_| ())
1244+
}
1245+
10501246
/// Suspend the thread until a signal is received
10511247
///
10521248
/// See also [pause(2)](http://pubs.opengroup.org/onlinepubs/9699919799/functions/pause.html)
@@ -1361,7 +1557,7 @@ pub enum SysconfVar {
13611557
OPEN_MAX = libc::_SC_OPEN_MAX,
13621558
#[cfg(any(target_os="dragonfly", target_os="freebsd", target_os = "ios",
13631559
target_os="linux", target_os = "macos", target_os="openbsd"))]
1364-
/// The implementation supports the Advisory Information option.
1560+
/// The implementation supports the Advisory Information option.
13651561
_POSIX_ADVISORY_INFO = libc::_SC_ADVISORY_INFO,
13661562
#[cfg(any(target_os="dragonfly", target_os="freebsd", target_os = "ios",
13671563
target_os="linux", target_os = "macos", target_os="netbsd",
@@ -1380,7 +1576,7 @@ pub enum SysconfVar {
13801576
target_os="openbsd"))]
13811577
/// The implementation supports the Process CPU-Time Clocks option.
13821578
_POSIX_CPUTIME = libc::_SC_CPUTIME,
1383-
/// The implementation supports the File Synchronization option.
1579+
/// The implementation supports the File Synchronization option.
13841580
_POSIX_FSYNC = libc::_SC_FSYNC,
13851581
#[cfg(any(target_os="dragonfly", target_os="freebsd", target_os = "ios",
13861582
target_os="linux", target_os = "macos", target_os="openbsd"))]
@@ -1495,7 +1691,7 @@ pub enum SysconfVar {
14951691
target_os="linux", target_os = "macos", target_os="openbsd"))]
14961692
/// The implementation supports timeouts.
14971693
_POSIX_TIMEOUTS = libc::_SC_TIMEOUTS,
1498-
/// The implementation supports timers.
1694+
/// The implementation supports timers.
14991695
_POSIX_TIMERS = libc::_SC_TIMERS,
15001696
#[cfg(any(target_os="dragonfly", target_os="freebsd", target_os = "ios",
15011697
target_os="linux", target_os = "macos", target_os="openbsd"))]

test/test.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ lazy_static! {
4343
/// Any test that changes the process's current working directory must grab
4444
/// this mutex
4545
pub static ref CWD_MTX: Mutex<()> = Mutex::new(());
46+
/// Any test that changes the process's supplementary groups must grab this
47+
/// mutex
48+
pub static ref GROUPS_MTX: Mutex<()> = Mutex::new(());
4649
/// Any test that creates child processes must grab this mutex, regardless
4750
/// of what it does with those children.
4851
pub static ref FORK_MTX: Mutex<()> = Mutex::new(());

test/test_unistd.rs

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use nix::unistd::*;
44
use nix::unistd::ForkResult::*;
55
use nix::sys::wait::*;
66
use nix::sys::stat;
7-
use std::{env, iter};
7+
use std::{self, env, iter};
88
use std::ffi::CString;
99
use std::fs::File;
1010
use std::io::Write;
@@ -122,6 +122,73 @@ mod linux_android {
122122
}
123123
}
124124

125+
#[test]
126+
// `getgroups()` and `setgroups()` do not behave as expected on Apple platforms
127+
#[cfg(not(any(target_os = "ios", target_os = "macos")))]
128+
fn test_setgroups() {
129+
// Skip this test when not run as root as `setgroups()` requires root.
130+
if !Uid::current().is_root() {
131+
let stderr = std::io::stderr();
132+
let mut handle = stderr.lock();
133+
writeln!(handle, "test_setgroups requires root privileges. Skipping test.").unwrap();
134+
return;
135+
}
136+
137+
#[allow(unused_variables)]
138+
let m = ::GROUPS_MTX.lock().expect("Mutex got poisoned by another test");
139+
140+
// Save the existing groups
141+
let old_groups = getgroups().unwrap();
142+
143+
// Set some new made up groups
144+
let groups = [Gid::from_raw(123), Gid::from_raw(456)];
145+
setgroups(&groups).unwrap();
146+
147+
let new_groups = getgroups().unwrap();
148+
assert_eq!(new_groups, groups);
149+
150+
// Revert back to the old groups
151+
setgroups(&old_groups).unwrap();
152+
}
153+
154+
#[test]
155+
// `getgroups()` and `setgroups()` do not behave as expected on Apple platforms
156+
#[cfg(not(any(target_os = "ios", target_os = "macos")))]
157+
fn test_initgroups() {
158+
// Skip this test when not run as root as `initgroups()` and `setgroups()`
159+
// require root.
160+
if !Uid::current().is_root() {
161+
let stderr = std::io::stderr();
162+
let mut handle = stderr.lock();
163+
writeln!(handle, "test_initgroups requires root privileges. Skipping test.").unwrap();
164+
return;
165+
}
166+
167+
#[allow(unused_variables)]
168+
let m = ::GROUPS_MTX.lock().expect("Mutex got poisoned by another test");
169+
170+
// Save the existing groups
171+
let old_groups = getgroups().unwrap();
172+
173+
// It doesn't matter if the root user is not called "root" or if a user
174+
// called "root" doesn't exist. We are just checking that the extra,
175+
// made-up group, `123`, is set.
176+
// FIXME: Test the other half of initgroups' functionality: whether the
177+
// groups that the user belongs to are also set.
178+
let user = CString::new("root").unwrap();
179+
let group = Gid::from_raw(123);
180+
let group_list = getgrouplist(&user, group).unwrap();
181+
assert!(group_list.contains(&group));
182+
183+
initgroups(&user, group).unwrap();
184+
185+
let new_groups = getgroups().unwrap();
186+
assert_eq!(new_groups, group_list);
187+
188+
// Revert back to the old groups
189+
setgroups(&old_groups).unwrap();
190+
}
191+
125192
macro_rules! execve_test_factory(
126193
($test_name:ident, $syscall:ident, $exe: expr) => (
127194
#[test]

0 commit comments

Comments
 (0)