Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

unistd: Add getgroups, setgroups, getgrouplist, initgroups #733

Merged
merged 13 commits into from
Nov 12, 2017
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
([#771](https://github.com/nix-rust/nix/pull/771))
- Added `nix::sys::uio::{process_vm_readv, process_vm_writev}` on Linux
([#568](https://github.com/nix-rust/nix/pull/568))
- Added `nix::unistd::{getgroups, setgroups, getgrouplist, initgroups}`. ([#733](https://github.com/nix-rust/nix/pull/733))

### Changed
- Renamed existing `ptrace` wrappers to encourage namespacing ([#692](https://github.com/nix-rust/nix/pull/692))
Expand Down
218 changes: 207 additions & 11 deletions src/unistd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,13 @@ use fcntl::{fcntl, OFlag, O_CLOEXEC, FD_CLOEXEC};
use fcntl::FcntlArg::F_SETFD;
use libc::{self, c_char, c_void, c_int, c_long, c_uint, size_t, pid_t, off_t,
uid_t, gid_t, mode_t};
use std::mem;
use std::{fmt, mem, ptr};
use std::ffi::{CString, CStr, OsString, OsStr};
use std::os::unix::ffi::{OsStringExt, OsStrExt};
use std::os::unix::io::RawFd;
use std::path::{PathBuf};
use void::Void;
use sys::stat::Mode;
use std::fmt;

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

fn to_exec_array(args: &[CString]) -> Vec<*const c_char> {
use std::ptr;
use libc::c_char;

let mut args_p: Vec<*const c_char> = args.iter().map(|s| s.as_ptr()).collect();
args_p.push(ptr::null());
args_p
Expand Down Expand Up @@ -804,7 +800,7 @@ pub enum Whence {
SeekCur = libc::SEEK_CUR,
/// Specify an offset relative to the end of the file.
SeekEnd = libc::SEEK_END,
/// Specify an offset relative to the next location in the file greater than or
/// Specify an offset relative to the next location in the file greater than or
/// equal to offset that contains some data. If offset points to
/// some data, then the file offset is set to offset.
#[cfg(any(target_os = "dragonfly", target_os = "freebsd",
Expand All @@ -813,7 +809,7 @@ pub enum Whence {
target_arch = "mips64")))))]
SeekData = libc::SEEK_DATA,
/// Specify an offset relative to the next hole in the file greater than
/// or equal to offset. If offset points into the middle of a hole, then
/// or equal to offset. If offset points into the middle of a hole, then
/// the file offset should be set to offset. If there is no hole past offset,
/// then the file offset should be adjusted to the end of the file (i.e., there
/// is an implicit hole at the end of any file).
Expand Down Expand Up @@ -1047,6 +1043,206 @@ pub fn setgid(gid: Gid) -> Result<()> {
Errno::result(res).map(drop)
}

/// Get the list of supplementary group IDs of the calling process.
///
/// [Further reading](http://pubs.opengroup.org/onlinepubs/009695399/functions/getgroups.html)
///
/// **Note:** This function is not available for Apple platforms. On those
/// platforms, checking group membership should be achieved via communication
/// with the `opendirectoryd` service.
#[cfg(not(any(target_os = "ios", target_os = "macos")))]
pub fn getgroups() -> Result<Vec<Gid>> {
// First get the number of groups so we can size our Vec
let ret = unsafe { libc::getgroups(0, ptr::null_mut()) };

// Now actually get the groups. We try multiple times in case the number of
// groups has changed since the first call to getgroups() and the buffer is
// now too small.
let mut groups = Vec::<Gid>::with_capacity(Errno::result(ret)? as usize);
loop {
// FIXME: On the platforms we currently support, the `Gid` struct has
// the same representation in memory as a bare `gid_t`. This is not
// necessarily the case on all Rust platforms, though. See RFC 1785.
let ret = unsafe {
libc::getgroups(groups.capacity() as c_int, groups.as_mut_ptr() as *mut gid_t)
};

match Errno::result(ret) {
Ok(s) => {
unsafe { groups.set_len(s as usize) };
return Ok(groups);
},
Err(Error::Sys(Errno::EINVAL)) => {
// EINVAL indicates that the buffer size was too small. Trigger
// the internal buffer resizing logic of `Vec` by requiring
// more space than the current capacity.
let cap = groups.capacity();
unsafe { groups.set_len(cap) };
groups.reserve(1);
},
Err(e) => return Err(e)
}
}
}

/// Set the list of supplementary group IDs for the calling process.
///
/// [Further reading](http://man7.org/linux/man-pages/man2/getgroups.2.html)
///
/// **Note:** This function is not available for Apple platforms. On those
/// platforms, group membership management should be achieved via communication
/// with the `opendirectoryd` service.
///
/// # Examples
///
/// `setgroups` can be used when dropping privileges from the root user to a
/// specific user and group. For example, given the user `www-data` with UID
/// `33` and the group `backup` with the GID `34`, one could switch the user as
/// follows:
/// ```
/// let uid = Uid::from_raw(33);
/// let gid = Gid::from_raw(34);
/// setgroups(&[gid])?;
/// setgid(gid)?;
/// setuid(uid)?;
/// ```
#[cfg(not(any(target_os = "ios", target_os = "macos")))]
pub fn setgroups(groups: &[Gid]) -> Result<()> {
cfg_if! {
if #[cfg(any(target_os = "dragonfly",
target_os = "freebsd",
target_os = "ios",
target_os = "macos",
target_os = "netbsd",
target_os = "openbsd"))] {
type setgroups_ngroups_t = c_int;
} else {
type setgroups_ngroups_t = size_t;
}
}
// FIXME: On the platforms we currently support, the `Gid` struct has the
// same representation in memory as a bare `gid_t`. This is not necessarily
// the case on all Rust platforms, though. See RFC 1785.
let res = unsafe {
libc::setgroups(groups.len() as setgroups_ngroups_t, groups.as_ptr() as *const gid_t)
};

Errno::result(res).map(|_| ())
}

/// Calculate the supplementary group access list.
///
/// Gets the group IDs of all groups that `user` is a member of. The additional
/// group `group` is also added to the list.
///
/// [Further reading](http://man7.org/linux/man-pages/man3/getgrouplist.3.html)
///
/// **Note:** This function is not available for Apple platforms. On those
/// platforms, checking group membership should be achieved via communication
/// with the `opendirectoryd` service.
///
/// # Errors
///
/// Although the `getgrouplist()` call does not return any specific
/// errors on any known platforms, this implementation will return a system
/// error of `EINVAL` if the number of groups to be fetched exceeds the
/// `NGROUPS_MAX` sysconf value. This mimics the behaviour of `getgroups()`
/// and `setgroups()`. Additionally, while some implementations will return a
/// partial list of groups when `NGROUPS_MAX` is exceeded, this implementation
/// will only ever return the complete list or else an error.
#[cfg(not(any(target_os = "ios", target_os = "macos")))]
pub fn getgrouplist(user: &CStr, group: Gid) -> Result<Vec<Gid>> {
let ngroups_max = match sysconf(SysconfVar::NGROUPS_MAX) {
Ok(Some(n)) => n as c_int,
Ok(None) | Err(_) => <c_int>::max_value(),
};
use std::cmp::min;
let mut ngroups = min(ngroups_max, 8);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

8 is pretty arbitrarily chosen. NGROUPS_MAX can vary quite a bit:

  • macOS: 16
  • Debian (Linux/GNU): 65536
  • Alpine (Linux/musl): 32

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the point of capping the number of groups?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I understand? I'm choosing 8 as a good size to start the groups buffer at. If groups is bigger than NGROUPS_MAX then the getgrouplist() call will error on most platforms.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, re-read the docs and it makes sense now.

let mut groups = Vec::<Gid>::with_capacity(ngroups as usize);
cfg_if! {
if #[cfg(any(target_os = "ios", target_os = "macos"))] {
type getgrouplist_group_t = c_int;
} else {
type getgrouplist_group_t = gid_t;
}
}
let gid: gid_t = group.into();
loop {
let ret = unsafe {
libc::getgrouplist(user.as_ptr(),
gid as getgrouplist_group_t,
groups.as_mut_ptr() as *mut getgrouplist_group_t,
&mut ngroups)
};

// BSD systems only return 0 or -1, Linux returns ngroups on success.
if ret >= 0 {
unsafe { groups.set_len(ngroups as usize) };
return Ok(groups);
} else if ret == -1 {
// Returns -1 if ngroups is too small, but does not set errno.
// BSD systems will still fill the groups buffer with as many
// groups as possible, but Linux manpages do not mention this
// behavior.

let cap = groups.capacity();
if cap >= ngroups_max as usize {
// We already have the largest capacity we can, give up
return Err(Error::invalid_argument());
}

// Reserve space for at least ngroups
groups.reserve(ngroups as usize);

// Even if the buffer gets resized to bigger than ngroups_max,
// don't ever ask for more than ngroups_max groups
ngroups = min(ngroups_max, groups.capacity() as c_int);
}
}
}

/// Initialize the supplementary group access list.
///
/// Sets the supplementary group IDs for the calling process using all groups
/// that `user` is a member of. The additional group `group` is also added to
/// the list.
///
/// [Further reading](http://man7.org/linux/man-pages/man3/initgroups.3.html)
///
/// **Note:** This function is not available for Apple platforms. On those
/// platforms, group membership management should be achieved via communication
/// with the `opendirectoryd` service.
///
/// # Examples
///
/// `initgroups` can be used when dropping privileges from the root user to
/// another user. For example, given the user `www-data`, we could look up the
/// UID and GID for the user in the system's password database (usually found
/// in `/etc/passwd`). If the `www-data` user's UID and GID were `33` and `33`,
/// respectively, one could switch the user as follows:
/// ```
/// let user = CString::new("www-data").unwrap();
/// let uid = Uid::from_raw(33);
/// let gid = Gid::from_raw(33);
/// initgroups(&user, gid)?;
/// setgid(gid)?;
/// setuid(uid)?;
/// ```
#[cfg(not(any(target_os = "ios", target_os = "macos")))]
pub fn initgroups(user: &CStr, group: Gid) -> Result<()> {
cfg_if! {
if #[cfg(any(target_os = "ios", target_os = "macos"))] {
type initgroups_group_t = c_int;
} else {
type initgroups_group_t = gid_t;
}
}
let gid: gid_t = group.into();
let res = unsafe { libc::initgroups(user.as_ptr(), gid as initgroups_group_t) };

Errno::result(res).map(|_| ())
}

