You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
SockRef::from, Socket::sendfile and other functions that operate on arbitrary file descriptors or SOCKETs potentially should be unsafe #218
Here, the safe SockRef::from allows to crash a (fake) "external library" by letting it dereference a null pointer using only safe code. Making functions that accept arbitrary file descriptors or SOCKETs unsafe can probably solve this problem.
I have no idea what you mean with private, but if you mean shouldn't be modified by external (outside of the type's impls) that it should not implement AsRawFd/AsRawSocket. If it does implement AsRawFd/AsRawSocket that it must be able to deal with the situation it's actually used.
Here, the safe SockRef::from allows to crash a (fake) "external library" by letting it dereference a null pointer using only safe code. Making functions that accept arbitrary file descriptors or SOCKETs unsafe can probably solve this problem.
The SocketRef::from(&3) should be unsafe, but not because SocketRef::from should be unsafe. Because you're creating (implicitly) a RawFd which should be valid, but isn't in this example, that is the unsafe part.
Furthermore you're dereferencing a raw pointer using unsafe, so you're clearing not only using safe code. Putting code in an "external library" doesn't mean it shouldn't count towards unsafe code, it still is.
So, I do agree that creating RawFd from literals, especially when passing them directly to function as you did in the example should be unsafe or at least clearer. But I don't agree SocketRef::from is unsafe, or should be marked as such.
It turns out that RawFd itself implementsAsRawFd. So AsRawFd doesn't say anything about the validity or ownership of the returned RawFd value.
rust-lang/rust#72175 is still being discussed, and looks like it may lead to an RFC. If that happens, and if the RFC is accepted, functions that operate on RawFd values should be unsafe. That includes SockRef::from in its current form, since it accepts any type that implements AsRawFd which includes RawFd itself.
That said, SockRef::from is a valuable use case, and ideally it should be possible to do what it's doing without unsafe in its API. As one possible approach, the unsafe-io crate has an OwnsRaw trait which types that implement AsRawFd etc. can implement to additionally state that they really do own their handles, and traits like AsUnsafeSocket type which combine OwnsRaw and AsRawFd (AsRawSocket on Windows). With these, it should be possible to have fn SockRef::from(t: &T) where T: AsUnsafeSocket as a safe API. (Aside: unsafe-io is a new crate, and I'm interested if anyone has ideas for how to improve the API!)
But of course, it makes sense to wait and see how the discussion in rust-lang/rust#72175 and the possible RFC turn out before making major changes here.
I'm working on a draft of a proposal to add new official wording about RawFd to Rust.
In particular, this proposal would mean functions like SockRef::from with AsRawFd arguments should be marked unsafe, or migrated to alternatives. But the upside would be clearer guarantees for many use cases. I'd be interested in any feedback from folks working on socket2!
I'm working on a draft of a proposal to add new official wording about RawFd to Rust.
In particular, this proposal would mean functions like SockRef::from with AsRawFd arguments should be marked unsafe, or migrated to alternatives. But the upside would be clearer guarantees for many use cases. I'd be interested in any feedback from folks working on socket2!
I don't have time to look through the entire proposal, but SockRef::from will remain a safe function. In my opinion the AsRawFd/AsRawSocket documentation should be clearer that the returned fd/socket should be valid. The current documentation for AsRawFd says the following
This method does not pass ownership of the raw file descriptor to the caller. The descriptor is only guaranteed to be valid while the original object has not yet been destroyed.
The second sentence says to me that the implementation needs to ensure the file descriptor is valid (while the object is alive). Unfortunately RawFd is just a c_int (because it needs to match C semantics) and can "just be created" without being valid, i.e. the literal 123 is a "valid" (as in type) RawFd but of course isn't a valid fd. Couple that with the AsRawFd impl for RawFd and we got a problem as shown in the initial issue report.
I think the problem is that the creation of RawFd should be an unsafe operation in which the programmer needs to ensure it's a valid descriptor. But we can't change that anymore. We can improve the documentation however.
Instead of making it unsafe, what would you think about changing SockRef::from to use AsUnsafeSocket in place of AsRawFd?
AsUnsafeSocket is implemented for all the relevant std types, TcpStream, TcpListener, UdpSocket, UnixStream, UnixListener, and UnixDatagram. For other types, the only thing needed is to add an impl OwnsRaw, so types like socket2::Socket and others could easily implement it. That way, SockGet::from wouldn't be unsafe, and many use cases would continue to work as-is.
And to be sure, this isn't urgent; I'm still in the process of exploring the options.
Instead of making it unsafe, what would you think about changing SockRef::from to use AsUnsafeSocket in place of AsRawFd?
I don't think socket2 should dependencies outside of libc/winapi. Furthermore exposing those dependencies in a public API is a bad idea as we're then tied to a fixed version of the crate (i.e. we couldn't update to unsafe-io v2 without also updating to socket2 v2).
I'm also not convinced that AsUnsafeSocket is an improvement of AsRawFd. I think the best way forward is improving the documentation of AsRawFd/AsRawSocket.
AsUnsafeSocket is implemented for all the relevant std types, TcpStream, TcpListener, UdpSocket, UnixStream, UnixListener, and UnixDatagram. For other types, the only thing needed is to add an impl OwnsRaw, so types like socket2::Socket and others could easily implement it. That way, SockGet::from wouldn't be unsafe, and many use cases would continue to work as-is.
And to be sure, this isn't urgent; I'm still in the process of exploring the options.
Right now, it's possible for code in one library to use functions like SockRef::from to break the encapsulation of code in another library without itself using any unsafe. It's a small loophole in Rust, and it is fixable, so I'm interested in whether any of the possible fixes are practical, rather than just leaving the loophole open and documenting it.
One option would be to propose adding the parts of unsafe-io needed to fix this to std. The OwnsRaw trait might be sufficient. That way you could implement OwnsRaw and use AsRawFd + OwnsRaw, without any dependencies. Does something like that sound practical?
Right now, it's possible for code in one library to use functions like SockRef::from to break the encapsulation of code in another library without itself using any unsafe.
This is not true. If the encapsulation implements AsRawFd, which allows external access to the file descriptor, it must be able to handle changes to the file descriptor, e.g. options are set on it. If it can't deal with this it shouldn't implement AsRawFd, it's as simple as that.
It's a small loophole in Rust, and it is fixable, so I'm interested in whether any of the possible fixes are practical, rather than just leaving the loophole open and documenting it.
It's not a loophole, it's by design. It's required to deal with C/Unix/Windows I/O model of using integers/void pointers as identifiers to a kernel file description.
One option would be to propose adding the parts of unsafe-io needed to fix this to std. The OwnsRaw trait might be sufficient. That way you could implement OwnsRaw and use AsRawFd + OwnsRaw, without any dependencies. Does something like that sound practical?
I still don't get the point of OwnsRaw and the benefit over AsRawFd, could you elaborate why you think it's an improvement?
Suppose library A holds a RawFd it never exposes, which happens to have the value 4. Suppose library B does this:
let s = socket2::SockRef::from(&4);letmut buf = vec![std::mem::MaybeUninit::new(0u8);32];
s.peek(&mut buf[..])?;
This compiles on stable Rust today. B can read A's otherwise encapsulated data, with no unsafe of its own.
Another example would be B accidentally holding a RawFd value after closing it, which then aliases a newly created file descriptors in A. POSIX considers all this defined behavior. But in Rust, it's expected that one library shouldn't be able to read another's encapsulated data without using unsafe.
There's a way to fix this: from_raw_fd is unsafe. However there's a loophole: AsRawFd and IntoRawFd are not unsafe, so anything can implement them. It's tempting to just document this, and say that implementing them should imply certain guarantees, however std itself implements them for RawFd and makes no guarantees.
OwnsRaw solves this by being an unsafe trait. Types which implement it have to use the keyword unsafe, and in doing so they commit to the desired guarantees.
Suppose library A holds a RawFd it never exposes, which happens to have the value 4. Suppose library B does this:
let s = socket2::SockRef::from(&4);letmut buf = vec![std::mem::MaybeUninit::new(0u8);32];
s.peek(&mut buf[..])?;
This comment by withoutboats (not pinging) points to the core issue: rust-lang/rust#76969 (comment), that RawFd is just a type alias. In your example you're creating a RawFd from nothing: that should be unsafe, but isn't. That is not a problem with socket2, but one with std lib, which due to the v1 stability guarantees can't be changed.
We should clearly document this footgun of using integer literals as RawFd in both SocketRef::from and AsRawFd.
Integer literals are one example. Use-after-close is another. Arithmetic and deserialization are others.
I agree, we could better document the current situation. At the same time, this problem has the same form as problems with raw pointers, such as dangling and aliasing, which Rust does more than just document. Fixing it seems desirable in principle. And it seems possible. Is it practical?
I agree with withoutboats; the underlying problem is in std. OwnsRaw is a new trait, designed to be used with the existing types and traits without changing or replacing them. If there's a consensus around an RFC, then we should be able to arrange for all the important types that implement AsRawFd to implement OwnsRaw too—it's just one line of code per type, and it doesn't break anything to add them. std itself wouldn't need any breaking changes. Functions like SockRef::from could wait until the OwnsRaw impls are in place before adding + OwnsRaw to their bound to minimize disruption.
I have no idea what you mean with private, but if you mean shouldn't be modified by external (outside of the type's impls) that it should not implement AsRawFd/AsRawSocket. If it does implement AsRawFd/AsRawSocket that it must be able to deal with the situation it's actually used.
I strongly disagree. AsRawFd for a file descriptor is the equivalent of casting a reference to an integer. The latter is a safe operation, and so is the former. The inverse, turning an integer to a reference, is unsafe -- not just because the integer could be dangling, but also because that memory could be owned by other parties that e.g. have &mut or Box pointers.
The situation here is entirely analogous: as_raw_fd (the "ptr-to-int cast") is safe; anything working with such potentially dangling, potentially aliased FDs ("dereferencing a raw pointer") should be unsafe.
This is not true. If the encapsulation implements AsRawFd, which allows external access to the file descriptor, it must be able to handle changes to the file descriptor, e.g. options are set on it. If it can't deal with this it shouldn't implement AsRawFd, it's as simple as that.
Again, consider the analogy with references and integers: just because &mut can be safely cast to a raw pointer, does not mean that references can handle arbitrary changes to the underlying data through this raw pointer -- it is still wrong to violate the uniqueness assumption of the &mut reference.
Taking a step back, there are two options for how the Rust ecosystem as a whole could treat raw FDs:
"free-for-all", no ownership, doing arbitrary operations on arbitrary FDs is, in principle, allowed.
"exclusive", with ownership, one may only operate on FDs that one owns.
Both options are self-consistent and internally coherent. The standard library quite clearly took option 2, as a consequence as_raw_fd is safe and from_raw_fd is unsafe and RawFd is just a type aliased. This is also the option Rust took for pointers/memory. (Here, option 1 is not really realistic as that would violate memory safety.) You seem to claim that what the standard library does is somehow incoherent; that claim is wrong. It is entirely coherent to say both "AsRawFd is safe" and "working with non-owned FDs is unsafe"; this is exactly what Rust does for pointers and it works equally well for FDs. The only incoherence we have here is parts of the ecosystem picking option 2 with others picking option 1.
In your example you're creating a RawFd from nothing: that should be unsafe, but isn't.
Here you are presuming option 1 -- but that is simply not the option the standard library took. Under option 2, there is nothing wrong with creating a RawFd being a safe operation. This is in analogy with how turning a usize to a *mut i32 is a safe operation.
So, it could be unsafe (if option 1 is what we'd use), but saying it "should" be unsafe is incorrect.
I strongly disagree. AsRawFd for a file descriptor is the equivalent of casting a reference to an integer. The latter is a safe operation, and so is the former. The inverse, turning an integer to a reference, is unsafe -- not just because the integer could be dangling, but also because that memory could be owned by other parties that e.g. have &mut or Box pointers.
The situation here is entirely analogous: as_raw_fd (the "ptr-to-int cast") is safe; anything working with such potentially dangling, potentially aliased FDs ("dereferencing a raw pointer") should be unsafe.
I see your point and partially agree. I don't working with fd should be as unsafe as it can/should never cause memory unsafety as the OS always checks the validity of the file descriptor and returns an error if it's invalid. But I do see the point your making.
Again, consider the analogy with references and integers: just because &mut can be safely cast to a raw pointer, does not mean that references can handle arbitrary changes to the underlying data through this raw pointer -- it is still wrong to violate the uniqueness assumption of the &mut reference.
I have to disagree here. If a type needs to hold some invariant it shouldn't allow arbitrary access to the underlying data. Take NonZeroUsize for example, it needs to ensure that the underlying data (usize) is never zero. So it can hand out mutable reference to the underlying data as they might break the invariant. Compared to say Wrapping (which doesn't have any such invariant) it's underlying data is public.
Taking a step back, there are two options for how the Rust ecosystem as a whole could treat raw FDs:
1. "free-for-all", no ownership, doing arbitrary operations on arbitrary FDs is, in principle, allowed.
2. "exclusive", with ownership, one may only operate on FDs that one owns.
Both options are self-consistent and internally coherent. The standard library quite clearly took option 2, as a consequence as_raw_fd is safe and from_raw_fd is unsafe and RawFd is just a type aliased. This is also the option Rust took for pointers/memory. (Here, option 1 is not really realistic as that would violate memory safety.) You seem to claim that what the standard library does is somehow incoherent; that claim is wrong. It is entirely coherent to say both "AsRawFd is safe" and "working with non-owned FDs is unsafe"; this is exactly what Rust does for pointers and it works equally well for FDs. The only incoherence we have here is parts of the ecosystem picking option 2 with others picking option 1.
In your example you're creating a RawFd from nothing: that should be unsafe, but isn't.
Here you are presuming option 1 -- but that is simply not the option the standard library took. Under option 2, there is nothing wrong with creating a RawFd being a safe operation. This is in analogy with how turning a usize to a *mut i32 is a safe operation.
So, it could be unsafe (if option 1 is what we'd use), but saying it "should" be unsafe is incorrect.
I see your point.
What do you (not just @RalfJung) suggest the new API would look like? I have some requirements:
No external dependencies.
Preferably the operation requires no unsafe for known valid fds, e.g. going from &TcpStream to SocketRef should be safe (as it is with the current API).
Preferably crates that expose socket types, e.g. mio::net::TcpStream, don't need to have a (public) dependency on socket2.
What do you (not just @RalfJung) suggest the new API would look like? I have some requirements:
* No external dependencies.
* _Preferably_ the operation requires no unsafe for known valid fds, e.g. going from `&TcpStream` to `SocketRef` should be safe (as it is with the current API).
* _Preferably_ crates that expose socket types, e.g. `mio::net::TcpStream`, don't need to have a (public) dependency on socket2.
To eliminate the need for a dependency, I've now added the OwnsRaw trait to my RFC draft. So if the RFC is accepted, and once we have OwnsRaw impls for mio::net::TcpStream, socket2::Socket, and other important types, the new API could look like this:
use std::io::OwnsRaw;
...impl<'s,S> From<&'s S> forSockRef<'s>whereS:AsRawFd + OwnsRaw,
That would meet all three requirements. Does that look reasonable?
NonZeroUsize can be converted to usize in safe code. In other words, even types that need to hold invariants can safely and reasonably provide read-only access to their internal data. Even &mut T maintains some very important invariant on the underlying data, and yet that data can be safely inspected (by casting to usize). That is exactly what as_raw_fd does as well.
What I meant was that NonZeroUsize doesn't hand out &mut usize, because it couldn't ensure the invariant. In my opinion AsRawFd is similar to handing out a OsLock<FileDescription>. Yes, access to Rust's memory isn't possible with just a RawFd, but the implementer should known mutable access in possible to the kernel's file description (the data the kernel holds for each file/socket/etc.). So, what I tried to convey was that handing out a RawFd, even when the method is safe, does allow mutable access to the file description. And types that implemented AsRawFd should be able to handle this.
Note that the file descriptor in File is not public, and you cannot safely construct a File from the raw underlying data (unlike Wrapping). So I do not understand which analogy you are making here.
I was referring to mutable access to Wrapping's internal data (e.g. usize), compared to NonZeroUsize. But let's leave this, me bringing this type up sidetracks us more then anything else.
To eliminate the need for a dependency, I've now added the OwnsRaw trait to my RFC draft. So if the RFC is accepted, and once we have OwnsRaw impls for mio::net::TcpStream, socket2::Socket, and other important types, the new API could look like this:
I guess your proposal uses the 's lifetime of SocketRef to ensure that the ref does not outlive the owned FD? Neat.
Exactly, that's why it only implements From<&'s AsRawFd>, so we always ensured the owned socket outlives the SockRef.
In my opinion AsRawFd is similar to handing out a OsLock.
I think here we are getting to the bottom of the problem -- the ownership associated with the return value of AsRawFd is unspecified. This is the exact problem that C has with pointers (types don't say who owns what); Rust solved that with Box and reference types.
You interpret it to be actually returning some form of ownership (even though I am not sure what the OsLock is about). However, note that as_raw_fd takes a type with a lifetime, so surely your idealized return type must have a lifetime as well? That file descriptor is definitely not valid forever.
I interpret it to be like this:
typeRawInt = usize;pubfnas_raw_integer<T>(x:&mutT) -> RawInt{ x as*mut_asusize}
IOW, I do not think any ownership is returned here.
I do think that there are some indications in std that as_raw_fd does not return ownership. Given your idealized return type, you do seem to entertain the idea that FDs can be exclusively owned -- but clearly, that is in contradiction with having safe ways to access arbitrary usize file descriptors, i.e., it is in contradiction with the APIs currently offered by this crates. Or am I misunderstanding something?
That said, I also admit that the AsRawFd trait seems kind of useless in this world.
In my opinion AsRawFd is similar to handing out a OsLock.
I think here we are getting to the bottom of the problem -- the ownership associated with the return value of AsRawFd is unspecified. This is the exact problem that C has with pointers (types don't say who owns what); Rust solved that with Box and reference types.
You interpret it to be actually returning some form of ownership (even though I am not sure what the OsLock is about).
I interpret it not about ownership, but it's about access. With OsLock meant that the OS serialises access to the file desctiption (avoiding data races inside the kernel etc.), so it's like handing out a lock. I shouldn't been clearer about this.
However, note that as_raw_fd takes a type with a lifetime, so surely your idealized return type must have a lifetime as well? That file descriptor is definitely not valid forever.
Indeed that why SockRef has a lifetime. In a sense SockRef is already the idealized return type to me. It holds the raw fd, but has a lifetime to ensure the fd remains valid.
I interpret it to be like this:
typeRawInt = usize;pubfnas_raw_integer<T>(x:&mutT) -> RawInt{ x as*mut_asusize}
IOW, I do not think any ownership is returned here.
In a sense, but file descriptor are of course special. They aren't about the value themselves, but what give you access to the file description. Returning a file descriptor (to me at least) also implicitly returning access to the file description.
I do think that there are some indications in std that as_raw_fd does not return ownership.
I agree AsRawFd doesn't return ownership, for that we IntoRawFd which passes ownership.
Given your idealized return type, you do seem to entertain the idea that FDs can be exclusively owned
I indeed think file descriptor can be exclusively owned. AsRawFd (to me) allows the user to borrow the file descriptor, meaning the type doesn't have exclusive access anymore
-- but clearly, that is in contradiction with having safe ways to access arbitrary usize file descriptors, i.e., it is in contradiction with the APIs currently offered by this crates. Or am I misunderstanding something?
The API in question From<&T> where T: AsRawFd was never about accessing arbitrary usize file descriptors, in fact quite the opposite. It was about borrowing, safely, a socket type without having go through the unsafe hoop of AsRawFd/FromRawFd (as the From implementation did that for the user). So they can do SockRef::from(&TcpStream) without having to deal with unsafe or raw file desctiptors.
An ideal trait, perhaps as a safer alternative to AsRawFd, would look something like the following:
structBorrowedFd<'fd>{fd:RawFd,_lifetime:PhantomData<&'fdRawFd>,// Lifetime of the file descriptor we're borrowing from}traitAsBorrowedFd{// Perhaps some types need mutable access, so `&'fd mut self`.fnas_borrow_fd<'fd>(&'fdself) -> BorrowedFd<'fd>;}
That said, I also admit that the AsRawFd trait seems kind of useless in this world.
In a sense, but file descriptor are of course special. They aren't about the value themselves, but what give you access to the file description. Returning a file descriptor (to me at least) also implicitly returning access to the file description.
One could say the same thing about pointers. :) You don't care about the address itself, you care about the data the ptr gives you access to. FDs might be special but not more special than pointers.
In Rust, we all agree that returning a usize that represents the underlying address of some data in memory does not implicitly give you the right to do anything with that address. You need to carefully read the docs to figure out who is allowed to do what with which piece of memory and when. We use ownership to encode those things in the type system and have the compiler help us. I think using the same strategy and terminology makes a lot of sense for FDs, as well. They are just pointers into a different address space.
I indeed think file descriptor can be exclusively owned. AsRawFd (to me) allows the user to borrow the file descriptor, meaning the type doesn't have exclusive access anymore
The thing is, without the "I/O safety" proposal, FDs cannot be exclusively owned. You need to accept that proposal to even have the notion of "owning an FD" in the vocabulary of Rust type invariants. If anyone can just take the integer "5" and treat it as an FD and write to it, all in safe code -- that is a counterexample to ownership of FDs.
The API in question From<&T> where T: AsRawFd was never about accessing arbitrary usize file descriptors, in fact quite the opposite. It was about borrowing, safely, a socket type without having go through the unsafe hoop of AsRawFd/FromRawFd (as the From implementation did that for the user). So they can do SockRef::from(&TcpStream) without having to deal with unsafe or raw file desctiptors.
You can also do SockRef::from(&(5 as RawFd)), though. So this story never quite worked, did it?
It looks like AsRawFd is simply the wrong trait for what you want to achieve. The name already says that this is about getting "raw" FD access -- the "raw" here is in direct analogy to "raw pointer". The fact that any integer can be safely turned into an impl AsRawFd is another giveaway. So since you have been using AsRawFd I interpreted this as a deliberate choice; now it looks to me like you did this for lack of a better alternative? (You could have created your own trait for this, but I guess that has non-obvious downsides as well and anyway now is not the time to rehash the design of this crate.^^) In that case I think we have agreement, even though we are not using all the same words for the same things. :)
AsBorrowedFd looks great, yes! Crucially, BorrowedFdborrows ownership of the underlying FD and thus this type must be unsafe-to-construct from a RawFd (but it can implement AsRawFd for the conversion the other way around). Please post it in that forum thread. I was imagining something like AsOwnedFd, which would be basically the same but forIntoRawFd.
One could say the same thing about pointers. :) You don't care about the address itself, you care about the data the ptr gives you access to. FDs might be special but not more special than pointers.
Indeed both pointer and fds are special cases on integers.
In Rust, we all agree that returning a usize that represents the underlying address of some data in memory does not implicitly give you the right to do anything with that address. You need to carefully read the docs to figure out who is allowed to do what with which piece of memory and when. We use ownership to encode those things in the type system and have the compiler help us. I think using the same strategy and terminology makes a lot of sense for FDs, as well. They are just pointers into a different address space.
I indeed think file descriptor can be exclusively owned. AsRawFd (to me) allows the user to borrow the file descriptor, meaning the type doesn't have exclusive access anymore
The thing is, without the "I/O safety" proposal, FDs cannot be exclusively owned. You need to accept that proposal to even have the notion of "owning an FD" in the vocabulary of Rust type invariants. If anyone can just take the integer "5" and treat it as an FD and write to it, all in safe code -- that is a counterexample to ownership of FDs.
But we can't write to integer 5 without unsafe code (well we can with the current SocketRef::from, but we already established that is unsound and undesired), you need to call the unsafe libc::write function or similar. But I very much agree that Rust would benefit from a ownership, lifetime and borrowing systems (equivalents) for file descriptors.
The API in question From<&T> where T: AsRawFd was never about accessing arbitrary usize file descriptors, in fact quite the opposite. It was about borrowing, safely, a socket type without having go through the unsafe hoop of AsRawFd/FromRawFd (as the From implementation did that for the user). So they can do SockRef::from(&TcpStream) without having to deal with unsafe or raw file desctiptors.
You can also do SockRef::from(&(5 as RawFd)), though. So this story never quite worked, did it?
It looks like AsRawFd is simply the wrong trait for what you want to achieve.
Agreed.
The name already says that this is about getting "raw" FD access -- the "raw" here is in direct analogy to "raw pointer". The fact that any integer can be safely turned into an impl AsRawFd is another giveaway. So since you have been using AsRawFd I interpreted this as a deliberate choice; now it looks to me like you did this for lack of a better alternative?
Yes.
(You could have created your own trait for this, but I guess that has non-obvious downsides as well and anyway now is not the time to rehash the design of this crate.^^) In that case I think we have agreement, even though we are not using all the same words for the same things. :)
Great.
AsBorrowedFd looks great, yes! Crucially, BorrowedFdborrows ownership of the underlying FD and thus this type must be unsafe-to-construct from a RawFd (but it can implement AsRawFd for the conversion the other way around). Please post it in that forum thread.
Done.
I was imagining something like AsOwnedFd, which would be basically the same but forIntoRawFd.
Pr #237 adds an example and some more docs around the unsafe/unsound usage of SockRef::from as a stop-gap for v0.4. Hopefully we can figure out a proper fix for v0.5.
I/O Safety has been stabilized in 1.63.0. I would be willing to write a PR that changes this crate to use the new I/O-safe traits, if the MSRV bump is acceptable.
@notgull we're going to bump to 1.63 in v0.5 for that reason, see #320.
We do have to be careful about the change in API though, we'll want to give people and nice path to upgrade. So, maybe we add the owned variants of the traits and leave the current (unsound) ones in place an deprecate them, removing them in the next version.
Activity
Thomasdezeeuw commentedon Apr 13, 2021
I have no idea what you mean with private, but if you mean shouldn't be modified by external (outside of the type's
impls) that it should not implementAsRawFd/AsRawSocket. If it does implementAsRawFd/AsRawSocketthat it must be able to deal with the situation it's actually used.The
SocketRef::from(&3)should be unsafe, but not becauseSocketRef::fromshould be unsafe. Because you're creating (implicitly) aRawFdwhich should be valid, but isn't in this example, that is the unsafe part.Furthermore you're dereferencing a raw pointer using
unsafe, so you're clearing not only using safe code. Putting code in an "external library" doesn't mean it shouldn't count towards unsafe code, it still is.So, I do agree that creating
RawFdfrom literals, especially when passing them directly to function as you did in the example should be unsafe or at least clearer. But I don't agreeSocketRef::fromis unsafe, or should be marked as such.sunfishcode commentedon Apr 13, 2021
It turns out that
RawFditself implementsAsRawFd. SoAsRawFddoesn't say anything about the validity or ownership of the returnedRawFdvalue.rust-lang/rust#72175 is still being discussed, and looks like it may lead to an RFC. If that happens, and if the RFC is accepted, functions that operate on
RawFdvalues should beunsafe. That includesSockRef::fromin its current form, since it accepts any type that implementsAsRawFdwhich includesRawFditself.That said,
SockRef::fromis a valuable use case, and ideally it should be possible to do what it's doing withoutunsafein its API. As one possible approach, theunsafe-iocrate has anOwnsRawtrait which types that implementAsRawFdetc. can implement to additionally state that they really do own their handles, and traits likeAsUnsafeSockettype which combineOwnsRawandAsRawFd(AsRawSocketon Windows). With these, it should be possible to havefn SockRef::from(t: &T) where T: AsUnsafeSocketas a safe API. (Aside: unsafe-io is a new crate, and I'm interested if anyone has ideas for how to improve the API!)But of course, it makes sense to wait and see how the discussion in rust-lang/rust#72175 and the possible RFC turn out before making major changes here.
sunfishcode commentedon Apr 15, 2021
I'm working on a draft of a proposal to add new official wording about
RawFdto Rust.In particular, this proposal would mean functions like
SockRef::fromwithAsRawFdarguments should be marked unsafe, or migrated to alternatives. But the upside would be clearer guarantees for many use cases. I'd be interested in any feedback from folks working on socket2!Thomasdezeeuw commentedon Apr 16, 2021
I don't have time to look through the entire proposal, but
SockRef::fromwill remain a safe function. In my opinion theAsRawFd/AsRawSocketdocumentation should be clearer that the returned fd/socket should be valid. The current documentation forAsRawFdsays the followingThe second sentence says to me that the implementation needs to ensure the file descriptor is valid (while the object is alive). Unfortunately
RawFdis just ac_int(because it needs to match C semantics) and can "just be created" without being valid, i.e. the literal123is a "valid" (as in type)RawFdbut of course isn't a valid fd. Couple that with theAsRawFdimpl forRawFdand we got a problem as shown in the initial issue report.I think the problem is that the creation of
RawFdshould be an unsafe operation in which the programmer needs to ensure it's a valid descriptor. But we can't change that anymore. We can improve the documentation however.sunfishcode commentedon Apr 16, 2021
Instead of making it
unsafe, what would you think about changingSockRef::fromto useAsUnsafeSocketin place ofAsRawFd?AsUnsafeSocketis implemented for all the relevant std types,TcpStream,TcpListener,UdpSocket,UnixStream,UnixListener, andUnixDatagram. For other types, the only thing needed is to add an implOwnsRaw, so types likesocket2::Socketand others could easily implement it. That way,SockGet::fromwouldn't beunsafe, and many use cases would continue to work as-is.And to be sure, this isn't urgent; I'm still in the process of exploring the options.
Thomasdezeeuw commentedon Apr 17, 2021
I don't think socket2 should dependencies outside of libc/winapi. Furthermore exposing those dependencies in a public API is a bad idea as we're then tied to a fixed version of the crate (i.e. we couldn't update to unsafe-io v2 without also updating to socket2 v2).
I'm also not convinced that
AsUnsafeSocketis an improvement ofAsRawFd. I think the best way forward is improving the documentation ofAsRawFd/AsRawSocket.sunfishcode commentedon Apr 19, 2021
Right now, it's possible for code in one library to use functions like
SockRef::fromto break the encapsulation of code in another library without itself using anyunsafe. It's a small loophole in Rust, and it is fixable, so I'm interested in whether any of the possible fixes are practical, rather than just leaving the loophole open and documenting it.One option would be to propose adding the parts of unsafe-io needed to fix this to
std. TheOwnsRawtrait might be sufficient. That way you could implementOwnsRawand useAsRawFd + OwnsRaw, without any dependencies. Does something like that sound practical?Thomasdezeeuw commentedon Apr 20, 2021
This is not true. If the encapsulation implements
AsRawFd, which allows external access to the file descriptor, it must be able to handle changes to the file descriptor, e.g. options are set on it. If it can't deal with this it shouldn't implementAsRawFd, it's as simple as that.It's not a loophole, it's by design. It's required to deal with C/Unix/Windows I/O model of using integers/void pointers as identifiers to a kernel file description.
I still don't get the point of
OwnsRawand the benefit overAsRawFd, could you elaborate why you think it's an improvement?sunfishcode commentedon Apr 20, 2021
Suppose library A holds a
RawFdit never exposes, which happens to have the value 4. Suppose library B does this:This compiles on stable Rust today. B can read A's otherwise encapsulated data, with no
unsafeof its own.Another example would be B accidentally holding a
RawFdvalue after closing it, which then aliases a newly created file descriptors in A. POSIX considers all this defined behavior. But in Rust, it's expected that one library shouldn't be able to read another's encapsulated data without usingunsafe.There's a way to fix this:
from_raw_fdis unsafe. However there's a loophole:AsRawFdandIntoRawFdare not unsafe, so anything can implement them. It's tempting to just document this, and say that implementing them should imply certain guarantees, howeverstditself implements them forRawFdand makes no guarantees.OwnsRawsolves this by being an unsafe trait. Types which implement it have to use the keywordunsafe, and in doing so they commit to the desired guarantees.Thomasdezeeuw commentedon Apr 20, 2021
This comment by withoutboats (not pinging) points to the core issue: rust-lang/rust#76969 (comment), that
RawFdis just a type alias. In your example you're creating aRawFdfrom nothing: that should be unsafe, but isn't. That is not a problem with socket2, but one with std lib, which due to the v1 stability guarantees can't be changed.We should clearly document this footgun of using integer literals as
RawFdin bothSocketRef::fromandAsRawFd.sunfishcode commentedon Apr 20, 2021
Integer literals are one example. Use-after-
closeis another. Arithmetic and deserialization are others.I agree, we could better document the current situation. At the same time, this problem has the same form as problems with raw pointers, such as dangling and aliasing, which Rust does more than just document. Fixing it seems desirable in principle. And it seems possible. Is it practical?
I agree with withoutboats; the underlying problem is in std.
OwnsRawis a new trait, designed to be used with the existing types and traits without changing or replacing them. If there's a consensus around an RFC, then we should be able to arrange for all the important types that implementAsRawFdto implementOwnsRawtoo—it's just one line of code per type, and it doesn't break anything to add them. std itself wouldn't need any breaking changes. Functions likeSockRef::fromcould wait until theOwnsRawimpls are in place before adding+ OwnsRawto their bound to minimize disruption.RalfJung commentedon Apr 23, 2021
I strongly disagree.
AsRawFdfor a file descriptor is the equivalent of casting a reference to an integer. The latter is a safe operation, and so is the former. The inverse, turning an integer to a reference, is unsafe -- not just because the integer could be dangling, but also because that memory could be owned by other parties that e.g. have&mutorBoxpointers.The situation here is entirely analogous:
as_raw_fd(the "ptr-to-int cast") is safe; anything working with such potentially dangling, potentially aliased FDs ("dereferencing a raw pointer") should beunsafe.Again, consider the analogy with references and integers: just because
&mutcan be safely cast to a raw pointer, does not mean that references can handle arbitrary changes to the underlying data through this raw pointer -- it is still wrong to violate the uniqueness assumption of the&mutreference.Taking a step back, there are two options for how the Rust ecosystem as a whole could treat raw FDs:
Both options are self-consistent and internally coherent. The standard library quite clearly took option 2, as a consequence
as_raw_fdis safe andfrom_raw_fdis unsafe andRawFdis just a type aliased. This is also the option Rust took for pointers/memory. (Here, option 1 is not really realistic as that would violate memory safety.) You seem to claim that what the standard library does is somehow incoherent; that claim is wrong. It is entirely coherent to say both "AsRawFd is safe" and "working with non-owned FDs is unsafe"; this is exactly what Rust does for pointers and it works equally well for FDs. The only incoherence we have here is parts of the ecosystem picking option 2 with others picking option 1.Here you are presuming option 1 -- but that is simply not the option the standard library took. Under option 2, there is nothing wrong with creating a
RawFdbeing a safe operation. This is in analogy with how turning ausizeto a*mut i32is a safe operation.So, it could be unsafe (if option 1 is what we'd use), but saying it "should" be unsafe is incorrect.
Thomasdezeeuw commentedon Apr 27, 2021
I see your point and partially agree. I don't working with fd should be as unsafe as it can/should never cause memory unsafety as the OS always checks the validity of the file descriptor and returns an error if it's invalid. But I do see the point your making.
I have to disagree here. If a type needs to hold some invariant it shouldn't allow arbitrary access to the underlying data. Take
NonZeroUsizefor example, it needs to ensure that the underlying data (usize) is never zero. So it can hand out mutable reference to the underlying data as they might break the invariant. Compared to sayWrapping(which doesn't have any such invariant) it's underlying data is public.I see your point.
What do you (not just @RalfJung) suggest the new API would look like? I have some requirements:
&TcpStreamtoSocketRefshould be safe (as it is with the current API).mio::net::TcpStream, don't need to have a (public) dependency on socket2.sunfishcode commentedon Apr 27, 2021
To eliminate the need for a dependency, I've now added the
OwnsRawtrait to my RFC draft. So if the RFC is accepted, and once we haveOwnsRawimpls formio::net::TcpStream,socket2::Socket, and other important types, the new API could look like this:That would meet all three requirements. Does that look reasonable?
4 remaining items
Thomasdezeeuw commentedon May 1, 2021
What I meant was that
NonZeroUsizedoesn't hand out&mut usize, because it couldn't ensure the invariant. In my opinionAsRawFdis similar to handing out aOsLock<FileDescription>. Yes, access to Rust's memory isn't possible with just aRawFd, but the implementer should known mutable access in possible to the kernel's file description (the data the kernel holds for each file/socket/etc.). So, what I tried to convey was that handing out aRawFd, even when the method is safe, does allow mutable access to the file description. And types that implementedAsRawFdshould be able to handle this.I was referring to mutable access to
Wrapping's internal data (e.g.usize), compared toNonZeroUsize. But let's leave this, me bringing this type up sidetracks us more then anything else.Exactly, that's why it only implements
From<&'s AsRawFd>, so we always ensured the owned socket outlives theSockRef.Could you link to that I can't find it.
RalfJung commentedon May 1, 2021
I think here we are getting to the bottom of the problem -- the ownership associated with the return value of
AsRawFdis unspecified. This is the exact problem that C has with pointers (types don't say who owns what); Rust solved that withBoxand reference types.You interpret it to be actually returning some form of ownership (even though I am not sure what the
OsLockis about). However, note thatas_raw_fdtakes a type with a lifetime, so surely your idealized return type must have a lifetime as well? That file descriptor is definitely not valid forever.I interpret it to be like this:
IOW, I do not think any ownership is returned here.
I do think that there are some indications in std that
as_raw_fddoes not return ownership. Given your idealized return type, you do seem to entertain the idea that FDs can be exclusively owned -- but clearly, that is in contradiction with having safe ways to access arbitraryusizefile descriptors, i.e., it is in contradiction with the APIs currently offered by this crates. Or am I misunderstanding something?That said, I also admit that the
AsRawFdtrait seems kind of useless in this world.Sure, sorry: https://internals.rust-lang.org/t/pre-rfc-i-o-safety/14585
Thomasdezeeuw commentedon May 1, 2021
I interpret it not about ownership, but it's about access. With
OsLockmeant that the OS serialises access to the file desctiption (avoiding data races inside the kernel etc.), so it's like handing out a lock. I shouldn't been clearer about this.Indeed that why
SockRefhas a lifetime. In a senseSockRefis already the idealized return type to me. It holds the raw fd, but has a lifetime to ensure the fd remains valid.In a sense, but file descriptor are of course special. They aren't about the value themselves, but what give you access to the file description. Returning a file descriptor (to me at least) also implicitly returning access to the file description.
I agree
AsRawFddoesn't return ownership, for that weIntoRawFdwhich passes ownership.I indeed think file descriptor can be exclusively owned.
AsRawFd(to me) allows the user to borrow the file descriptor, meaning the type doesn't have exclusive access anymoreThe API in question
From<&T> where T: AsRawFdwas never about accessing arbitraryusizefile descriptors, in fact quite the opposite. It was about borrowing, safely, a socket type without having go through theunsafehoop ofAsRawFd/FromRawFd(as theFromimplementation did that for the user). So they can doSockRef::from(&TcpStream)without having to deal withunsafeor raw file desctiptors.An ideal trait, perhaps as a safer alternative to
AsRawFd, would look something like the following:RalfJung commentedon May 1, 2021
One could say the same thing about pointers. :) You don't care about the address itself, you care about the data the ptr gives you access to. FDs might be special but not more special than pointers.
In Rust, we all agree that returning a
usizethat represents the underlying address of some data in memory does not implicitly give you the right to do anything with that address. You need to carefully read the docs to figure out who is allowed to do what with which piece of memory and when. We use ownership to encode those things in the type system and have the compiler help us. I think using the same strategy and terminology makes a lot of sense for FDs, as well. They are just pointers into a different address space.The thing is, without the "I/O safety" proposal, FDs cannot be exclusively owned. You need to accept that proposal to even have the notion of "owning an FD" in the vocabulary of Rust type invariants. If anyone can just take the integer "5" and treat it as an FD and write to it, all in safe code -- that is a counterexample to ownership of FDs.
You can also do
SockRef::from(&(5 as RawFd)), though. So this story never quite worked, did it?It looks like
AsRawFdis simply the wrong trait for what you want to achieve. The name already says that this is about getting "raw" FD access -- the "raw" here is in direct analogy to "raw pointer". The fact that any integer can be safely turned into animpl AsRawFdis another giveaway. So since you have been usingAsRawFdI interpreted this as a deliberate choice; now it looks to me like you did this for lack of a better alternative? (You could have created your own trait for this, but I guess that has non-obvious downsides as well and anyway now is not the time to rehash the design of this crate.^^) In that case I think we have agreement, even though we are not using all the same words for the same things. :)AsBorrowedFdlooks great, yes! Crucially,BorrowedFdborrows ownership of the underlying FD and thus this type must be unsafe-to-construct from aRawFd(but it can implementAsRawFdfor the conversion the other way around). Please post it in that forum thread. I was imagining something likeAsOwnedFd, which would be basically the same but forIntoRawFd.Thomasdezeeuw commentedon May 3, 2021
Indeed both pointer and fds are special cases on integers.
But we can't write to integer 5 without unsafe code (well we can with the current
SocketRef::from, but we already established that is unsound and undesired), you need to call the unsafelibc::writefunction or similar. But I very much agree that Rust would benefit from a ownership, lifetime and borrowing systems (equivalents) for file descriptors.Agreed.
Yes.
Great.
Done.
Thomasdezeeuw commentedon May 13, 2021
Pr #237 adds an example and some more docs around the unsafe/unsound usage of
SockRef::fromas a stop-gap for v0.4. Hopefully we can figure out a proper fix for v0.5.notgull commentedon Apr 28, 2022
Could we add an
Into<Socket>bound to this, since the types that can be fed intoInto<Socket>are well-known to be able to be converted into sockets?Thomasdezeeuw commentedon Apr 29, 2022
@notgull Anything that implements
From<T> for SocketimpliesInto<Socket> for T, see https://doc.rust-lang.org/std/convert/trait.From.html#generic-implementations.Thomasdezeeuw commentedon May 11, 2022
Looks like I/O safety might be stabilised in 1.62: rust-lang/rust#95118.
notgull commentedon Aug 11, 2022
I/O Safety has been stabilized in 1.63.0. I would be willing to write a PR that changes this crate to use the new I/O-safe traits, if the MSRV bump is acceptable.
Thomasdezeeuw commentedon Aug 11, 2022
@notgull we're going to bump to 1.63 in v0.5 for that reason, see #320.
We do have to be careful about the change in API though, we'll want to give people and nice path to upgrade. So, maybe we add the owned variants of the traits and leave the current (unsound) ones in place an deprecate them, removing them in the next version.