Repository navigation
add std::os::unix::process::CommandExt::fd #145687
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
756b94c
b3ff6bd
a32b7eb
bf24f0e
c5f935e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,10 +13,13 @@ use libc::{gid_t, uid_t}; | |
| use super::common::*; | ||
| use crate::io::{self, Error, ErrorKind}; | ||
| use crate::num::NonZero; | ||
| use crate::os::fd::AsRawFd; | ||
| #[cfg(not(any(target_os = "tvos", target_os = "watchos")))] | ||
| use crate::os::fd::OwnedFd; | ||
| use crate::process::StdioPipes; | ||
| use crate::sys::cvt; | ||
| #[cfg(target_os = "linux")] | ||
| use crate::sys::process::PidFd; | ||
| use crate::sys::{FromInner, IntoInner, cvt}; | ||
| use crate::{fmt, mem, sys}; | ||
|
|
||
| cfg_select! { | ||
|
|
@@ -71,8 +74,12 @@ impl Command { | |
| let (ours, theirs) = self.setup_io(default, needs_stdin)?; | ||
|
|
||
| if let Some(ret) = self.posix_spawn(&theirs, envp.as_ref())? { | ||
| self.last_spawn_was_posix_spawn(true); | ||
| // Close fds in the parent that have been duplicated in the child | ||
| self.close_owned_fds(); | ||
|
Qelxiros marked this conversation as resolved.
Resolved
|
||
| return Ok((ret, ours)); | ||
| } | ||
| self.last_spawn_was_posix_spawn(false); | ||
|
|
||
| #[cfg(target_os = "linux")] | ||
| let (input, output) = sys::net::Socket::new_pair(libc::AF_UNIX, libc::SOCK_SEQPACKET)?; | ||
|
|
@@ -101,7 +108,15 @@ impl Command { | |
| if self.get_create_pidfd() { | ||
| self.send_pidfd(&output); | ||
| } | ||
| let Err(err) = unsafe { self.do_exec(theirs, envp.as_ref()) }; | ||
| #[cfg(target_os = "linux")] | ||
| let mut output_fd = output.into_inner().into_inner(); | ||
| #[cfg(not(target_os = "linux"))] | ||
| let mut output_fd = output.into_inner(); | ||
| let Err(err) = unsafe { self.do_exec(theirs, envp.as_ref(), Some(&mut output_fd)) }; | ||
| #[cfg(target_os = "linux")] | ||
| let output = sys::net::Socket::from_inner(sys::fd::FileDesc::from_inner(output_fd)); | ||
| #[cfg(not(target_os = "linux"))] | ||
| let output = sys::fd::FileDesc::from_inner(output_fd); | ||
| let errno = err.raw_os_error().unwrap_or(libc::EINVAL) as u32; | ||
| let errno = errno.to_be_bytes(); | ||
| let bytes = [ | ||
|
|
@@ -124,6 +139,9 @@ impl Command { | |
| drop(env_lock); | ||
| drop(output); | ||
|
|
||
| // Close fds in the parent that have been duplicated in the child | ||
| self.close_owned_fds(); | ||
|
|
||
| #[cfg(target_os = "linux")] | ||
| let pidfd = if self.get_create_pidfd() { self.recv_pidfd(&input) } else { -1 }; | ||
|
|
||
|
|
@@ -241,7 +259,7 @@ impl Command { | |
| // environment lock before we try to exec. | ||
| let _lock = sys::env::env_read_lock(); | ||
|
|
||
| let Err(e) = self.do_exec(theirs, envp.as_ref()); | ||
| let Err(e) = self.do_exec(theirs, envp.as_ref(), None); | ||
| e | ||
| } | ||
| } | ||
|
|
@@ -284,6 +302,7 @@ impl Command { | |
| &mut self, | ||
| stdio: ChildPipes, | ||
| maybe_envp: Option<&CStringArray>, | ||
| mut preserve_fd: Option<&mut OwnedFd>, | ||
| ) -> Result<!, io::Error> { | ||
| use crate::sys::{self, cvt_r}; | ||
|
|
||
|
|
@@ -297,6 +316,22 @@ impl Command { | |
| cvt_r(|| libc::dup2(fd, libc::STDERR_FILENO))?; | ||
| } | ||
|
|
||
| for &(ref old_fd, new_fd) in self.get_fds() { | ||
| if let Some(ref mut preserve_fd) = preserve_fd | ||
| && new_fd == preserve_fd.as_raw_fd() | ||
| { | ||
| let mut copy = preserve_fd.try_clone()?; | ||
| mem::swap(&mut copy, preserve_fd); | ||
| // after the swap, copy holds the same file descriptor as new_fd, so we can let dup2 | ||
| // close it instead | ||
| mem::forget(copy); | ||
| } | ||
| cvt_r(|| libc::dup2(old_fd.as_raw_fd(), new_fd))?; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is problematic and probably unsound (as it violates I/O safety) if
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a way to check for that? If not, it seems impossible to avoid.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @joboet friendly nudge :)
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You could use
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe I don't understand the issue. If new_fd might collide with the pipe/socket, then it might still collide after I move the pipe/socket, right?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @joboet can you restate the concern? I don't think I'm quite seeing what you're getting at.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Prior to I think Jonas is saying this can be mitigated by doing something like pass You can actually Great catch Jonas.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see, yeah, that sounds like it's basically the same issue I'm referencing here - #145687 (comment) - i.e., we're not really satisfying I/O Safety here. I don't thinkt he current API allows for that though, even if we mitigate it for a small set of known FDs.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Was able to write a test that reliably fails because of this but passes with posix-spawn. @Qelxiros could you cherry pick tgross35@986dba3?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should probably mention something about CLOEXEC not being set by us somewhere in the docs.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nope that doesn't make sense since we haven't |
||
| if old_fd.as_raw_fd() != new_fd { | ||
| cvt_r(|| libc::close(old_fd.as_raw_fd()))?; | ||
| } | ||
| } | ||
|
|
||
| #[cfg(not(target_os = "l4re"))] | ||
| { | ||
| if let Some(_g) = self.get_groups() { | ||
|
|
@@ -462,6 +497,7 @@ impl Command { | |
| use core::sync::atomic::{Atomic, AtomicU8, Ordering}; | ||
|
|
||
| use crate::mem::MaybeUninit; | ||
| use crate::os::fd::AsRawFd; | ||
| use crate::pin::{Pin, pin}; | ||
| use crate::sys::helpers::COpaque; | ||
| use crate::sys::{self, cvt_nz, on_broken_pipe_used}; | ||
|
|
@@ -734,6 +770,19 @@ impl Command { | |
| libc::STDERR_FILENO, | ||
| ))?; | ||
| } | ||
| for &(ref old_fd, new_fd) in self.get_fds() { | ||
| cvt_nz(libc::posix_spawn_file_actions_adddup2( | ||
| file_actions.0.get(), | ||
| old_fd.as_raw_fd(), | ||
| new_fd, | ||
| ))?; | ||
| if old_fd.as_raw_fd() != new_fd { | ||
| cvt_nz(libc::posix_spawn_file_actions_addclose( | ||
| file_actions.0.get(), | ||
| old_fd.as_raw_fd(), | ||
| ))?; | ||
| } | ||
| } | ||
| if let Some((f, cwd)) = addchdir { | ||
| cvt_nz(f(file_actions.0.get(), cwd.as_ptr()))?; | ||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure what "file descriptor will be lost" means -- losing an integer is not really a thing, we should say something about closing or not mapping certain fds. I think the semantics we intend is something like:
I think what @joboet is getting at in the other thread is that this is inherently in conflict with I/O safety, which states
Here we are closing arbitrary target fds. My sense is that this API is not really usable as-is. I think the only possible API shape is for the caller to pre-allocate the target FDs for us, e.g., by passing old_fd: OwnedFd, new_fd: OwnedFd; we would then dup2 into the new_fds safely. Or am I missing something?
View changes since the review
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm: I think this may be safe since we're duping in the child so the parent's originals remain untouched. And afaict we're not claiming exclusive ownership of the FDs except for the
output.This would be a problem if e.g. you hand the child a list of "safe" FDs that it can assume
from_raw_fdon, without realizing that it contains duplicates because some non-cloexec FD happens to overlap with a value set in.fd. Perhaps it's worth a note that the child is still responsible for ensuring it only opens one of each FD?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It’s a good point that the damage/risk is mostly (only? not sure if there are kernel APIs that act on any close - iirc, there are bad file locking APIs that unlock on any close) in the child.
But it feels like safe rust code being able to close arbitrary fds in the child is at least border line violating the expected semantics from I/O safety. To some extent that is unavoidable with the APIs present in Linux, but it still seems iffy. I think ideally we would allow dup(fd) semantics with some Rust protocol for retrieving them soundly in the child as owned…
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I certainly don’t disagree, sending numbers around like this is a rather low-level API.
Do you think there’s a good resolution here? Maybe the “It is recommended to provide further information to the child by some other mechanism” bit could be made stronger and put in its own section with examples. And/or an unresolved question on the tracking issue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think unresolved question is probably the right thing. In the short term we could make this unsafe, though I’m not sure what the exact safety condition should be (and how the caller can guarantee it), especially given that the fd table is inherently a global property. Maybe the target fd should be mandated to be an OwnedFd as I suggest above? That’s a bit wasteful but does seem like it satisfies the requirement that you’re not clobbering random fds…
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Started #t-libs > Safety of sending file descriptors since the others may have thoughts on this.