Skip to content

Commit b7f2ee5

Browse files
quark-zjufacebook-github-bot
authored andcommitted
spawn-ext: extend Command::spawn to avoid inheriting fds
Summary: The Rust upstream took the "set F_CLOEXEC on every opened file" approach and provided no support for closing fds at spawn time to make spawn lightweight [1]. However, that does not play well in our case: - On Windows: - stdin/stdout/stderr are not created by Rust, and inheritable by default (other process like `cargo`, or `dotslash` might leak them too). - a few other handles like "Null", "Afd" are inheritable. It's unclear how they get created, though. - Fortunately, files opened by Python or C in edenscm (ex. packfiles) seem to be not inheritable and do not require special handling. - On Linux: - Files opened by Python or C are likely lack of F_CLOEXEC and need special handling. Implement logic to close file handlers (or set F_CLOEXEC) explicitly. [1]: rust-lang/rust#12148 Reviewed By: DurhamG Differential Revision: D23124167 fbshipit-source-id: 32f3a1b9e3ae3a9475609df282151c9d6c4badd4
1 parent b3fd513 commit b7f2ee5

File tree

3 files changed

+230
-0
lines changed

3 files changed

+230
-0
lines changed

eden/scm/lib/spawn-ext/Cargo.toml

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[package]
2+
name = "spawn-ext"
3+
version = "0.0.1"
4+
edition = "2018"
5+
6+
[dependencies]
7+
libc = "0.2"
8+
9+
[target.'cfg(windows)'.dependencies]
10+
winapi = { version = "0.3", features = ["handleapi", "winbase"] }
11+
12+
[dev-dependencies]
13+
tempfile = "3"
+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This software may be used and distributed according to the terms of the
5+
* GNU General Public License version 2.
6+
*/
7+
8+
use spawn_ext::CommandExt;
9+
use std::process::Command;
10+
use std::time::SystemTime;
11+
12+
fn main() {
13+
let exe_path = std::env::current_exe().unwrap();
14+
if std::env::args().len() == 1 {
15+
let pid = unsafe { libc::getpid() };
16+
println!("parent pid: {}", pid);
17+
let mut cmd = Command::new(exe_path);
18+
let clock = SystemTime::now();
19+
cmd.arg("child").avoid_inherit_handles().new_session();
20+
println!("avoid_inherit_handles took: {:?}", clock.elapsed().unwrap());
21+
let child_pid = cmd.arg("child").spawn_detached().unwrap().id();
22+
println!("child pid: {}", child_pid);
23+
println!("spawn took: {:?}", clock.elapsed().unwrap());
24+
println!("Both processes are sleeping.");
25+
println!();
26+
if cfg!(windows) {
27+
println!("On Windows, use Process Hacker to check handles.");
28+
println!("Inheritable handles are highlighted in cyan.");
29+
} else {
30+
println!(
31+
"On Linux, use 'ls -l /proc/{{{},{}}}/fd/' to check fds.",
32+
pid, child_pid
33+
);
34+
}
35+
println!();
36+
println!("The child should not have more file handles than the parent.");
37+
}
38+
std::thread::sleep(std::time::Duration::from_secs(300))
39+
}

eden/scm/lib/spawn-ext/src/lib.rs

