Skip to content

Commit a0e6252

Browse files
committed
add std::os::unix::process::CommandExt::fd
1 parent ff08635 commit a0e6252

4 files changed

Lines changed: 298 additions & 0 deletions

File tree

library/std/src/os/unix/process.rs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,88 @@ pub trait CommandExt: Sealed {
219219

220220
#[unstable(feature = "process_setsid", issue = "105376")]
221221
fn setsid(&mut self, setsid: bool) -> &mut process::Command;
222+
223+
/// Pass a file descriptor to a child process.
224+
///
225+
/// `old_fd` is an open file descriptor in the parent process. This fd will be duplicated in the
226+
/// child process and associated with the fd number `new_fd`.
227+
///
228+
/// Getting this right is tricky. It is recommended to provide further information to the child
229+
/// process by some other mechanism. This could be an argument confirming file descriptors that
230+
/// the child can use, device/inode numbers to allow for sanity checks, or something similar.
231+
///
232+
/// If `old_fd` is an open file descriptor in the child process (e.g. if multiple parent fds are being
233+
/// mapped to the same child one) and closing it would produce one or more errors,
234+
/// those errors will be lost when this function is called. See
235+
/// [`man 2 dup`](https://www.man7.org/linux/man-pages/man2/dup.2.html#NOTES) for more information.
236+
///
237+
/// ```
238+
/// #![feature(command_pass_fds)]
239+
///
240+
/// use std::process::{Command, Stdio};
241+
/// use std::os::unix::process::CommandExt;
242+
/// use std::io::{self, Write};
243+
///
244+
/// # fn main() -> io::Result<()> {
245+
/// let (pipe_reader, mut pipe_writer) = io::pipe()?;
246+
///
247+
/// let fd_num = 123;
248+
///
249+
/// let mut cmd = Command::new("cat");
250+
/// cmd.arg(format!("/dev/fd/{fd_num}")).stdout(Stdio::piped()).fd(fd_num, pipe_reader);
251+
///
252+
/// let mut child = cmd.spawn()?;
253+
/// let mut stdout = child.stdout.take().unwrap();
254+
///
255+
/// pipe_writer.write_all(b"Hello, world!")?;
256+
/// drop(pipe_writer);
257+
///
258+
/// child.wait()?;
259+
/// assert_eq!(io::read_to_string(&mut stdout)?, "Hello, world!");
260+
///
261+
/// # Ok(())
262+
/// # }
263+
/// ```
264+
///
265+
/// If this method is called multiple times with the same `new_fd`, all but one file descriptor
266+
/// will be lost.
267+
///
268+
/// ```
269+
/// #![feature(command_pass_fds)]
270+
///
271+
/// use std::process::{Command, Stdio};
272+
/// use std::os::unix::process::CommandExt;
273+
/// use std::io::{self, Write};
274+
///
275+
/// # fn main() -> io::Result<()> {
276+
/// let (pipe_reader1, mut pipe_writer1) = io::pipe()?;
277+
/// let (pipe_reader2, mut pipe_writer2) = io::pipe()?;
278+
///
279+
/// let fd_num = 123;
280+
///
281+
/// let mut cmd = Command::new("cat");
282+
/// cmd.arg(format!("/dev/fd/{fd_num}"))
283+
/// .stdout(Stdio::piped())
284+
/// .fd(fd_num, pipe_reader1)
285+
/// .fd(fd_num, pipe_reader2);
286+
///
287+
/// pipe_writer1.write_all(b"Hello from pipe 1!")?;
288+
/// drop(pipe_writer1);
289+
///
290+
/// pipe_writer2.write_all(b"Hello from pipe 2!")?;
291+
/// drop(pipe_writer2);
292+
///
293+
/// let mut child = cmd.spawn()?;
294+
/// let mut stdout = child.stdout.take().unwrap();
295+
///
296+
/// child.wait()?;
297+
/// assert_eq!(io::read_to_string(&mut stdout)?, "Hello from pipe 2!");
298+
///
299+
/// # Ok(())
300+
/// # }
301+
/// ```
302+
#[unstable(feature = "command_pass_fds", issue = "144989")]
303+
fn fd(&mut self, new_fd: RawFd, old_fd: impl Into<OwnedFd>) -> &mut Self;
222304
}
223305

224306
#[stable(feature = "rust1", since = "1.0.0")]
@@ -274,6 +356,11 @@ impl CommandExt for process::Command {
274356
self.as_inner_mut().setsid(setsid);
275357
self
276358
}
359+
360+
fn fd(&mut self, new_fd: RawFd, old_fd: impl Into<OwnedFd>) -> &mut Self {
361+
self.as_inner_mut().fd(old_fd.into(), new_fd);
362+
self
363+
}
277364
}
278365

279366
/// Unix-specific extensions to [`process::ExitStatus`] and

library/std/src/sys/process/unix/common.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,11 @@ pub struct Command {
103103
create_pidfd: bool,
104104
pgroup: Option<pid_t>,
105105
setsid: bool,
106+
// A map of parent FDs to child FDs to be inherited during spawn.
107+
fds: Vec<(OwnedFd, RawFd)>,
108+
// For testing purposes: store `Some(true)` if the last spawn used `posix_spawn`, `Some(false)`
109+
// if it used `exec`, and `None` if it hasn't been spawned yet.
110+
last_spawn_was_posix_spawn: Option<bool>,
106111
}
107112

108113
// passed to do_exec() with configuration of what the child stdio should look
@@ -183,6 +188,8 @@ impl Command {
183188
create_pidfd: false,
184189
pgroup: None,
185190
setsid: false,
191+
fds: Vec::new(),
192+
last_spawn_was_posix_spawn: None,
186193
}
187194
}
188195

@@ -364,6 +371,29 @@ impl Command {
364371
let theirs = ChildPipes { stdin: their_stdin, stdout: their_stdout, stderr: their_stderr };
365372
Ok((ours, theirs))
366373
}
374+
375+
pub fn fd(&mut self, old_fd: OwnedFd, new_fd: RawFd) {
376+
self.fds.push((old_fd, new_fd));
377+
}
378+
379+
pub fn get_fds(&self) -> &[(OwnedFd, RawFd)] {
380+
&self.fds
381+
}
382+
383+
/// Clear the fd vector, closing all descriptors owned by this `Command`.
384+
pub fn close_owned_fds(&mut self) {
385+
self.fds.clear();
386+
}
387+
388+
pub fn last_spawn_was_posix_spawn(&mut self, val: bool) {
389+
self.last_spawn_was_posix_spawn = Some(val);
390+
}
391+
392+
// this lint can't see tests
393+
#[allow(unused)]
394+
pub fn get_last_spawn_was_posix_spawn(&self) -> Option<bool> {
395+
self.last_spawn_was_posix_spawn
396+
}
367397
}
368398

