1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.

//! closefds is a library that provides support for setting the `FD_CLOEXEC` flag on all open
//! file descriptors after `fork()` and before `exec()` on UNIX-like systems.
//!
//! Any file descriptors that aren't marked with this flag will stay open after `exec()`
//! which can cause resources to leak and can lead to deadlocks. Ideally, whenever a file
//! descriptor is created, it will be created with the `FD_CLOEXEC` flag already set.
//! However, this may not be possible in some circumstances - such as when using an
//! external library or a system call that does not support the `FD_CLOEXEC` flag, such as
//! `pipe()`.
//!
//! The function `close_fds_on_exec()` will create a closure that can be passed
//! as a `pre_exec()` function when spawning a child process via the `Command` interface
//! and will set the `FD_CLOEXEC` flag as appropriate on open file descriptors.

use std::{ffi::CStr, io, os::unix::io::RawFd, ptr};

#[cfg(any(
    target_os = "dragonfly",
    target_os = "freebsd",
    target_os = "netbsd",
    target_os = "openbsd",
    target_os = "macos",
))]
const FD_DIR_NAME: &'static [u8; 8] = b"/dev/fd\0";

#[cfg(target_os = "linux")]
const FD_DIR_NAME: &'static [u8; 14] = b"/proc/self/fd\0";

struct OpenDir {
    dir: *mut libc::DIR,
}

// My best understanding is that functions that work with a libc::DIR
// do the appropriate locking to make it safe to work with from
// multiple threads.
unsafe impl Send for OpenDir {}
unsafe impl Sync for OpenDir {}

impl OpenDir {
    fn open(dir_path: &CStr) -> io::Result<OpenDir> {
        let dir = unsafe { libc::opendir(dir_path.as_ptr()) };
        if dir == ptr::null_mut() {
            return Err(io::Error::last_os_error());
        }
        Ok(OpenDir { dir })
    }
}

impl Drop for OpenDir {
    fn drop(&mut self) {
        // This will likely call free() - which is why the closure that
        // is created by close_fds_on_exec() should not be dropped by
        // the child process after fork().
        let _ = unsafe { libc::closedir(self.dir) };
    }
}

fn set_cloexec(fd: RawFd, set: bool) -> io::Result<()> {
    let mut fd_flags = unsafe { libc::fcntl(fd, libc::F_GETFD) };
    if fd_flags == -1 {
        return Err(io::Error::last_os_error());
    }

    let is_set = fd_flags & libc::FD_CLOEXEC != 0;

    if set == is_set {
        return Ok(());
    } else {
        if set {
            fd_flags |= libc::FD_CLOEXEC;
        } else {
            fd_flags &= !libc::FD_CLOEXEC;
        }
    }

    if unsafe { libc::fcntl(fd, libc::F_SETFD, fd_flags) } == -1 {
        return Err(io::Error::last_os_error());
    }

    Ok(())
}

unsafe fn pos_int_from_ascii(mut name: *const libc::c_char) -> io::Result<libc::c_int> {
    let mut num = 0;
    while *name >= '0' as i8 && *name <= '9' as i8 {
        num = num * 10 + (*name - '0' as i8) as libc::c_int;
        name = name.offset(1);
    }
    // If the last byte isn't a NULL, it means we found a
    // non-digit.
    if *name != 0 {
        errno::set_errno(errno::Errno(libc::ENOENT));
        return Err(io::Error::new(
            io::ErrorKind::Other,
            "fd file name contained non-integer characters",
        ));
    }
    Ok(num)
}

struct CloseFdsOnExec {
    dir: OpenDir,
    keep_fds: Vec<RawFd>,
}

impl CloseFdsOnExec {
    pub fn new(mut keep_fds: Vec<RawFd>) -> io::Result<Self> {
        let dir = OpenDir::open(CStr::from_bytes_with_nul(FD_DIR_NAME).expect("Invalid Path"))?;
        keep_fds.sort_unstable();
        Ok(CloseFdsOnExec { dir, keep_fds })
    }