+178
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
/*
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This software may be used and distributed according to the terms of the
5+
* GNU General Public License version 2.
6+
*/
7+
8+
//! This crate extends the `process::Command` interface in stdlib.
9+
//! - `avoid_inherit_handles` is similar to `close_fds=True` in Python.
10+
//! - `new_session` uses `CREATE_NEW_PROCESS_GROUP` on Windows, and `setsid` on
11+
//! Unix.
12+
//! - `spawn_detached` is a quicker way to spawn and forget.
13+
14+
use std::io;
15+
use std::process::Child;
16+
use std::process::Command;
17+
use std::process::Stdio;
18+
19+
/// Extensions to `std::process::Command`.
20+
pub trait CommandExt {
21+
/// Attempt to avoid inheriting file handles.
22+
/// Call this before setting up redirections!
23+
fn avoid_inherit_handles(&mut self) -> &mut Self;
24+
25+
/// Use a new session for the new process.
26+
/// Call this after `avoid_inherit_handles`!
27+
fn new_session(&mut self) -> &mut Self;
28+
29+
/// Spawn a process with stdio redirected to null and forget about it.
30+
/// Return the process id.
31+
fn spawn_detached(&mut self) -> io::Result<Child>;
32+
}
33+
34+
impl CommandExt for Command {
35+
fn avoid_inherit_handles(&mut self) -> &mut Self {
36+
#[cfg(unix)]
37+
unix::avoid_inherit_handles(self);
38+
39+
#[cfg(windows)]
40+
windows::avoid_inherit_handles(self);
41+
42+
self
43+
}
44+
45+
fn new_session(&mut self) -> &mut Self {
46+
#[cfg(unix)]
47+
unix::new_session(self);
48+
49+
#[cfg(windows)]
50+
windows::new_session(self);
51+
52+
self
53+
}
54+
55+
fn spawn_detached(&mut self) -> io::Result<Child> {
56+
self.avoid_inherit_handles()
57+
.new_session()
58+
.stdin(Stdio::null())
59+
.stdout(Stdio::null())
60+
.stderr(Stdio::null())
61+
.spawn()
62+
}
63+
}
64+
65+
#[cfg(windows)]
66+
mod windows {
67+
use super::*;
68+
use std::os::windows::process::CommandExt;
69+
use winapi::um::handleapi::SetHandleInformation;
70+
use winapi::um::winbase::CREATE_NEW_PROCESS_GROUP;
71+
use winapi::um::winbase::CREATE_NO_WINDOW;
72+
use winapi::um::winbase::HANDLE_FLAG_INHERIT;
73+
74+
// Practically MAX_HANDLE < 1000 seems to be enough.
75+
// A larger value like 8192 adds visible overhead (ex. >5ms).
76+
// 2048 has about 2ms overhead and seems reasonable.
77+
const MAX_HANDLE: usize = 2048;
78+
79+
pub fn avoid_inherit_handles(command: &mut Command) {
80+
// Attempt to mark handles as "not inheritable".
81+
//
82+
// Practically only a few handles are accidentally inheritable.
83+
// Use Process Hacker to examine handles [1]. Inheritable handles
84+
// are highlighted in cyan background. Example:
85+
//
86+
// File, \Device\ConDrv, 0x58
87+
// File, \Device\ConDrv, 0x5c
88+
// File, \Device\ConDrv, 0x60
89+
// File, \Device\Afd, 0x348
90+
//
91+
// [1]: https://github.com/processhacker/processhacker/
92+
for handle in 1..=MAX_HANDLE {
93+
let handle = unsafe { std::mem::transmute(handle) };
94+
unsafe { SetHandleInformation(handle, HANDLE_FLAG_INHERIT, 0) };
95+
}
96+
// A cleaner way might be setting bInheritHandles to FALSE at
97+
// CreateProcessW time. However the Rust stdlib does not expose an
98+
// interface to set bInheritHandles, and bInheritHandles=FALSE
99+
// could break file redirections (with possible solutions [2] [3]).
100+
//
101+
// [2]: https://github.com/python/cpython/commit/b2a6083eb0384f38839d
102+
// [3]: https://devblogs.microsoft.com/oldnewthing/20111216-00/?p=8873
103+
104+
// CREATE_NO_WINDOW forbids allocating stdio handles.
105+
command.creation_flags(CREATE_NO_WINDOW);
106+
}
107+
108+
pub fn new_session(command: &mut Command) {
109+
command.creation_flags(CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP);
110+
}
111+
}
112+
113+
#[cfg(unix)]
114+
mod unix {
115+
use super::*;
116+
use std::os::unix::process::CommandExt;
117+
118+
// Linux by default has max fd limited to 1024.
119+
// 2048 is practically more than enough with about 2ms overhead.
120+
const MAXFD: i32 = 2048;
121+
122+
pub fn avoid_inherit_handles(command: &mut Command) {
123+
// There are some constraints for this function.
124+
// See std::os::unix::process::CommandExt::pre_exec.
125+
// Namely, do not allocate.
126+
unsafe { command.pre_exec(pre_exec_close_fds) };
127+
}
128+
129+
pub fn new_session(command: &mut Command) {
130+
unsafe { command.pre_exec(pre_exec_setsid) };
131+
}
132+
133+
fn pre_exec_close_fds() -> io::Result<()> {
134+
// Set FD_CLOEXEC on files.
135+
// Note: using `close` might break error reporting if exec fails.
136+
for fd in 3..=MAXFD {
137+
unsafe { libc::fcntl(fd, libc::F_SETFD, libc::FD_CLOEXEC) };
138+
}
139+
Ok(())
140+
}
141+
142+
fn pre_exec_setsid() -> io::Result<()> {
143+
// Create a new session.
144+
unsafe { libc::setsid() };
145+
Ok(())
146+
}
147+
}
148+
149+
#[cfg(test)]
150+
mod tests {
151+
use super::*;
152+
153+
// It's hard to test the real effects. Here we just check command still runs.
154+
// Use `cargo run --example spawn` to manually check the close_fds behavior.
155+
#[test]
156+
fn smoke_test_command_still_runs() {
157+
let dir = tempfile::tempdir().unwrap();
158+
159+
let args = if cfg!(unix) {
160+
vec!["/bin/sh", "-c", "echo foo > a"]
161+
} else {
162+
vec!["cmd.exe", "/c", "echo foo > a"]
163+
};
164+
let mut command = if cfg!(unix) {
165+
Command::new(&args[0])
166+
} else {
167+
Command::new(&args[0])
168+
};
169+
let mut child = command
170+
.args(&args[1..])
171+
.current_dir(&dir.path())
172+
.spawn_detached()
173+
.unwrap();
174+
child.wait().unwrap();
175+
176+
assert_eq!(&std::fs::read(dir.path().join("a")).unwrap()[..3], b"foo")
177+
}
178+
}

0 commit comments

Comments
 (0)