/// Suspend the thread until a signal is received
///
/// See also [pause(2)](http://pubs.opengroup.org/onlinepubs/9699919799/functions/pause.html)
Expand Down Expand Up @@ -1361,7 +1557,7 @@ pub enum SysconfVar {
OPEN_MAX = libc::_SC_OPEN_MAX,
#[cfg(any(target_os="dragonfly", target_os="freebsd", target_os = "ios",
target_os="linux", target_os = "macos", target_os="openbsd"))]
/// The implementation supports the Advisory Information option.
/// The implementation supports the Advisory Information option.
_POSIX_ADVISORY_INFO = libc::_SC_ADVISORY_INFO,
#[cfg(any(target_os="dragonfly", target_os="freebsd", target_os = "ios",
target_os="linux", target_os = "macos", target_os="netbsd",
Expand All @@ -1380,7 +1576,7 @@ pub enum SysconfVar {
target_os="openbsd"))]
/// The implementation supports the Process CPU-Time Clocks option.
_POSIX_CPUTIME = libc::_SC_CPUTIME,
/// The implementation supports the File Synchronization option.
/// The implementation supports the File Synchronization option.
_POSIX_FSYNC = libc::_SC_FSYNC,
#[cfg(any(target_os="dragonfly", target_os="freebsd", target_os = "ios",
target_os="linux", target_os = "macos", target_os="openbsd"))]
Expand Down Expand Up @@ -1495,7 +1691,7 @@ pub enum SysconfVar {
target_os="linux", target_os = "macos", target_os="openbsd"))]
/// The implementation supports timeouts.
_POSIX_TIMEOUTS = libc::_SC_TIMEOUTS,
/// The implementation supports timers.
/// The implementation supports timers.
_POSIX_TIMERS = libc::_SC_TIMERS,
#[cfg(any(target_os="dragonfly", target_os="freebsd", target_os = "ios",
target_os="linux", target_os = "macos", target_os="openbsd"))]
Expand Down
3 changes: 3 additions & 0 deletions test/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ lazy_static! {
/// Any test that changes the process's current working directory must grab
/// this mutex
pub static ref CWD_MTX: Mutex<()> = Mutex::new(());
/// Any test that changes the process's supplementary groups must grab this
/// mutex
pub static ref GROUPS_MTX: Mutex<()> = Mutex::new(());
/// Any test that creates child processes must grab this mutex, regardless
/// of what it does with those children.
pub static ref FORK_MTX: Mutex<()> = Mutex::new(());
Expand Down
69 changes: 68 additions & 1 deletion test/test_unistd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use nix::unistd::*;
use nix::unistd::ForkResult::*;
use nix::sys::wait::*;
use nix::sys::stat;
use std::{env, iter};
use std::{self, env, iter};
use std::ffi::CString;
use std::fs::File;
use std::io::Write;
Expand Down Expand Up @@ -122,6 +122,73 @@ mod linux_android {
}
}

