-
Notifications
You must be signed in to change notification settings - Fork 42
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
Fix IO safety in unix.rs
#101
base: main
Are you sure you want to change the base?
Conversation
49877d1
to
40cffa2
Compare
So the behaviour of spawning a configured command after the (not-inherited) jobserver
|
src/unix.rs
Outdated
-1 => { | ||
let err = io::Error::last_os_error(); | ||
if err.raw_os_error() == Some(libc::ENOSYS) { | ||
PIPE2_AVAILABLE.store(false, Ordering::SeqCst); |
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 Relaxed
here is definitely enough for store and load
PIPE2_AVAILABLE.store(false, Ordering::SeqCst); | |
PIPE2_AVAILABLE.store(false, Ordering::Relaxed); |
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.
Agreed
src/unix.rs
Outdated
return Err(err); | ||
unsafe { | ||
let mut pipes = [0; 2]; | ||
match libc::syscall(libc::SYS_pipe2, pipes.as_mut_ptr(), libc::O_CLOEXEC) { |
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.
Could use cvt
here and match on the error:
match libc::syscall(libc::SYS_pipe2, pipes.as_mut_ptr(), libc::O_CLOEXEC) { | |
match cvt(libc::syscall(libc::SYS_pipe2, pipes.as_mut_ptr(), libc::O_CLOEXEC)) { |
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.
Unfortunately libc::syscall
returns a c_long
whereas cvt
takes a c_int
.
src/unix.rs
Outdated
drop(set_cloexec(read.as_fd(), true)); | ||
drop(set_cloexec(write.as_fd(), true)); |
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 the error should be propagated instead of ignored:
drop(set_cloexec(read.as_fd(), true)); | |
drop(set_cloexec(write.as_fd(), true)); | |
set_cloexec(read.as_fd(), true)?; | |
set_cloexec(write.as_fd(), true)?; |
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.
Done
src/unix.rs
Outdated
PIPE2_AVAILABLE.store(false, Ordering::SeqCst); | ||
} else { | ||
return Err(err); | ||
unsafe { |
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.
My two cent is that smaller unsafe
block is better, as it makes it easier to read
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've shrunk it further.
tests/spawn-after-drop.rs
Outdated
|
||
use jobserver::{Client, FromEnvErrorKind}; | ||
|
||
macro_rules! t { |
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 don't think we need t!
anymore, it's probably used in times where .unwrap()
isn't available
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.
Replaced with .unwrap()
.
// Keep a reference to the jobserver alive in the closure so that | ||
// the pipe FDs aren't closed, otherwise `set_cloexec` might end up | ||
// targetting a completely unrelated file descriptor. | ||
let arc = self.clone(); |
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.
This is awesome, now it would always work once Client::configure
is called
let client = unsafe { | ||
t!(match Client::from_env_ext(true).client { | ||
// Its ok for a dropped jobservers path to no longer exist (e.g. on Windows). | ||
Err(e) if matches!(e.kind(), FromEnvErrorKind::CannotOpenPath) => return, |
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.
IMHO it's kind of a bad behavior that it has different behavior on different platform.
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.
Unfortunately having the same behaviour would be difficult:
- On Windows, the child process doesn't have any OS-level reference to the jobserver until it opens the semaphore, so if the parent process
Client
is dropped beforeClient::from_env_ext
is called thenClient::from_env_ext
will fail as semaphores are automatically deleted when there are no handles referencing them. - On Linux (when using pipes, which is what jobserver-rs currently does when creating it's own jobserver) the opposite is true: as long as the FDs are alive when the process is spawned (which this PR ensures) then
Client::from_env_ext
will succeed in the child process even if the parent processClient
is dropped before the client process calls it.
The only possibility that comes to mind would be if on Windows the parent process lets the child process inherit a copy of the semaphore handle; this would mean that the semaphore would stick around for as long as that process was running. The difficult bit is ensuring only the desired processes inherit the handle and making sure the handle isn't closed before the command is spawned. This would require stdlib support, possibly involving PROC_THREAD_ATTRIBUTE_HANDLE_LIST
and rust-lang/rust#114854.
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.
@ChrisDenton, one more case to start supporting this.
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 am happy to collaborate would @beetrees on this to stop processes from inheriting handles.
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.
Check out rust-lang/rust#115501.
(I've also fixed an unused import warning in the |
That's unfortunate since windows don't have I have a fork of jobserver, which I workaround this issue by forcing user to pass in a closure and spawn the process there pub fn configure_and_run<Cmd, F, R>(&self, cmd: Cmd, f: F) -> Result<R>
where
Cmd: Command,
F: FnOnce(&mut Cmd) -> Result<R>;
I was planning to open a PR to optimize away the Though that'd mean the jobserver would be inherited without |
As well as using
BorrowedFd
as opposed toc_int
in more places, this PR fixes two related bugs:Client::configure()
wouldn't ensure the file descriptors passed toCommand::configure
remained valid until the command was actually spawned. This meant that if theClient
was dropped before theCommand
was spawned, theset_cloexec
call could end up hitting an unrelated file descriptor (which the spawned process would then treat as a jobserver).string_arg
uses the FDs fromClientCreationArg::Fds
to put in the environment variables, but before this PRClient::configure
would use the FDs fromself.read
/self.write
instead. This would result in the wrong FDs getting passed toset_cloexec
. This was basically harmless (apart from having the use-after-drop problem) as the only time the two sets of FDs differ is when the jobserver has been inherited from the environment, in which case the FDs are going to be not-CLOEXEC anyway.I've also removed the
unsafe
from the definitions ofClient::mk
andClient::from_fds
as they don't appear to have any safety requirements.