diff options
author | bors[bot] <bors[bot]@users.noreply.github.com> | 2017-11-12 23:10:12 +0000 |
---|---|---|
committer | bors[bot] <bors[bot]@users.noreply.github.com> | 2017-11-12 23:10:12 +0000 |
commit | 7d7e3929665259544713afecb7c83c78b0a028aa (patch) | |
tree | 61c390a7864e34399ae961aa29ee35057e01a1d6 | |
parent | 41c9e1bb21d539d473fc53632862e5b70e2ceb08 (diff) | |
parent | 0c786dfe62dc43b7a463d00fd9c37475ee5a1ce3 (diff) | |
download | nix-7d7e3929665259544713afecb7c83c78b0a028aa.zip |
Merge #733
733: unistd: Add getgroups, setgroups, getgrouplist, initgroups r=Susurrus a=JayH5
-rw-r--r-- | CHANGELOG.md | 1 | ||||
-rw-r--r-- | src/unistd.rs | 218 | ||||
-rw-r--r-- | test/test.rs | 3 | ||||
-rw-r--r-- | test/test_unistd.rs | 69 |
4 files changed, 279 insertions, 12 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 351f7dec..f09b4bed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) diff --git a/src/unistd.rs b/src/unistd.rs index 401357d3..dc0d5d07 100644 --- a/src/unistd.rs +++ b/src/unistd.rs @@ -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::*; @@ -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), @@ -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 @@ -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", @@ -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). @@ -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); + 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) @@ -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", @@ -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"))] @@ -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"))] diff --git a/test/test.rs b/test/test.rs index 1b73c4ff..f4942d3d 100644 --- a/test/test.rs +++ b/test/test.rs @@ -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(()); diff --git a/test/test_unistd.rs b/test/test_unistd.rs index 627eb09b..672cbb9c 100644 --- a/test/test_unistd.rs +++ b/test/test_unistd.rs @@ -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; @@ -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() { + // 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(); +} + +#[test] +// `getgroups()` and `setgroups()` do not behave as expected on Apple platforms +#[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(); +} + macro_rules! execve_test_factory( ($test_name:ident, $syscall:ident, $exe: expr) => ( #[test] |