#[test]
// `getgroups()` and `setgroups()` do not behave as expected on Apple platforms
#[cfg(not(any(target_os = "ios", target_os = "macos")))]
fn test_setgroups() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two tests race against each other. You must add a mutex to prevent that. grep for CWD_MTX for an example.

// Skip this test when not run as root as `setgroups()` requires root.
if !Uid::current().is_root() {
let stderr = std::io::stderr();
let mut handle = stderr.lock();
writeln!(handle, "test_setgroups requires root privileges. Skipping test.").unwrap();
return;
}

#[allow(unused_variables)]
let m = ::GROUPS_MTX.lock().expect("Mutex got poisoned by another test");

// Save the existing groups
let old_groups = getgroups().unwrap();

// Set some new made up groups
let groups = [Gid::from_raw(123), Gid::from_raw(456)];
setgroups(&groups).unwrap();

let new_groups = getgroups().unwrap();
assert_eq!(new_groups, groups);

// Revert back to the old groups
setgroups(&old_groups).unwrap();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this have one last assertion following this to check that they were properly reverted?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The getgroups() and setgroups() calls at the start and end of these tests aren't really part of the tests, they're just a small attempt to restore the state of the current process to what it was before the test ran. 😕

}

#[test]
// `getgroups()` and `setgroups()` do not behave as expected on Apple platforms
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd think it'd still be good to test the mac/ios functionality if we could. Do you understand how they work enough to write some tests for them, even if they don't cover all functionality? I don't like that we document how Apple's platforms are different and then just don't test them even though it's clear we should know how they do work well enough to test them.

#[cfg(not(any(target_os = "ios", target_os = "macos")))]
fn test_initgroups() {
// Skip this test when not run as root as `initgroups()` and `setgroups()`
// require root.
if !Uid::current().is_root() {
let stderr = std::io::stderr();
let mut handle = stderr.lock();
writeln!(handle, "test_initgroups requires root privileges. Skipping test.").unwrap();
return;
}

#[allow(unused_variables)]
let m = ::GROUPS_MTX.lock().expect("Mutex got poisoned by another test");

// Save the existing groups
let old_groups = getgroups().unwrap();

// It doesn't matter if the root user is not called "root" or if a user
// called "root" doesn't exist. We are just checking that the extra,
// made-up group, `123`, is set.
// FIXME: Test the other half of initgroups' functionality: whether the
// groups that the user belongs to are also set.
let user = CString::new("root").unwrap();
let group = Gid::from_raw(123);
let group_list = getgrouplist(&user, group).unwrap();
assert!(group_list.contains(&group));

initgroups(&user, group).unwrap();

let new_groups = getgroups().unwrap();
assert_eq!(new_groups, group_list);

// Revert back to the old groups
setgroups(&old_groups).unwrap();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, shouldn't there be one last assertion following this?

}

macro_rules! execve_test_factory(
($test_name:ident, $syscall:ident, $exe: expr) => (
#[test]
Expand Down