    pub fn before_exec(&mut self) -> io::Result<()> {
        unsafe {
            errno::set_errno(errno::Errno(0));
            libc::rewinddir(self.dir.dir);
            if errno::errno() != errno::Errno(0) {
                return Err(io::Error::last_os_error());
            }

            loop {
                errno::set_errno(errno::Errno(0));
                let dir_entry = libc::readdir(self.dir.dir);
                if dir_entry == ptr::null_mut() {
                    if errno::errno() != errno::Errno(0) {
                        return Err(io::Error::last_os_error());
                    } else {
                        break;
                    }
                }

                let f = pos_int_from_ascii((*dir_entry).d_name.as_ptr())?;
                let needs_cloexec = self.keep_fds.binary_search(&f).is_err();
                set_cloexec(f, needs_cloexec)?;
            }
        }
        Ok(())
    }
}

/// Create a closure that will set the `FD_CLOEXEC` flag on all open file descriptors when called.
/// This function should be called _before_ invoking `fork()` as it may allocate memory. The
/// returned closure should be called after `fork()` by the child process.
///
/// This function may fail to create the closure. Additionally, the closure may
/// also fail when called. However, in no case will we fall back to an implementation that does not
/// guarantee that all open file descriptors have been successfully processed (ie: We will not
/// look up the max number of open file descriptors and then attempt to close all file
/// descriptors up to that number as such an approach may fail to process some file descriptors
/// if the max number of open file descriptors changes).
///
/// `keep_fds` is a `Vec` of file descriptors to ensure that the `FD_CLOEXEC` flag is
/// _not_ set on. `FD_CLOEXEC` will be set on all other file descriptors.
///
/// # Current Implementation
///
/// The current implementation opens either the `/proc/self/fd/` directory (Linux) or `/dev/fd/`
/// directory (BSDs) in the parent process with `opendir()`. `readdir()` is used in the child
/// process to iterate over the entries in that directory and set the `FD_CLOEXEC` flag as
/// appropriate.
///
/// Notes:
///
/// * `readdir()` is not async-signal-safe according to any standard. However, the process
/// spawning code in both Python and Java work similarly, so `readdir()` seems
/// to be safe to call in practice after `fork()`.
///
/// * `/proc/self/fd/` or `/dev/fd/` directories _must_ be available.
///
/// * The returned closure needs to be dropped in the parent process in order to close
/// the opened directory. However, it must not be dropped in the child process as doing
/// so will call `free()` which may deadlock - all resources will instead be freed when
/// `exec()` occurs. (The standard library `CommandExt` interface does not drop closures
/// before `exec()`).
///
/// # Future Implementations
///
/// A future version of this library may change the implementation for all supported operating
/// systems or for specific operating systems. The likely reasons to do so would be to improve
/// performance, to remove calls to non-async-signal-safe functions, or to remove the dependency
/// on specific directories being available. However, any future implementation is guaranteed
/// to still process all open file descriptors.
///
/// # Example
///
/// The following example will spawn a child process while making sure that only STDIN, STDOUT, and
/// STDERR are inherited.
///
/// ```no_run
/// # use closefds::close_fds_on_exec;
/// # use std::process::Command;
/// # use std::os::unix::process::CommandExt;
/// # fn main() -> std::io::Result<()> {
/// # unsafe {
/// Command::new("path/to/program")
///     .pre_exec(close_fds_on_exec(vec![0, 1, 2])?)
///     .spawn()
///     .expect("Spawn Failed");
/// # }
/// # Ok(())
/// # }
/// ```
pub fn close_fds_on_exec(keep_fds: Vec<RawFd>) -> io::Result<impl FnMut() -> io::Result<()>> {
    let mut close_fds_on_exec = CloseFdsOnExec::new(keep_fds)?;

    let func = move || close_fds_on_exec.before_exec();

    Ok(func)
}

#[allow(dead_code)]
fn assert_traits() {
    fn check_traits<T: Send + Sync + 'static>(_: T) {}

    check_traits(close_fds_on_exec(vec![]));
    check_traits(CloseFdsOnExec::new(vec![]));
}