Skip to content
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

nix 0.27 Epoll API should be marked unsafe #2115

Open
dimbleby opened this issue Aug 28, 2023 · 13 comments
Open

nix 0.27 Epoll API should be marked unsafe #2115

dimbleby opened this issue Aug 28, 2023 · 13 comments

Comments

@dimbleby
Copy link

dimbleby commented Aug 28, 2023

The new Epoll API (as at nix 0.27) should be marked unsafe, probably on Epoll::add().

Currently the API has no unsafe on it, uses types from the io-safety proposal - but is in fact unsafe. Because one can add a file descriptor to an Epoll's set of interests, and then drop that file descriptor - which is just what io-safety was trying to prevent.

Useful discussion starts at #2115 (comment).

(Original report preserved below)


I'm interacting with a C library so I have a RawFd all along.

Previously epoll_ctl() etc took a RawFd so that was very convenient. But the new API takes Fd: AsFd which is not so convenient: I have to unsafely borrow my RawFd; and then nix is only going to .as_raw_fd() it again anyway. It's an awkward and unnecessary dance.

Fd: AsRawFd might make more sense, I reckon that's what is actually needed?

#1882, cc @JonathanWoollett-Light

@dimbleby dimbleby changed the title is BorrowedFd the write API for Epoll::add() etc? is Fd: AsFd the write API for Epoll::add() etc? Aug 28, 2023
@JonathanWoollett-Light
Copy link
Contributor

JonathanWoollett-Light commented Aug 28, 2023

There is an explanation behind the purpose of OwnedFd and BorrowedFd here https://rust-lang.github.io/rfcs/3128-io-safety.html.

Nix is moving to this approach, it is unfortunately slow however which can introduce inconsistency that makes it a bit awkward.

@dimbleby dimbleby changed the title is Fd: AsFd the write API for Epoll::add() etc? is Fd: AsFd the right API for Epoll::add() etc? Aug 28, 2023
@dimbleby
Copy link
Author

hmm, it's all a bit awkward.

cf smol-rs/polling#123 in which polling seems to have gone with a mixture of:

  • use RawFd in some places, but acknowledge that it's unsafe
  • use BorrowedFd in other places

I think that's probably more principled. Unless I misunderstand, this crate is currently exposing a purportedly safe API - but callers still can add file descriptors to an Epoll and close and reuse those file descriptors while the Epoll retains an interest. Which is pretty much what the io_safety stuff is trying to avoid, right?

Is that true?

@JonathanWoollett-Light
Copy link
Contributor

JonathanWoollett-Light commented Aug 28, 2023

Due to limitations in Rust's compile time evaluation I do not beleive it is currently possible to implement Epoll in this form fully safely (this would be possible with something like Zig's comptime).

It is true you can add a file descriptor with Epoll::Add then close it before closing the epoll or removing it from the interest list. You are correct I agree, this is unsafe, this should be better documented.

I am not sure what code changes would make sense here to better illustrate this.

@dimbleby
Copy link
Author

dimbleby commented Aug 28, 2023

I don't think it's only a documentation thing: it's unsafe but the API has no unsafe on it.

The suggestion - via polling - is that Epoll::add() be unsafe and accept a RawFd.

That would be the place to put the documentation saying that callers are responsible for making sure that they Epoll::delete() the file descriptor before dropping it.

@JonathanWoollett-Light
Copy link
Contributor

JonathanWoollett-Light commented Aug 28, 2023

The suggestion - via polling - is that Epoll::add() be unsafe and accept a RawFd.

I would suggest using AsRawFd so its more ergonomic, this way you could still call it with a reference to an OwnedFd.

I do not disagree with adding unsafe to it.

That would be the place to put the documentation saying that callers are responsible for making sure that the file descriptor stays alive up until the point that they Epoll::delete() it.

I do not think this actually matters. If you attempt to de-register a file descriptor that isn't registered it is safe. It will error, but it is safe.

The safety concern can be written

You must gurantee that the file descriptor given to Epoll::add is de-registered with Epoll::del or the Epoll is dropped before this file descriptor is dropped.

@dimbleby
Copy link
Author

cool, I think we're agreeing.

I don't intend to submit an MR, but I'll update the title and thread-opener to clarify where we've got to.

@JonathanWoollett-Light
Copy link
Contributor

With your update

That would be the place to put the documentation saying that callers are responsible for making sure that they Epoll::delete() the file descriptor before dropping it.

They do not need to call Epoll::delete if they just drop the epoll itself.

@dimbleby dimbleby changed the title is Fd: AsFd the right API for Epoll::add() etc? nix 0.27 Epoll API should be marked unsafe Aug 28, 2023
@asomers
Copy link
Member

asomers commented Sep 29, 2023

I think that Epoll::add can still be safe, as long as the Epoll captures the lifetime of the file descriptors that it's adding. There might still be use-cases where the Epoll must be long-lived even though the files it polls aren't. For those cases, we'll have to bypass I/O safety. I suggest an unsafe add_raw method. Finally, as discussed in #2134, no method should be taking an AsFd except by reference. So it would be something like this:

struct Epoll<'fd> {
    /// The actual epoll file descriptor
    pollfd: OwnedFd,
    PhantomData<'fd>
}
impl<'fd> Epoll {
    pub fn add<Fd: AsFd + 'fd>(&self, fd: &Fd) -> Result<()> {...}
    pub unsafe fn add_raw(&self, fd: RawFd) -> Result<()> {...}
}

@dimbleby
Copy link
Author

I think that Epoll::add can still be safe, as long as the Epoll captures the lifetime of the file descriptors that it's adding.

I reckon that just doesn't match how Epoll is usually used.

Certainly in my code - but, I claim, this is the typical and intended use - the pattern is a long-lived Epoll and lots of file descriptors coming and going over the course of a program's runtime. ie I think you'll find that your add_raw() is almost always the API that people want: and in that case the lifetime-aware add() isn't worth the trouble.

The corresponding analysis in polling starts at smol-rs/polling#38 (comment), it might be interesting to compare notes.

@asomers
Copy link
Member

asomers commented Sep 29, 2023

There are definitely APIs which can be used with I/O Safety, but in their most compelling use cases require the BorrowedFds to be 'static. I've run into that myself, with the POSIX AIO stuff. In cases like this, I'm not sure what the best option is:

  • Ignore I/O safety and operate on raw FDs only.
  • Ignore I/O safety and operate on raw FDs only, but add I/O safety via some other mechanism at a higher level in some other crate.
  • Add I/O safe methods of limited usefulness, plus unsafe methods that take raw file descriptors.
  • Add I/O safety by using runtime checks, for example a bunch of Arc<OwnedFd>. This is my least favorite option.

@SteveLauC
Copy link
Member

#2232 will probably fix this issue

@dimbleby
Copy link
Author

... but does it also make Epoll unusable in this typical usage #2115 (comment)?

@SteveLauC
Copy link
Member

... but does it also make Epoll unusable in this typical usage #2115 (comment)?

Well, I guess so.

And unfortunately, even though you remove a fd from the Epoll with .remove(), the borrow checker would consider this as anothe borrow:

use std::fs::File;
use std::marker::PhantomData;
use std::os::fd::{AsFd, BorrowedFd};

struct FdSet<'a> {
    _marker: PhantomData<*mut &'a ()>,
}

impl<'a> FdSet<'a> {
    fn new() -> Self {
        Self {
            _marker: PhantomData
        }
    }
    fn add(&self, fd: BorrowedFd<'a>) {}

    fn remove(&self, fd: BorrowedFd<'a>) {}
}

fn main() {
    let set = FdSet::new();
    let file = File::open("Cargo.toml").unwrap();
    set.add(file.as_fd());
    set.remove(file.as_fd());


    // It should be safe to drop the file here
    drop(file);
    drop(set);
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants