Skip to content

Commit fe30064

Browse files
committed
add std::os::unix::process::CommandExt::fd
1 parent bbcbc78 commit fe30064

File tree

4 files changed

+263
-1
lines changed

4 files changed

+263
-1
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: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ pub struct Command {
103103
create_pidfd: bool,
104104
pgroup: Option<pid_t>,
105105
setsid: bool,
106+
fds: Vec<(OwnedFd, RawFd)>,
107+
last_spawn_was_posix_spawn: Option<bool>,
106108
}
107109

108110
// passed to do_exec() with configuration of what the child stdio should look
@@ -183,6 +185,8 @@ impl Command {
183185
create_pidfd: false,
184186
pgroup: None,
185187
setsid: false,
188+
fds: Vec::new(),
189+
last_spawn_was_posix_spawn: None,
186190
}
187191
}
188192

@@ -360,6 +364,26 @@ impl Command {
360364
let theirs = ChildPipes { stdin: their_stdin, stdout: their_stdout, stderr: their_stderr };
361365
Ok((ours, theirs))
362366
}
367+
368+
pub fn fd(&mut self, old_fd: OwnedFd, new_fd: RawFd) {
369+
self.fds.push((old_fd, new_fd));
370+
}
371+
372+
pub fn get_fds(&self) -> &[(OwnedFd, RawFd)] {
373+
&self.fds
374+
}
375+
376+
pub fn close_owned_fds(&mut self) {
377+
self.fds.clear();
378+
}
379+
380+
pub fn last_spawn_was_posix_spawn(&mut self, val: bool) {
381+
self.last_spawn_was_posix_spawn = Some(val);
382+
}
383+
384+
pub fn get_last_spawn_was_posix_spawn(&self) -> Option<bool> {
385+
self.last_spawn_was_posix_spawn
386+
}
363387
}
364388

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

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

Lines changed: 24 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,11 @@ 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+
self.close_owned_fds();
7477
return Ok((ret, ours));
7578
}
79+
self.last_spawn_was_posix_spawn(false);
7680

