-
Notifications
You must be signed in to change notification settings - Fork 12.8k
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
I/O safety forbids the "pass FD via env var" pattern (e.g., jobserver) #116059
Comments
Well, we could promote more sane protocols instead such as named fifos, file-descriptor passing via unix sockets or also putting the dev/ino in the environment variable to enable sanity checks in the child. Then the sane ways are safe and the legacy approaches are unsafe. |
I'm kind of assuming that this would take way too long to really prevent us from having to come up with a justification for what happens currently. Like, realistically, when could (And that's assuming that someone will actually spend all the effort of doing that, and they will be able to convince the rest of the ecosystem to adopt the new approach. In particular the second point I am doubtful about.) |
The new jobserver protocol already exists, we didn't even have to invent it. We just need to works towards making it the default (yes, that'll take time)
Well, we can be pragmatic there and treat it like before_exec which should be |
If anyone has a concrete proposal for |
This comment was marked as off-topic.
This comment was marked as off-topic.
A good starting point would be taking a look at this PR: rust-lang/jobserver-rs#57 |
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
I'm still pretty fond of the idea of having |
But that's a lot of steps to make that work and file-descriptor-enumeration is kinda shaky as a cross-unix feature - e.g. procfs isn't part of POSIX and manually enumerating FDs at startup would be costly - and probably unsupportable for windows handles. |
I'd be more optimistic about this plan if it was just jobserver, but systemd socket activation uses a similar protocol AFAIK, and that's generally considered a "modern" platform. Is there even an alternative to FD passing there? |
systemd's C function for this (sd_listen_fds) at least checks the PID of the current process against those environment variables so this doesn't get foisted on entire process hierarchies and they won't go out of sync , which makes it a bit better. In Rust terms it would still have to be We could ask the systemd devs whether they'd be willing to add another environment variable listing the inode numbers to increase verifiability. Though I suppose there's still some conflict between the static-borrow vs. the owned usage model of those descriptors. A registry could arbitrate that... |
I think in general we actually have to solve the problem, either by changing the model in some way, defining it as outside of Rust's scope, or just accepting willful unsoundness. There's a lot of crates that don't think about this, for example https://lib.rs/crates/sd-listen-fds, lots of old (and even new) APIs and protocols that just pass FDs to a number in an environment variable (or even just choose something arbitrary like 3). We can try and solve the ecosystem issues that we're facing, but we do realistically have to accept that we're not going to change decades of Unix tradition and see how we're going to fit in with this.
This is not really portable in a practical way - I suppose you can have pre-main code check |
The std-listen-fds crate does several questionable things. Afaict it allows you to call the method any number of times and yet each time it returns an I don't think poorly written unsafe code is a good justification for just throwing up our arms and giving up. |
How are you imaging this would work? |
|
Assuming a maximum FD number has historically turned out badly for
There is no way to take a snapshot of all FDs that have existed at program startup, only a way to enumerate the currently open FDs - which may sound like nitpicking, but enumeration would also pick up FDs that the libc implementation might be temporarily using in another thread it created, or other internal FDs that happened to be open at the time, and The environment variable itself is just external untrusted input like any other input - whether I read the FD number from an environment variable or a file doesn't change its value. And a table wouldn't help against all forms of invalid input anyway - say I tell a program to use the FD |
That's not entirely the same situation, since we are only bounding the maximum FD number present at program startup. Also, I'm not saying the solution is perfect. There are no great solutions here that I can see. We're merely looking for the least-bad solution. Is bounding the number to 1024 (or 64k or whatever we will pick) -- and only for cases where
Yes, libc having other FDs open is a potential problem. We'd still get a hard guarantee on libc's that don't do that, and we could work with libc's that do do that to be able to distinguish their internal FDs from "primordial" FDs present at program startup. Do you know any libc that does things like that? (The libc opening and closing files before jumping to the Rust entry point would be fine. Only having any files still open, including in other threads, would be an issue.)
The table will help here, without
You have misunderstood my proposal, this is not part of it. |
@RalfJung A problem with that registry design is that if Rust code be linked with C libraries, the C libraries could claim ownership of some of the incoming fds and close them, without expecting that it needs to notify Rust's std. I'd like to probe approach (1) more, exporting the safety burden to the environment. The informal reasoning people use in practice is something like "the name To call |
Just to be clear, what this means is we are deciding that we are okay with UB and soundness bugs when the environment (or other parts of the program) sets these env vars the wrong way. We are not expecting Rust code to be resilient against such environment issues. This does leave me quite uneasy. |
The parent process could have made a copy of the executable it is going to spawn, modify it in a way that will cause UB and then execute this copy. Or it could ptrace it even on systems that restrict arbitrary ptracing through the yama LSM ptrace_scope option (default on Ubuntu I believe). In other words you already have to trust the parent process. |
You are comparing scenarios that are not even remotely comparable. "It's UB if you modify the binary" and "it's UB if some environment variable has a wrong value" are not even in the same league, those scenarios are solar systems apart. Setting an environment variable is a perfectly normal every-day thing for everyone using the shell or spawning processes. Modifying the executable image is orders of magnitude less common. Same for attaching a tracer that actually alters program behavior. Nobody will be surprised if you can cause UB by putting a program into gdb and just changing some variables. Many people (myself included) will be surprised if you can cause UB by running |
Yeah, it's not great that an environment variable can cause UB in a program, but the environment itself is a hostile places for Rust programs, so I'm not sure where the line is (hence the opsem proposal just opened). I will note we have a classic trilemma here: safe abstractions to There are potentially other solutions that get us out of the trilemma, but I am deeply skeptical of any of them actually working for Rust users, but more importantly not becoming a landmine for FFI ala Footnotes |
This isn't about a hostile environment though. It's possible to accidentally let the file descriptors and the environment variables get out of sync within a process hierarchy. All it takes is a single process in the process hierarchy that closes or renumbers the descriptors at some point before spawning a child but doesn't change the environment for the child to be faced with potentially-UB-causing inputs. I think I somewhere saw a jobserver-related issue reporting that this actually happened in the wild. I'd put mmap and fd-passing in different categories. mmap is a standardized OS-provided primitive which has some fundamental prerequisites to be used safely and while those prerequisites are hard to check or enforce strictly they're quite reasonable and also required for correctness and robustness of many other things. Shaping rules around that makes sense. Fd-passing on the other hand is a landscape of possible protocols. Which provides the flexibility of drawing a line around some areas in that landscape and saying "these are I don't see how saying that something is |
Yes, exporting the safety burden to the environment is not great. But, it's How Unix Has Always Worked and it doesn't add any costs, limitations, or opinions to every program, whether it wants them or not. I'd also like to bring up the idea of a structured environment-variable API for consideration. Perhaps such an API could look something like this: #[derive(IncomingValues)]
struct Config {
#[value(parse_var = from_raw_decimal_fd, var = "SOME_FD", pid_var = "SOME_PID")]
file: File,
}
fn main(config: Config) {
...
} or in a lib.rs: static CONFIG: Config = incoming_values!(); I don't have a complete design to propose, but the idea is: a design in this direction has the potential to completely encapsulate the safety burden. Any costs, limitations, or opinions it has could be opt-in. It could be part of a broader plan to make If we think of that as the path forward, then perhaps it's easier to see how "export the safety burden to the environment" makes sense to do here now, because that's no longer the last word on the subject. |
Having every program written in C and having Undefined Behavior left and right is How Unix Has Always Worked, so I am not persuaded by that argument. The entire raison d`être of Rust is to raise the bar for the level of safety in systems programming. Allowing programs to be UB when an env var has the wrong integer in it seems like giving up on a notable chunk of that goal. |
@RalfJung Could I ask you to share your thoughts on the rest of my post as well? |
I didn't entirely understand the API you were proposing. What are the underlying semantics, which operations are safe, what are the soundness promises and assumptions? Is the idea that instead if a table of "primordial" FDs, we have some declaration for which primordial FDs we are interested in and then std will hand them to us? Potential problems with this:
|
Yes, the idea is that all operations would be safe. And there may be other ways to arrange the lib.rs use case. But also, yes, a more complete design for this feature would imply arbitrary parsing code running before main. That's a problem I hadn't anticipated. I don't yet have any ideas for how to avoid that. On the other hand, if we go with the fd registry, I don't know how we could avoid breaking C libraries and existing Rust code that claims incoming fds without coordinating with the registry. |
Your approach doesn't really solve that either, does it? Existing Rust and external C code can still just claim an FD that was also passed in via The major problems of the registry of primordial FDs that I can see are:
The second point could possibly be avoided by taking a hint from your proposal: instead of having access to the FD registry via global functions in |
I might have missed this part of the discussion, but I don't understand how the behavior of that function can be considered safe or sound if it behaves as described in this thread. Isn't the only possible solution that satisfies Rust safety goals to change the In other words, a bare This isn't a matter of being "technically" unsound, it's a quite significant soundness bug in |
FYI, It's We could, introduce a new function to I'm a bit reluctant to adding a function to provide the |
In #114780 we have clarified what is meany by I/O safety. In particular:
This is unfortunately in conflict with a reasonably common pattern on Unix systems:
exec
ing a process with a file descriptor initialized somewhere, and setting an env var to tell it where to find the FD. This pattern is used, for instance, by the jobserver protocol. The jobserver crate hence just takes an integer from an env var, turns it into a file descriptor, and reads/writes to that -- a violation of I/O safety. Crates likecc
hence are technically unsound when they expose a safe function that internally uses the jobserver crate. The potential concern here is that the env var might be wrong, the jobserver crate now acts on a random FD by someone else, and that someone might have relied on I/O safety to protect their FD from foreign influence.Assuming that we don't want to tell the
cc
maintainer thatcc::Build::new()
must be unsafe, what shall we do about this?There's been a lot of prior discussion:
dup
ing arbitrary file descriptors? #114167I'm aware of the following proposals to resolve this situation:
cc
and setting the env var the wrong way is violating a precondition of this process, and Rust's soundness guarantees do not apply. This is undermined byset_var
though; even if we make thatunsafe
we'd have to phrase its safety contract very carefully if it want to go this route.cc
/jobserver
could use to be sure that the FD they work on already existed when the program started, and is not some other crate's private property. See here for a bit of elaboration on that idea.We should pick one, or develop some alternative that provides a satisfying answer to how
cc::Build::new()
can be a sound function.The text was updated successfully, but these errors were encountered: