diff --git a/src/liblibc/lib.rs b/src/liblibc/lib.rs index d895a3e62a32..b81d8e333c78 100644 --- a/src/liblibc/lib.rs +++ b/src/liblibc/lib.rs @@ -3410,6 +3410,8 @@ pub mod consts { pub const F_GETFL : c_int = 3; pub const F_SETFL : c_int = 4; + pub const FD_CLOEXEC : c_int = 1; + pub const O_ACCMODE : c_int = 3; pub const SIGTRAP : c_int = 5; @@ -3522,6 +3524,8 @@ pub mod consts { pub const F_GETFL : c_int = 3; pub const F_SETFL : c_int = 4; + pub const FD_CLOEXEC : c_int = 1; + pub const SIGTRAP : c_int = 5; pub const SIG_IGN: size_t = 1; @@ -4174,6 +4178,8 @@ pub mod consts { pub const F_GETFL : c_int = 3; pub const F_SETFL : c_int = 4; + pub const FD_CLOEXEC : c_int = 1; + pub const SIGTRAP : c_int = 5; pub const SIG_IGN: size_t = 1; @@ -4633,6 +4639,8 @@ pub mod consts { pub const F_SETLKW : c_int = 9; pub const F_DUPFD_CLOEXEC : c_int = 10; + pub const FD_CLOEXEC : c_int = 1; + pub const SIGTRAP : c_int = 5; pub const SIG_IGN: size_t = 1; @@ -5072,6 +5080,8 @@ pub mod consts { pub const F_GETFL : c_int = 3; pub const F_SETFL : c_int = 4; + pub const FD_CLOEXEC : c_int = 1; + pub const O_ACCMODE : c_int = 3; pub const SIGTRAP : c_int = 5; @@ -5795,6 +5805,7 @@ pub mod funcs { extern { pub fn closedir(dirp: *mut DIR) -> c_int; + pub fn dirfd(dirp: *const DIR) -> c_int; pub fn rewinddir(dirp: *mut DIR); pub fn seekdir(dirp: *mut DIR, loc: c_long); pub fn telldir(dirp: *mut DIR) -> c_long; diff --git a/src/libstd/fs.rs b/src/libstd/fs.rs index a903452d5407..bffc309b0a22 100644 --- a/src/libstd/fs.rs +++ b/src/libstd/fs.rs @@ -674,6 +674,10 @@ impl Iterator for ReadDir { } } +impl AsInner for ReadDir { + fn as_inner(&self) -> &fs_imp::ReadDir { &self.0 } +} + #[stable(feature = "rust1", since = "1.0.0")] impl DirEntry { /// Returns the full path to the file that this entry represents. diff --git a/src/libstd/sys/unix/ext/io.rs b/src/libstd/sys/unix/ext/io.rs index 580d2dbcf742..9802f6efe9d6 100644 --- a/src/libstd/sys/unix/ext/io.rs +++ b/src/libstd/sys/unix/ext/io.rs @@ -101,6 +101,10 @@ impl AsRawFd for net::TcpListener { impl AsRawFd for net::UdpSocket { fn as_raw_fd(&self) -> RawFd { *self.as_inner().socket().as_inner() } } +#[unstable(feature = "raw_dirfd", reason = "recently added")] +impl AsRawFd for fs::ReadDir { + fn as_raw_fd(&self) -> RawFd { self.as_inner().as_inner().dirfd() } +} #[stable(feature = "from_raw_os", since = "1.1.0")] impl FromRawFd for net::TcpStream { diff --git a/src/libstd/sys/unix/ext/process.rs b/src/libstd/sys/unix/ext/process.rs index e984c5779350..d887f713c823 100644 --- a/src/libstd/sys/unix/ext/process.rs +++ b/src/libstd/sys/unix/ext/process.rs @@ -12,6 +12,7 @@ #![stable(feature = "rust1", since = "1.0.0")] +use collections::HashSet; use os::unix::raw::{uid_t, gid_t}; use os::unix::io::{FromRawFd, RawFd, AsRawFd, IntoRawFd}; #[cfg(stage0)] @@ -44,6 +45,18 @@ pub trait CommandExt { /// spawn another process (the daemon) in the same session. #[unstable(feature = "process_session_leader", reason = "recently added")] fn session_leader(&mut self, on: bool) -> &mut process::Command; + + /// Set to `false` to prevent file descriptors leak (default is `true`). + #[unstable(feature = "process_leak_fds", reason = "recently added")] + fn leak_fds(&mut self, on: bool) -> &mut process::Command; + + /// Allow to prevent file descriptors leak except for an authorized whitelist. + /// + /// The file descriptors in the whitelist will leak through *all* the subsequent executions + /// (cf. `open(2)` and `O_CLOEXEC`). The new process should change the property of this file + /// descriptors to avoid unintended leaks (cf. `fcntl(2)` and `FD_CLOEXEC`). + #[unstable(feature = "process_leak_fds", reason = "recently added")] + fn leak_fds_whitelist(&mut self, leak: HashSet) -> &mut process::Command; } #[stable(feature = "rust1", since = "1.0.0")] @@ -62,6 +75,18 @@ impl CommandExt for process::Command { self.as_inner_mut().session_leader = on; self } + + fn leak_fds(&mut self, on: bool) -> &mut process::Command { + self.as_inner_mut().leak_fds = on; + self + } + + fn leak_fds_whitelist(&mut self, whitelist: HashSet) -> &mut process::Command { + self.as_inner_mut().leak_fds_whitelist = whitelist; + // Do not leak any FDs except those from the whitelist + self.as_inner_mut().leak_fds = false; + self + } } /// Unix-specific extensions to `std::process::ExitStatus` diff --git a/src/libstd/sys/unix/fs.rs b/src/libstd/sys/unix/fs.rs index ddab24b133fb..49204c1b45d3 100644 --- a/src/libstd/sys/unix/fs.rs +++ b/src/libstd/sys/unix/fs.rs @@ -158,6 +158,16 @@ impl Iterator for ReadDir { } } +impl AsInner for ReadDir { + fn as_inner(&self) -> &Dir { &self.dirp } +} + +impl Dir { + pub fn dirfd(&self) -> RawFd { + unsafe { ::libc::dirfd(self.0) } + } +} + impl Drop for Dir { fn drop(&mut self) { let r = unsafe { libc::closedir(self.0) }; diff --git a/src/libstd/sys/unix/process.rs b/src/libstd/sys/unix/process.rs index 2a365cff6cb9..103994f46ffa 100644 --- a/src/libstd/sys/unix/process.rs +++ b/src/libstd/sys/unix/process.rs @@ -11,7 +11,7 @@ use prelude::v1::*; use os::unix::prelude::*; -use collections::HashMap; +use collections::{HashMap, HashSet}; use env; use ffi::{OsString, OsStr, CString, CStr}; use fmt; @@ -37,6 +37,8 @@ pub struct Command { pub uid: Option, pub gid: Option, pub session_leader: bool, + pub leak_fds: bool, + pub leak_fds_whitelist: HashSet, } impl Command { @@ -49,6 +51,9 @@ impl Command { uid: None, gid: None, session_leader: false, + // Backward compatibility: + leak_fds: true, + leak_fds_whitelist: HashSet::new(), } } @@ -254,6 +259,81 @@ impl Process { unsafe { libc::_exit(1) } } + fn walk_open_fd_brute(action_fd: F) where F: Fn(RawFd) { + // The `getdtablesize(3)` and `sysconf(_SC_OPEN_MAX)` could miss file descriptors if + // the RLIMIT_NOFILE is lowered (cf. #12148) but there is no good way to list them all + // except with procfs (cf. `walk_open_fd_proc()`). + // However, an unprivileged process (i.e. without CAP_SYS_RESOURCE on Linux) is not + // allowed to use a file descriptor upper or equal to it's hard limit nor to change + // this limit upwards. + let begin = libc::STDERR_FILENO + 1; + let end = unsafe { libc::sysconf(libc::consts::os::sysconf::_SC_OPEN_MAX) } as RawFd; + for fd in begin..end { + action_fd(fd); + } + } + + #[cfg(any(target_os = "linux", target_os = "android"))] + fn walk_open_fd_proc(action_fd: F) where F: Fn(RawFd) { + // Take care to look at a real procfs + let is_procfs = match ::fs::metadata("/proc") { + Ok(fs) => { + // The device ID is 3 for a bare Linux but change for other namespaces (e.g. + // with LXC). With Linux user namespace, the UID and GID may be different than + // 0, so we only check the root inode. + fs.is_dir() && fs.ino() == 1 + }, + Err(..) => false + }; + + if !is_procfs { + return walk_open_fd_brute(action_fd); + } + + // There is currently no way to atomically stat a file/directory and use it (cf. + // #15643). + match ::fs::read_dir("/proc/self/fd") { + Ok(dir) => { + let current = dir.as_raw_fd(); + for ret in dir { + match ret { + Ok(entry) => { + let filename = entry.file_name(); + let filename = match filename.to_str() { + Some(s) => s, + None => return walk_open_fd_brute(action_fd), + }; + let fd = match ::str::FromStr::from_str(filename) { + Ok(fd) => fd, + Err(..) => return walk_open_fd_brute(action_fd), + }; + match fd { + libc::STDIN_FILENO => {} + libc::STDOUT_FILENO => {} + libc::STDERR_FILENO => {} + other => if other != current { + action_fd(other); + } + } + } + Err(..) => return walk_open_fd_brute(action_fd), + } + } + }, + Err(..) => walk_open_fd_brute(action_fd), + } + } + + #[cfg(not(any(target_os = "linux", target_os = "android")))] + fn walk_open_fd(action_fd: F) where F: Fn(RawFd) { + walk_open_fd_brute(action_fd) + } + + #[cfg(any(target_os = "linux", target_os = "android"))] + fn walk_open_fd(action_fd: F) where F: Fn(RawFd) { + walk_open_fd_proc(action_fd) + } + let setup = |src: Stdio, dst: c_int| { match src { Stdio::Inherit => true, @@ -283,6 +363,24 @@ impl Process { if !setup(out_fd, libc::STDOUT_FILENO) { fail(&mut output) } if !setup(err_fd, libc::STDERR_FILENO) { fail(&mut output) } + let close_or_leak = |fd: RawFd| { + if cfg.leak_fds_whitelist.contains(&fd) { + let flag_orig = libc::fcntl(fd, libc::F_GETFD, 0); + if flag_orig < 0 { + return; + } + let flag_new = flag_orig & !libc::FD_CLOEXEC; + if flag_orig != flag_new { + let _ = libc::fcntl(fd, libc::F_SETFD, flag_new); + } + } else { + let _ = libc::close(fd); + } + }; + if !cfg.leak_fds { + walk_open_fd(close_or_leak); + } + if let Some(u) = cfg.gid { if libc::setgid(u as libc::gid_t) != 0 { fail(&mut output); diff --git a/src/test/run-pass/process-leak-fds.rs b/src/test/run-pass/process-leak-fds.rs new file mode 100644 index 000000000000..08fdf9afcf32 --- /dev/null +++ b/src/test/run-pass/process-leak-fds.rs @@ -0,0 +1,145 @@ +// Copyright 2015 The Rust Project Developers. See the COPYRIGHT +// file at the top-level directory of this distribution and at +// http://rust-lang.org/COPYRIGHT. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +#![feature(convert)] +#![feature(libc)] +#![feature(process_leak_fds)] + +extern crate libc; + +use std::collections::HashSet; +use std::fs::{File, read_dir}; +use std::os::unix::io::{AsRawFd, RawFd}; +use std::os::unix::process::CommandExt; +use std::process::Command; +use std::str::FromStr; + +#[cfg(any(target_os = "linux", + target_os = "android"))] +fn check_fds(mut whitelist: HashSet) -> ! { + match read_dir("/dev/fd") { + Ok(dir) => { + let current = dir.as_raw_fd(); + for ret in dir { + match ret { + Ok(entry) => { + let filename = entry.file_name(); + let filename = match filename.to_str() { + Some(s) => s, + None => panic!("Failed to convert {:?}", filename), + }; + let fd = match FromStr::from_str(filename) { + Ok(fd) => fd, + Err(e) => panic!("Failed to convert {:?}: {}", filename, e), + }; + if fd != current && !whitelist.contains(&fd) { + panic!("Unexpected leaked FD {}", fd); + } + whitelist.remove(&fd); + } + Err(e) => panic!("Failed to get directory entry: {}", e), + } + } + } + Err(e) => panic!("Failed to get directory entry: {}", e), + } + if whitelist.len() != 0 { + panic!("Failed to leak FDs: {:?}", whitelist); + } + std::process::exit(0); +} + +macro_rules! add_args { + ($cmd: expr, $whitelist: expr) => { + $cmd.args($whitelist.iter().map(|x| format!("{}", x)).collect::>().as_slice()) + } +} + +#[cfg(any(target_os = "linux", + target_os = "android"))] +fn main() { + let args = std::env::args().collect::>(); + if args.len() > 1 { + let whitelist = args[1..].iter().map(|x| FromStr::from_str(x).unwrap()).collect(); + check_fds(whitelist); + } else { + let exe = std::env::current_exe().unwrap(); + // Preload stdio + let mut whitelist = HashSet::new(); + for fd in &[libc::STDIN_FILENO, libc::STDOUT_FILENO, libc::STDERR_FILENO] { + whitelist.insert(*fd); + } + + let mut cmd = Command::new(exe.clone()); + let ret = add_args!(cmd, whitelist). + status().unwrap().code().unwrap(); + if ret != 0 { + panic!("Test #1 failed"); + } + + // Leak a first file descriptor + let fd1 = unsafe { libc::open(exe.to_str().unwrap().as_ptr() as *const i8, + libc::O_RDONLY, 0) }; + + // Launch a command without FD leak + let mut cmd = Command::new(exe.clone()); + let ret = add_args!(cmd, whitelist). + leak_fds(false). + status().unwrap().code().unwrap(); + if ret != 0 { + panic!("Test #2 failed"); + } + + // Launch a command with the first FD leak + whitelist.insert(fd1); + let mut cmd = Command::new(exe.clone()); + let ret = add_args!(cmd, whitelist). + leak_fds_whitelist(whitelist.clone()). + status().unwrap().code().unwrap(); + if ret != 0 { + panic!("Test #3 failed"); + } + + // Leak a second file descriptor + let _ = unsafe { libc::open(exe.to_str().unwrap().as_ptr() as *const i8, + libc::O_RDONLY, 0) }; + + // Open a third file descriptor but with O_CLOEXEC + let fd3 = File::open(&exe).unwrap(); + + // Launch a command with the first FD leak (but not the second nor the third) + let mut cmd = Command::new(exe.clone()); + let ret = add_args!(cmd, whitelist). + leak_fds_whitelist(whitelist.clone()). + status().unwrap().code().unwrap(); + if ret != 0 { + panic!("Test #4 failed"); + } + + // Launch a command with the first and the third FD (but not the second) + whitelist.insert(fd3.as_raw_fd()); + let mut cmd = Command::new(exe.clone()); + let ret = add_args!(cmd, whitelist). + leak_fds_whitelist(whitelist.clone()). + status().unwrap().code().unwrap(); + if ret != 0 { + panic!("Test #5 failed"); + } + } +} + +#[cfg(any(target_os = "bitrig", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "macos", + target_os = "netbsd", + target_os = "openbsd", + target_os = "windows"))] +pub fn main() { }