369399
fn os2c(s: &OsStr, saw_nul: &mut bool) -> CString {

library/std/src/sys/process/unix/unix.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use libc::{gid_t, uid_t};
1313
use super::common::*;
1414
use crate::io::{self, Error, ErrorKind};
1515
use crate::num::NonZero;
16+
use crate::os::fd::AsRawFd;
1617
use crate::process::StdioPipes;
1718
use crate::sys::cvt;
1819
#[cfg(target_os = "linux")]
@@ -71,8 +72,12 @@ impl Command {
7172
let (ours, theirs) = self.setup_io(default, needs_stdin)?;
7273

7374
if let Some(ret) = self.posix_spawn(&theirs, envp.as_ref())? {
75+
self.last_spawn_was_posix_spawn(true);
76+
// Close fds in the parent that have been duplicated in the child
77+
self.close_owned_fds();
7478
return Ok((ret, ours));
7579
}
80+
self.last_spawn_was_posix_spawn(false);
7681

7782
#[cfg(target_os = "linux")]
7883
let (input, output) = sys::net::Socket::new_pair(libc::AF_UNIX, libc::SOCK_SEQPACKET)?;
@@ -124,6 +129,9 @@ impl Command {
124129
drop(env_lock);
125130
drop(output);
126131

132+
// Close fds in the parent that have been duplicated in the child
133+
self.close_owned_fds();
134+
127135
#[cfg(target_os = "linux")]
128136
let pidfd = if self.get_create_pidfd() { self.recv_pidfd(&input) } else { -1 };
129137

@@ -292,6 +300,11 @@ impl Command {
292300
cvt_r(|| libc::dup2(fd, libc::STDERR_FILENO))?;
293301
}
294302

303+
for &(ref old_fd, new_fd) in self.get_fds() {
304+
cvt_r(|| libc::dup2(old_fd.as_raw_fd(), new_fd))?;
305+
cvt_r(|| libc::close(old_fd.as_raw_fd()))?;
306+
}
307+
295308
#[cfg(not(target_os = "l4re"))]
296309
{
297310
if let Some(_g) = self.get_groups() {
@@ -455,6 +468,7 @@ impl Command {
455468
use core::sync::atomic::{Atomic, AtomicU8, Ordering};
456469

457470
use crate::mem::MaybeUninit;
471+
use crate::os::fd::AsRawFd;
458472
use crate::sys::{self, cvt_nz, on_broken_pipe_used};
459473

460474
if self.get_gid().is_some()
@@ -715,6 +729,17 @@ impl Command {
715729
libc::STDERR_FILENO,
716730
))?;
717731
}
732+
for &(ref old_fd, new_fd) in self.get_fds() {
733+
cvt_nz(libc::posix_spawn_file_actions_adddup2(
734+
file_actions.0.as_mut_ptr(),
735+
old_fd.as_raw_fd(),
736+
new_fd,
737+
))?;
738+
cvt_nz(libc::posix_spawn_file_actions_addclose(
739+
file_actions.0.as_mut_ptr(),
740+
old_fd.as_raw_fd(),
741+
))?;
742+
}
718743
if let Some((f, cwd)) = addchdir {
719744
cvt_nz(f(file_actions.0.as_mut_ptr(), cwd.as_ptr()))?;
720745
}

library/std/src/sys/process/unix/unix/tests.rs

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
1+
use crate::fs;
2+
use crate::os::unix::fs::MetadataExt;
13
use crate::os::unix::process::{CommandExt, ExitStatusExt};
24
use crate::panic::catch_unwind;
35
use crate::process::Command;
6+
use crate::sys::AsInner;
47

58
// Many of the other aspects of this situation, including heap alloc concurrency
69
// safety etc., are tested in tests/ui/process/process-panic-after-fork.rs
710

11+
/// Use dev + ino to uniquely identify a file
12+
fn md_file_id(md: &fs::Metadata) -> (u64, u64) {
13+
(md.dev(), md.ino())
14+
}
15+
816
#[test]
917
fn exitstatus_display_tests() {
1018
// In practice this is the same on every Unix.
@@ -74,3 +82,151 @@ fn test_command_fork_no_unwind() {
7482
|| signal == libc::SIGSEGV
7583
);
7684
}
85+
86+
/// For `Command`'s fd-related tests, we want to be sure they work both with exec
87+
/// and with `posix_spawn`. We test both the default which should use `posix_spawn`
88+
/// on supported platforms, and using `pre_exec` to force spawn using `exec`.
89+
mod fd_impls {
90+
use super::{assert_spawn_method, md_file_id};
91+
use crate::fs;
92+
use crate::io::{self, Write};
93+
use crate::os::fd::AsRawFd;
94+
use crate::os::unix::process::CommandExt;
95+
use crate::process::{Command, Stdio};
96+
97+
/// Check setting the child's stdin via `.fd`.
98+
pub fn test_stdin(use_exec: bool) {
99+
let (pipe_reader, mut pipe_writer) = io::pipe().unwrap();
100+
101+
let fd_num = libc::STDIN_FILENO;
102+
103+
let mut cmd = Command::new("cat");
104+
cmd.stdout(Stdio::piped()).fd(fd_num, pipe_reader);
105+
106+
if use_exec {
107+
unsafe {
108+
cmd.pre_exec(|| Ok(()));
109+
}
110+
}
111+
112+
let mut child = cmd.spawn().unwrap();
113+
let mut stdout = child.stdout.take().unwrap();
114+
115+
assert_spawn_method(&cmd, use_exec);
116+
117+
pipe_writer.write_all(b"Hello, world!").unwrap();
118+
drop(pipe_writer);
119+
120+
child.wait().unwrap().exit_ok().unwrap();
121+
assert_eq!(io::read_to_string(&mut stdout).unwrap(), "Hello, world!");
122+
}
123+
124+
// FIXME: fails on android
125+
#[cfg_attr(not(target_os = "android"), should_panic)]
126+
/// Check that the last `.fd` mapping is preserved when there are conflicts.
127+
pub fn test_swap(use_exec: bool) {
128+
let (pipe_reader1, mut pipe_writer1) = io::pipe().unwrap();
129+
let (pipe_reader2, mut pipe_writer2) = io::pipe().unwrap();
130+
131+
let num1 = pipe_reader1.as_raw_fd();
132+
let num2 = pipe_reader2.as_raw_fd();
133+
134+
let mut cmd = Command::new("cat");
135+
cmd.arg(format!("/dev/fd/{num1}"))
136+
.arg(format!("/dev/fd/{num2}"))
137+
.stdout(Stdio::piped())
138+
.fd(num2, pipe_reader1)
139+
.fd(num1, pipe_reader2);
140+
141+
if use_exec {
142+
unsafe {
143+
cmd.pre_exec(|| Ok(()));
144+
}
145+
}
146+
147+
pipe_writer1.write_all(b"Hello from pipe 1!").unwrap();
148+
drop(pipe_writer1);
149+
150+
pipe_writer2.write_all(b"Hello from pipe 2!").unwrap();
151+
drop(pipe_writer2);
152+
153+
let mut child = cmd.spawn().unwrap();
154+
let mut stdout = child.stdout.take().unwrap();
155+
156+
assert_spawn_method(&cmd, use_exec);
157+
158+
child.wait().unwrap().exit_ok().unwrap();
159+
// the second pipe's output is clobbered; this is expected.
160+
assert_eq!(io::read_to_string(&mut stdout).unwrap(), "Hello from pipe 1!");
161+
}
162+
163+
// ensure that the fd is properly closed in the parent, but only after the child is spawned.
164+
pub fn test_close_time(use_exec: bool) {
165+
let (_pipe_reader, pipe_writer) = io::pipe().unwrap();
166+
167+
let fd = pipe_writer.as_raw_fd();
168+
let fd_path = format!("/dev/fd/{fd}");
169+
170+
let mut cmd = Command::new("true");
171+
cmd.fd(123, pipe_writer);
172+
173+
if use_exec {
174+
unsafe {
175+
cmd.pre_exec(|| Ok(()));
176+
}
177+
}
178+
179+
// Get the identifier of the fd (metadata follows symlinks)
180+
let fd_id = md_file_id(&fs::metadata(&fd_path).expect("fd should be open"));
181+
182+
cmd.spawn().unwrap().wait().unwrap().exit_ok().unwrap();
183+
184+
assert_spawn_method(&cmd, use_exec);
185+
186+
// After the child is spawned, our fd should be closed
187+
match fs::metadata(&fd_path) {
188+
// Ok; fd exists but points to a different file
189+
Ok(md) => assert_ne!(md_file_id(&md), fd_id),
190+
// Ok; fd does not exist
191+
Err(_) => (),
192+
}
193+
}
194+
}
195+
196+
#[test]
197+
fn fd_test_stdin() {
198+
fd_impls::test_stdin(false);
199+
fd_impls::test_stdin(true);
200+
}
201+
202+
#[test]
203+
fn fd_test_swap() {
204+
fd_impls::test_swap(false);
205+
fd_impls::test_swap(true);
206+
}
207+
208+
#[test]
209+
fn fd_test_close_time() {
210+
fd_impls::test_close_time(false);
211+
fd_impls::test_close_time(true);
212+
}
213+
214+
#[track_caller]
215+
fn assert_spawn_method(cmd: &Command, use_exec: bool) {
216+
let used_posix_spawn = cmd.as_inner().get_last_spawn_was_posix_spawn().unwrap();
217+
if use_exec {
218+
assert!(!used_posix_spawn, "posix_spawn used but exec was expected");
219+
} else if cfg!(any(
220+
target_os = "freebsd",
221+
target_os = "illumos",
222+
all(target_os = "linux", target_env = "gnu"),
223+
all(target_os = "linux", target_env = "musl"),
224+
target_os = "nto",
225+
target_vendor = "apple",
226+
target_os = "cygwin",
227+
)) {
228+
assert!(used_posix_spawn, "platform supports posix_spawn but it wasn't used");
229+
} else {
230+
assert!(!used_posix_spawn);
231+
}
232+
}

0 commit comments

Comments
 (0)