7781
#[cfg(target_os = "linux")]
7882
let (input, output) = sys::net::Socket::new_pair(libc::AF_UNIX, libc::SOCK_SEQPACKET)?;
@@ -124,6 +128,8 @@ impl Command {
124128
drop(env_lock);
125129
drop(output);
126130

131+
self.close_owned_fds();
132+
127133
#[cfg(target_os = "linux")]
128134
let pidfd = if self.get_create_pidfd() { self.recv_pidfd(&input) } else { -1 };
129135

@@ -292,6 +298,11 @@ impl Command {
292298
cvt_r(|| libc::dup2(fd, libc::STDERR_FILENO))?;
293299
}
294300

301+
for &(ref old_fd, new_fd) in self.get_fds() {
302+
cvt_r(|| libc::dup2(old_fd.as_raw_fd(), new_fd))?;
303+
cvt_r(|| libc::close(old_fd.as_raw_fd()))?;
304+
}
305+
295306
#[cfg(not(target_os = "l4re"))]
296307
{
297308
if let Some(_g) = self.get_groups() {
@@ -717,6 +728,19 @@ impl Command {
717728
libc::STDERR_FILENO,
718729
))?;
719730
}
731+
for &(ref old_fd, new_fd) in self.get_fds() {
732+
use crate::os::fd::AsRawFd;
733+
734+
cvt_nz(libc::posix_spawn_file_actions_adddup2(
735+
file_actions.0.as_mut_ptr(),
736+
old_fd.as_raw_fd(),
737+
new_fd,
738+
))?;
739+
cvt_nz(libc::posix_spawn_file_actions_addclose(
740+
file_actions.0.as_mut_ptr(),
741+
old_fd.as_raw_fd(),
742+
))?;
743+
}
720744
if let Some((f, cwd)) = addchdir {
721745
cvt_nz(f(file_actions.0.as_mut_ptr(), cwd.as_ptr()))?;
722746
}

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

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
1+
use crate::fs;
2+
use crate::io::{self, Write};
3+
use crate::os::unix::fs::MetadataExt;
4+
use crate::os::unix::io::AsRawFd;
15
use crate::os::unix::process::{CommandExt, ExitStatusExt};
26
use crate::panic::catch_unwind;
3-
use crate::process::Command;
7+
use crate::process::{Command, Stdio};
48

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

12+
/// Use dev + ino to uniquely identify a file
13+
fn md_file_id(md: &fs::Metadata) -> (u64, u64) {
14+
(md.dev(), md.ino())
15+
}
16+
817
#[test]
918
fn exitstatus_display_tests() {
1019
// In practice this is the same on every Unix.
@@ -74,3 +83,111 @@ fn test_command_fork_no_unwind() {
7483
|| signal == libc::SIGSEGV
7584
);
7685
}
86+
87+
fn fd_test_stdin(use_exec: bool) {
88+
let (pipe_reader, mut pipe_writer) = io::pipe().unwrap();
89+
90+
let fd_num = libc::STDIN_FILENO;
91+
92+
let mut cmd = Command::new("cat");
93+
cmd.stdout(Stdio::piped()).fd(fd_num, pipe_reader);
94+
95+
if use_exec {
96+
unsafe {
97+
cmd.pre_exec(|| Ok(()));
98+
}
99+
}
100+
101+
let mut child = cmd.spawn().unwrap();
102+
let mut stdout = child.stdout.take().unwrap();
103+
104+
assert_ne!(cmd.last_spawn_was_posix_spawn().unwrap_or(false), use_exec);
105+
106+
pipe_writer.write_all(b"Hello, world!").unwrap();
107+
drop(pipe_writer);
108+
109+
child.wait().unwrap();
110+
assert_eq!(io::read_to_string(&mut stdout).unwrap(), "Hello, world!");
111+
}
112+
113+
fn fd_test_swap(use_exec: bool) {
114+
let (pipe_reader1, mut pipe_writer1) = io::pipe().unwrap();
115+
let (pipe_reader2, mut pipe_writer2) = io::pipe().unwrap();
116+
117+
let num1 = pipe_reader1.as_raw_fd();
118+
let num2 = pipe_reader2.as_raw_fd();
119+
120+
let mut cmd = Command::new("cat");
121+
cmd.arg(format!("/dev/fd/{num1}"))
122+
.arg(format!("/dev/fd/{num2}"))
123+
.stdout(Stdio::piped())
124+
.fd(num2, pipe_reader1)
125+
.fd(num1, pipe_reader2);
126+
127+
if use_exec {
128+
unsafe {
129+
cmd.pre_exec(|| Ok(()));
130+
}
131+
}
132+
133+
pipe_writer1.write_all(b"Hello from pipe 1!").unwrap();
134+
drop(pipe_writer1);
135+
136+
pipe_writer2.write_all(b"Hello from pipe 2!").unwrap();
137+
drop(pipe_writer2);
138+
139+
let mut child = cmd.spawn().unwrap();
140+
let mut stdout = child.stdout.take().unwrap();
141+
142+
assert_ne!(cmd.last_spawn_was_posix_spawn().unwrap_or(false), use_exec);
143+
144+
child.wait().unwrap();
145+
// the second pipe's output is clobbered; this is expected.
146+
assert_eq!(io::read_to_string(&mut stdout).unwrap(), "Hello from pipe 1!");
147+
}
148+
149+
// ensure that the fd is properly closed in the parent, but only after the child is spawned.
150+
fn fd_test_close_time(use_exec: bool) {
151+
let (_pipe_reader, pipe_writer) = io::pipe().unwrap();
152+
153+
let fd = pipe_writer.as_raw_fd();
154+
let fd_path = format!("/dev/fd/{fd}");
155+
156+
let mut cmd = Command::new("true");
157+
cmd.fd(123, pipe_writer);
158+
159+
if use_exec {
160+
unsafe {
161+
cmd.pre_exec(|| Ok(()));
162+
}
163+
}
164+
165+
// Get the identifier of the fd (metadata follows symlinks)
166+
let fd_id = md_file_id(&fs::metadata(&fd_path).expect("fd should be open"));
167+
168+
cmd.spawn().unwrap().wait().unwrap();
169+
170+
assert_ne!(cmd.last_spawn_was_posix_spawn().unwrap_or(false), use_exec);
171+
172+
// After the child is spawned, our fd should be closed
173+
match fs::metadata(&fd_path) {
174+
// Ok; fd exists but points to a different file
175+
Ok(md) => assert_ne!(md_file_id(&md), fd_id),
176+
// Ok; fd does not exist
177+
Err(_) => (),
178+
}
179+
}
180+
181+
#[test]
182+
fn fd_tests_posix_spawn() {
183+
fd_test_stdin(false);
184+
fd_test_swap(false);
185+
fd_test_close_time(false);
186+
}
187+
188+
#[test]
189+
fn fd_tests_exec() {
190+
fd_test_stdin(true);
191+
fd_test_swap(true);
192+
fd_test_close_time(true);
193+
}

0 commit comments

Comments
 (0)