Skip to content

Commit bb5247b

Browse files
Auto merge of #145687 - Qelxiros:fd-passing, r=<try>
add std::os::unix::process::CommandExt::fd try-job: aarch64-apple try-job: arm-android try-job: test-various
2 parents 8205e6b + dd4c41c commit bb5247b

File tree

4 files changed

+303
-0
lines changed

4 files changed

+303
-0
lines changed

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

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,94 @@ pub trait CommandExt: Sealed {
213213

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

218306
#[stable(feature = "rust1", since = "1.0.0")]
@@ -268,6 +356,15 @@ impl CommandExt for process::Command {
268356
self.as_inner_mut().setsid(setsid);
269357
self
270358
}
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+
}
364+
365+
fn last_spawn_was_posix_spawn(&self) -> Option<bool> {
366+
self.as_inner().get_last_spawn_was_posix_spawn()
367+
}
271368
}
272369

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

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

Lines changed: 28 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

@@ -360,6 +367,27 @@ impl Command {
360367
let theirs = ChildPipes { stdin: their_stdin, stdout: their_stdout, stderr: their_stderr };
361368
Ok((ours, theirs))
362369
}
370+
371+
pub fn fd(&mut self, old_fd: OwnedFd, new_fd: RawFd) {
372+
self.fds.push((old_fd, new_fd));
373+
}
374+
375+
pub fn get_fds(&self) -> &[(OwnedFd, RawFd)] {
376+
&self.fds
377+
}
378+
379+
/// Clear the fd vector, closing all descriptors owned by this `Command`.
380+
pub fn close_owned_fds(&mut self) {
381+
self.fds.clear();
382+
}
383+
384+
pub fn last_spawn_was_posix_spawn(&mut self, val: bool) {
385+
self.last_spawn_was_posix_spawn = Some(val);
386+
}
387+
388+
pub fn get_last_spawn_was_posix_spawn(&self) -> Option<bool> {
389+
self.last_spawn_was_posix_spawn
390+
}
363391
}
364392

365393
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_flag_used};
459473

460474
if self.get_gid().is_some()
@@ -717,6 +731,17 @@ impl Command {
717731
libc::STDERR_FILENO,
718732
))?;
719733
}
734+
for &(ref old_fd, new_fd) in self.get_fds() {
735+
cvt_nz(libc::posix_spawn_file_actions_adddup2(
736+
file_actions.0.as_mut_ptr(),
737+
old_fd.as_raw_fd(),
738+
new_fd,
739+
))?;
740+
cvt_nz(libc::posix_spawn_file_actions_addclose(
741+
file_actions.0.as_mut_ptr(),
742+
old_fd.as_raw_fd(),
743+
))?;
744+
}
720745
if let Some((f, cwd)) = addchdir {
721746
cvt_nz(f(file_actions.0.as_mut_ptr(), cwd.as_ptr()))?;
722747
}

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

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
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;
46

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

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

0 commit comments

Comments
 (0)