-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
RFC: Unix socket ancillary data v2 #3430
Conversation
7885e8b
to
fda7369
Compare
information about received file descriptors). | ||
|
||
```rust | ||
struct ControlMessagesBuf; |
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.
Would it make sense to have a SmallVec
like buffer here:
struct ControlMessagesBuf; | |
struct ControlMessagesBuf<const N: usize>; |
where N
decides how many messages it can store inline?
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.
The ControlMessagesBuf
capacity is in bytes, rather than messages -- and some message types are dynamically sized, for example SCM_RIGHTS
is semantically a (usize, [i32])
tuple.
I think it would be possible to have an AncillaryData
backed by a &mut [u8]
, but when writing the draft implementation I found it difficult to use. If the number of heap allocations is a concern, reusing an AncillaryData
between socket calls is probably more practical than stack allocation.
fn start_recvmsg(&mut self) -> Option<&mut [u8]>; | ||
unsafe fn finish_recvmsg(&mut self, control_messages_len: usize); |
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 might be necessary for use with sendmmsg
, recvmmsg
, io_uring
. But for standard sendmsg
/recvmsg
it would be better to have some generic implementation that takes any AsFd
.
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 I understand the meaning of this comment. Was it meant to be placed elsewhere?
If it helps, here's how these functions are used:
impl RecvMessage for UnixStream {
fn recv_message(
&self,
bufs: &mut [IoSliceMut<'_>],
ancillary_data: &mut AncillaryData<'_>,
options: RecvOptions,
) -> io::Result<(usize, RecvResult)> {
let mut msg: libc::msghdr = unsafe { zeroed() };
msg.msg_iov = bufs.as_mut_ptr().cast();
msg.msg_iovlen = bufs.len() as _;
if let Some(ancillary_buf) = ancillary_data.start_recvmsg() {
msg.msg_control = ancillary_buf.as_mut_ptr().cast();
msg.msg_controllen = ancillary_buf.len();
}
let size = self.0.recv_msg(&mut msg, options.as_recv_flags())?;
unsafe { ancillary_data.finish_recvmsg(msg.msg_controllen) };
Ok((size, RecvResult::new(msg.msg_flags)))
}
}
```rust | ||
struct MessageSender<S, 'a>; | ||
|
||
impl<S, 'a> MessageSender<S, 'a> { |
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 if this carries its weight. It only borrows its member so it can never live long. At that point one might as well call the SendMessage
trait directly. Ditto for MessageReceiver
.
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.
Calling the trait directly is fairly verbose for simple cases, and I found MessageSender
useful when writing small utilities to experiment with the shape of the API. It's not strictly necessary, but is nice to have from an ergonomics perspective.
Without it we'd probably want the traits to have send_message
, send_message_vectored
, send_message_vectored_with_flags
, and so on. I'm not 100% opposed to this if it's what the libs-api team prefers, but as a future user of this API I'd prefer having a helper struct available.
impl RecvResult { | ||
fn new(flags: c_int) -> RecvResult; | ||
|
||
fn custom_flags(&self, flags: c_int) -> bool; |
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 should be fn custom_flags(&self, flag_mask: c_int) -> c_int;
in order to accommodate flags that take multiple bits.
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.
Hmm, I had expected the implementation would be something like this, which would (I think?) work for multi-bit flags:
fn custom_flags(&self, flags: c_int) -> bool {
self.bits & flags == flags
}
Is there a case that would need the user to inspect the masked bits directly? I'm fine with using that API, but would like to make sure I understand it so I can write good examples in the docs.
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.
what happens when you have a 2-bit field and you want to see if it's 2? self.bits & flags == flags
can't do that because the mask would need to be e.g. 0x30
but would need to be compared against 0x20
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 that means, sorry. If the platform has msg_flags
flags like this:
// set on `(*struct msghdr)->msg_flags
const MSG_DID_MAGIC: c_int = 0x30;
Then testing whether that flag was set would need to flags & MSG_DID_MAGIC == MSG_DID_MAGIC
.
Did you have a particular platform / flag in mind? Looking up its definition in the OS headers or libc
might be helpful.
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.
if you had:
const MSG_FOO_MODE_MASK: c_int = 0x30;
const MSG_FOO_MODE_A: c_int = 0;
const MSG_FOO_MODE_B: c_int = 0x10;
const MSG_FOO_MODE_C: c_int = 0x20;
const MSG_FOO_MODE_D: c_int = 0x30;
fn foo_mode_is(msg_flags: c_int, mode: c_int) -> bool {
msg_flags & MSG_FOO_MODE_MASK == mode
}
then we should design it so you can have an equivalent for foo_mode_is(msg_flags, MSG_FOO_MODE_C)
where it checks that both bit 1 << 4
is zero and bit 1 << 5
is one, since both bits need to be checked to match and not all bits are ones. flags & mask == mask
is only capable of checking for bits that are ones, it is incapable of checking for zeros.
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.
That's the first time I've seen that sort of structure in a msghdr
flag. Which OS / architecture has recvmsg message flags like that?
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.
Also, if there are any OSes with flags like that, then I'd probably try to have an API like this (names illustrative):
impl RecvResult {
fn custom_flags(&self, flags: c_int) -> bool;
fn as_raw_flags(&self) -> c_int;
// ... + methods for POSIX-defined flags
}
So that most platform-specific flags can be checked easily, and any that use multi-bit values with masking can do that part in their own code.
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.
That's the first time I've seen that sort of structure in a
msghdr
flag. Which OS / architecture has recvmsg message flags like that?
nothing I'm aware of, I was just attempting to elaborate on @riking's comment. That said, multi-bit flags like that could totally exist or be added sometime in the future.
I'm curious why Rust's standard library should have ancillary data support, as opposed to letting this functionality be provided by third-party libraries. The RFC mentions nix; another is rustix. Those specific libraries don't satisfy all the RFC's motivations, however, a new library API could be developed that does. With the Standard library developer's guide guidance in mind: ancillary data doesn't require special language or compiler support, and doesn't need to extend standard library types—it's possible to build the functionality on top of the existing |
As a general matter I think it makes sense for Rust to have a large and capable standard library. I'm aware this is not a universally-held opinion. Some people think Rust should delegate OS-specific functionality to third-party libraries, as is done in the JavaScript world. In this model there is a large set of implementations for everything from filesystem interaction to networking, and users are expected to mix-n-match to fit their needs. Using your library However, from my perspective, a library like Finally, yes, I could take this RFC's code and release it as a standalone library. But then I'm just contributing to the problem.
The same could be said for pretty much everything under |
Imo the value of adding cmsg support is that it's both tricky to implement (with all those gnarly C macros and unaligned data and so on) and very useful for common unix patterns such as fd-passing and checking peer credentials. FD-passing via unix socket is also the sanest way among alternatives. Using procfs or inheritance has issues. There even is a use of fd-passing in std itself and it would be great if we could replace that with a safe abstraction: https://github.com/rust-lang/rust/blob/45a45c6e60835e15c92374be1f832bc756fc8b1a/library/std/src/sys/unix/process/process_unix.rs#L670-L777
What do you have in mind? With |
IMO one of the key advantages here of having this in I don't actually see this being used too much by users of |
rust-lang/libs-team#284 (comment)
maybe add |
The clear method is |
b59e6c1
to
713b756
Compare
c2bb793
to
d57ff25
Compare
I've reworked the middle layer of the design with the goal of supporting stack allocation and clarifying the ownership of received FDs. @the8472 could you please let me know if the changes resolve your concerns in rust-lang/libs-team#284 (comment) ? The details are:
|
50493d2
to
339b3c7
Compare
339b3c7
to
beffd22
Compare
// copy a control message into the ancillary data; error on out-of-capacity. | ||
fn add_control_message( | ||
&mut self, | ||
control_message: impl Into<ControlMessage<'_>>, |
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.
Wouldn't it make more sense to demand impl Into<ControlMessage<'fd>>
?
Then you could implement From<BorrowedFd<'fd>>
and From<&'fd [BorrowedFd<'fd>]>
for ControlMessage<'fd>
and get rid of the special add_file_descriptors()
.
It could also be useful for future additions of specific control messages that borrow data.
I would even suggest that the name 'fd
is a bit specific. In essence the two lifetimes represent the byte buffer and the encoded resources. So 'buf
and 'res
could be used as more generic names.
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.
In this design, control messages are for structured viewing and appending to the msg_control
buffer. Think of the ControlMessage
struct as a Rustic equivalent to the CMSG_*()
macros. They don't own file descriptors -- the ownership of FDs is managed by AncillaryData
.
The purpose of the separate layers is to try to find the (rather small!) intersection between Rust's ownership semantics, the API idioms of early-1980s Unix, and a desire to avoid heap-allocating everything.
Thinking purely in terms of ownership, the logical signature of sendmsg
/ recvmsg
might be notated something like this:
trait ControlMessage { ... }
sendmsg(
&self,
buf: &[u8],
cmsg: Vec<Box<dyn ControlMessage>>,
output_cmsg_capacity: usize,
rights: Vec<BorrowedFd<'_>>,
) -> (io::Result<usize>, Vec<Box<dyn ControlMessage>>);
recvmsg(
&self,
buf: &mut [MaybeUninit<u8>],
cmsg: Vec<Box<dyn ControlMessage>>,
output_cmsg_capacity: usize,
) -> (io::Result<&mut [u8]>, Vec<Box<dyn ControlMessage>>, Vec<OwnedFd>);
In these signatures it's clear how the control messages are consumed and FDs are borrowed/produced. However, there's just no way such definitions could be used in Rust std
-- besides the awful call sites, you'd have to heap-allocate everything and you'd need to bake in knowledge of all the various SCM_*
types.
Supporting third-party libraries that want to add support for OS-specific SCM_*
types means that we need to expose the actual raw byes of control messages in some reasonably accessible place -- hence struct ControlMessage<'a>
providing a borrowed view of a &'a ScmTimestampPktinfo
or whatever.
But if you're exposing raw cmsg bytes, they can't also be used for ownership of FDs without adding even more complexity to the API surface. I tried -- the original version of this had ControlMessage<'fd>
and a pub enum ScmRights { Borrowed(...), Owned(..) }
-- but it just didn't work well.
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.
Hmm, but nothing prevents a user from adding SCM_RIGHTS
manually using this function, and then the lifetime of the FDs isn't checked. So I still think this should have a lifetime for the resources the control message represents.
Even then, I think this function should be unsafe, as the control message contains instructions for the kernel. So who knows what it will do (the lifetime will still help prevent wrong usage though).
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.
Hmm, but nothing prevents a user from adding
SCM_RIGHTS
manually using this function, and then the lifetime of the FDs isn't checked. So I still think this should have a lifetime for the resources the control message represents.
If we want to allow third-party code to implement arbitrary SCM_*
control message types, then there's no way to prevent the user from manually encoding an SCM_RIGHTS
.
Even then, I think this function should be unsafe, as the control message contains instructions for the kernel. So who knows what it will do (the lifetime will still help prevent wrong usage though).
That's not what the unsafe
keyword is for.
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.
Even then, I think this function should be unsafe, as the control message contains instructions for the kernel. So who knows what it will do (the lifetime will still help prevent wrong usage though).
That's not what the
unsafe
keyword is for.
It is, because we don't know what the kernel will do to our memory and open file descriptors because of those instructions.
If we want to allow third-party code to implement arbitrary
SCM_*
control message types, then there's no way to prevent the user from manually encoding anSCM_RIGHTS
.
Yeah, but we can help them not make mistakes by asking for the correct lifetime. This is also an example why I think the function should be unsafe. Otherwise you can use safe code to let the kernel dup arbitrary numbers as a 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.
Are you sure that you can receive control messages with sendmsg
and send them with recvmsg
? The C API does technically allow it according to the pointer types, but I can't find it anywhere in documentation.
nix
and rustix
also don't support it, although of course that doesn't mean it's impossible:
https://docs.rs/nix/latest/nix/sys/socket/fn.sendmsg.html
https://docs.rs/nix/latest/nix/sys/socket/fn.recvmsg.html
https://docs.rs/rustix/latest/rustix/net/fn.sendmsg.html
https://docs.rs/rustix/latest/rustix/net/fn.recvmsg.html
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.
An example of control messages received from a sendmsg()
is Linux's socket timestamps: https://www.kernel.org/doc/html/v6.6/networking/timestamping.html
Control messages passed into recvmsg()
are sometimes used to configure socket options for that specific call only, though I forget the exact SCM_*
type at the moment.
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.
That page says that timestamps are received with a recvmsg()
call:
2 Data Interfaces
Timestamps are read using the ancillary data feature of recvmsg(). See man 3 cmsg for details of this interface. The socket manual page (man 7 socket) describes how timestamps generated with SO_TIMESTAMP and SO_TIMESTAMPNS records can be retrieved.
The SO_TIMESTAMP
and SO_TIMESTAMPNS
options only apply to incoming packets:
O_TIMESTAMP
Generates a timestamp for each incoming packet in (not necessarily monotonic) system time.
Same timestamping mechanism as SO_TIMESTAMP, but reports the timestamp as struct timespec in nsec resolution.
You can enable timestamps through a control message, but only when sending packets:
In addition to socket options, timestamp generation can be requested per write via cmsg, only for SOF_TIMESTAMPING_TX_* (see Section 1.3.1). Using this feature, applications can sample timestamps per sendmsg() without paying the overhead of enabling and disabling timestamps via setsockopt.
This all indicates that you can only send control messages with sendmsg()
and you can only receive them with recvmsg()
.
And if that's true, we can reflect that in the API, which can be simplified a bit.
If you can send ancillary data with recvmsg()
and you can receive it with sendmsg()
, then I agree one struct for both is much easier. But it seems that you can't, and then splitting the API for the two separate use cases makes things a lot simpler.
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.
Hmm, interesting. I've written software in the past for a platform that modified the sendmsg()
control messages buffer to report metadata about outgoing packet processing, but apparently it shouldn't have been doing that.
I'll want to do some more testing just to double-check, but if the read/write stages are strictly separated on all reasonable target platforms then that should let the new API be cleaner.
Thank you for pointing me in the right direction!
) -> AncillaryData<'a, 'fd>; | ||
|
||
// returns initialized portion of `control_messages_buf`. | ||
fn control_messages(&self) -> &ControlMessages; |
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 function seems a little underpowered. As a low level API I understand something like this is needed, but it does leave the interpretation totally to the user. Even in the case of recognized messages like SCM_RIGHTS
.
In tokio_seqpacket
I went for an enum holding known types and Other
variant for unrecognized messages: https://docs.rs/tokio-seqpacket/latest/tokio_seqpacket/ancillary/enum.AncillaryMessage.html
The downside of that is that it's specific to one type of socket and/or address family.
Maybe there should be an associated type for the socket to advertise what type of control messages are supported. Then the iterator could use that to interpret messages into something more useful.
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.
The current implementation of unix_socket_ancillary_data
(in nightly) does use an enum, but as noted in the RFC's first section that enum has turned out to be one of the biggest problems.
The Rust standard library can't have enums with cases that only exist on some platforms, and there's only one control message type specified by POSIX -- SCM_RIGHTS
. So you end up with an enum that looks something like this:
pub enum ScmRights<'a> {
Borrowed(&'a [BorrowedFd<'a>]),
Owned(Vec<OwnedFd>), // heap allocation!
}
pub struct GenericControlMessage<'a> { ... }
pub enum ControlMessage<'a> {
ScmRights(ScmRights<'a>),
Other(GenericControlMessage<'a>),
}
Given that the SCM_RIGHTS
is also the only control message type that represents unmanaged resources transferred across the socket boundary, it makes sense to treat it specially inside the library. That leaves enum ControlMessage
with only a single case, at which point it can be collapsed.
Maybe there should be an associated type for the socket to advertise what type of control messages are supported. Then the iterator could use that to interpret messages into something more useful.
The maintenance load of cataloging SCM_*
types across OSes would be very high, and documentation is thin regarding which control messages are supported by any given socket type. I don't think there's any way we can expect the Rust standard library to provide types for more than a small handful of the most popular ones.
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.
Hmm, I get your point. If the enum is only used for reporting control messages from the kernel, then it wouldn't be too bad if it contains variants that don't exist on all platforms. They would simply never be constructed.
But I do agree that cataloguing all messages is too much, considering all the platform specific differences.
So it probably does make sense to implement a higher level API for end-users on top of a pretty low level API. That could certainly be left for crates.io libraries though.
) -> Result<(), AncillaryDataNoCapacity>; | ||
|
||
// Transfers ownership of received FDs to the iterator. | ||
fn received_fds(&mut self) -> AncillaryDataReceivedFds<'_>; |
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.
If I understand correctly you can use either control_messages()
or received_fds()
to iterate over the received messages (the latter iterating only over FDs and taking ownership of them).
Maybe this should be named take_received_fds()
to reflect that it takes ownership?
On a different topic, with this design, this function modifies the received messages but leaves the message in the buffer. So if user code later uses control_messages()
, it will contain SCM_RIGHTS
message with invalid file descriptors. While -1
is a commonly used sentinel value, it is not one that the kernel will put in an SCM_RIGHTS
message.
In toki_seqpacket
, I opted to either borrow or consume the control messages with AncillaryMessageReader::messages()
or AncillaryMessageReader::into_messages()
.
That way, you don't need to worry about leaving the SCM_RIGHTS
(or others) in an invalid state, because it will not be accessible anymore (except through the original &[u8]
, but nobody is promising it is a control message at that point).
As a downside, these are two totally separate iterator implementations. Even the yielded items are different (AncillaryMessage<'a>
versus OwnedAncillaryMessage<'a>
)
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.
Users iterating over the control messages need to be prepared to skip messages they weren't expecting:
for cmsg in ancillary.control_messages() {
if let Some(msg) = ScmSomeThing::from_cmsg(cmsg) { ... }
if let Some(msg) = ScmOtherThing::from_cmsg(cmsg) { ... }
}
So as long as they aren't specifically looking for SCM_RIGHTS
, the presence of tombstoned messages in the buffer won't bother them. And since the API for accessing file descriptors is right there in AncillaryData
, users have no need to construct their own SCM_RIGHTS
handling unless they want something really specific.
|
||
// Scan the control messages buffer for `SCM_RIGHTS` and take ownership of | ||
// any file descriptors found within. | ||
unsafe fn take_ownership_of_scm_rights(&mut self); |
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.
What exactly will this do?
Set some flag internally to not close file descriptors in drop()
? Or replace them all internally with -1
?
Since users can still observe the control messages though control_messages()
, I think the distinction should be very clear.
Also, if it replaces fds with -1
, it is hard to use correctly. You would need to call take_ownership_of_scm_rights()
after wrapping the fds in OwnedFd
. But if the code panics in between or someone returns an Err
, the file descriptor will be closed twice.
If it's a flag it's also difficult to use. You could set it before wrapping the fds in your own OwnedFd
, but then you might leak some file descriptors when you panic or exit early with an Err
(better than closing FDs twice, but still not great).
It would be much more convenient to take ownership as you're iterating. Then you can panic or return an error without worrying about IO safety.
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.
As described in the comment and elsewhere in the RFC, it takes ownership of the file descriptors in the ancillary data's control messages buffer.
It would be much more convenient to take ownership as you're iterating. Then you can panic or return an error without worrying about IO safety.
Ownership needs to be taken by the AncillaryData
before the iteration can start, otherwise iteration would require unsafe fn next()
. The place where the encoded RawFd
s are (logically) transmuted to OwnedFd
is here.
|
||
// Update the control messages buffer length according to the result of | ||
// calling `sendmsg()` or `recvmsg()`. | ||
fn set_control_messages_len(&mut self, len: usize); |
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 feels like it should be unsafe. Setting this wrong could cause other code to interpret parts of the buffer as data from the kernel.
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.
There's no way for code to know whether an AncillaryData
was populated with data from the kernel, or even whether a given ControlMessage
was created via AncillaryData
. Code that needs to trust its inputs must verify its call path into sendmsg()
/ recvmsg()
.
This particular function is not unsafe
because using it incorrectly will not violate any of Rust's memory-safety guarantees.
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.
Hmm, I see, because taking ownership of FDs is delayed until take_ownership_of_scm_rights()
is called, and that one is unsafe. I do think it would be nice if take_ownership_of_scm_rights()
is not a separate step, but we can discuss that in a different thread.
#### `fn AncillaryData::set_control_messages_len` | ||
|
||
Does not take ownership of FDs in any `SCM_RIGHTS` control messages that | ||
might exist within the new buffer length. | ||
|
||
**Panics**: | ||
* if `len > control_messages_buf.len()` | ||
* if `control_messages_buf()` hasn't been called to clear the length. | ||
|
||
The second panic condition means that creating an `AncillaryData` and then | ||
immediately calling `set_control_messages_len` will panic to avoid potentially | ||
reading uninitialized data. | ||
|
||
Also, calling `set_control_messages_len()` twice without an intervening | ||
`control_messages_buf()` will panic to avoid leaking received 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.
I don't think panicking prevents leaking file descriptors. A panic could be caught with catch_unwind()
. In most async runtimes this is already done for all spawned tasks, so when a background tasks panics, the process keeps running.
I also don't really understand why this doesn't take ownership of the file descriptors. I assume it will set the valid range of the control message, and the drop implementation will close them based on this. Won't it?
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 panicking prevents leaking file descriptors. A panic could be caught with
catch_unwind()
. In most async runtimes this is already done for all spawned tasks, so when a background tasks panics, the process keeps running.
The panic happens before the buffer length is reset. If the panic (assertion) did not happen, then an implementation of SendMessage
or RecvMessage
that called set_control_messages_len()
in the wrong sequence would implicitly leak all owned FDs in the ancillary data, which may be difficult to detect (vs a panic, which is quite easily detectable).
I also don't really understand why this doesn't take ownership of the file descriptors. I assume it will set the valid range of the control message, and the drop implementation will close them based on this. Won't it?
This function is called by both the sendmsg()
and recvmsg()
code paths. The sendmsg()
code path does not, and must not, take ownership of SCM_RIGHTS
in its buffer.
### `struct AncillaryDataBuf` | ||
|
||
An `AncillaryDataBuf` is an owned variant of `AncillaryData`, using heap | ||
allocation (an internal `Vec<u8>`). It exposes a subset of the `Vec` capacity | ||
management methods. |
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.
Personally I don't really see the need for this type. Ancillary data is in my experience fairly short. Using a fixed size buffer will almost always be good enough.
That said, I don't really mind this type existing. I do wonder if it's worth extending the API surface for 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.
It's useful when using file-descriptor passing as part of a larger / more complex API, such as Wayland or D-Bus. Dynamic allocation means the library doesn't need to hardcode the maximum number of FDs it can send/receive (other than the OS limit).
fn new() -> AncillaryDataBuf<'static>; | ||
fn with_capacity(capacity: usize) -> AncillaryDataBuf<'static>; |
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.
Why does the type have a lifetime if all constructors return a AncillaryDataBuf<'static>
?
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.
The lifetime parameter of AncillaryDataBuf
is the lifetime of the BorrowedFd
s that have been passed to it.
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.
But you can't construct one with a non-'static lifetime with the documented interface.
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.
The 'static
lifetime is a subtype of all other lifetimes.
fn new_static() -> &'static () { &() }
fn use_static() {
let t = ();
let ts = [&t, new_static()];
}
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.
Maybe it's just a typo? Should new()
and with_capacity()
return Self
instead of AncillaryDataBuf<'static>
?
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.
Ah, right. My bad, just looks confusing.
long enough to be sent, and (2) received FDs aren't leaked. | ||
|
||
```rust | ||
struct AncillaryData<'a, '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.
Why do these need to be two separate lifetimes? Both the byte buffer and the file descriptors need to be valid while the AncillaryData
struct exists. What's the benefit of splitting these?
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.
That was the original design, but the borrow checker complains -- it thinks the ancillary data's buffer remains borrowed for as long as any borrowed files. Giving them separate lifetimes gives the borrow better better visibility into what's actually going on.
If you want, you can clone my branch and change this to AncillaryData<'a>
-- the included test should then fail to compile.
By default an `AncillaryData` has no received FDs, and this method will | ||
ignore FDs added via `add_file_descriptors`. The iterator holds a mutable | ||
borrow on the `AncillaryData`, and will close any unclaimed FDs when | ||
it's dropped. |
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.
By default an `AncillaryData` has no received FDs, and this method will | |
ignore FDs added via `add_file_descriptors`. The iterator holds a mutable | |
borrow on the `AncillaryData`, and will close any unclaimed FDs when | |
it's dropped. | |
By default an `AncillaryData` has no received FDs, and this method will | |
ignore FDs added via `add_file_descriptors`. The iterator holds a mutable | |
borrow on the `AncillaryData`, and will close any unclaimed FDs when | |
it's dropped. |
I understand that the file descriptors added via add_file_descriptors()
need to be ignored. They're not owned by the AncillaryData
struct. But how does this work internally? Won't both recvmgs()
and add_file_descriptors()
put a SCM_RIGHTS
message in the underlying buffer? How will the implementation distinguish between these SCM_RIGHTS
messages?
In tokio_seqpacket
, this kind of issue is part of why I went for two separate types: AncillaryMessageReader
for receiving, and AncillaryMessageWriter
for sending message.
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.
Internally there's a high-watermark that gets set, so the AncillaryData
knows which portion of its buffer came from recvmsg()
and which portion was added later.
@de-vri-es Thank you for the detailed review! Many of the questions seem to be about what the implementation might look like, so I put together a quick sketch implementation at https://github.com/rust-lang/rust/compare/master...jmillikin:upstream__rust:unix-ancillary-data-v2?expand=1 -- it's not beautiful code, but it should be enough to get an idea of how things fit together. I'll respond to the review comments in their threads. |
It feels like a lot of complexity comes from supporting |
I think I would prefer to avoid making the new API surface even larger with separate send/recv cursors unless every other alternative has been exhausted. |
I dont disagree with that, but the implementation will be a lot simpler. The reader can assume all data it is passed is from the kernel, and it can instantly take ownership when it is created (or when you call And the writer doesn't have to worry about owning file descriptors, because it never owns anything. I also dont think the combined API would necessarily be larger: there's two types instead of one, but the writer doesn't need to allow iterating over added messages and the reader doesn't allow adding messages. /edit: I'm not the only one who though it was easier to have two types. |
Thanks for the extensive review, @de-vri-es! Based on consensus in today's @rust-lang/libs-api meeting, I'm un-nominating this for now. Please re-nominate when all the suggestions have either been resolved or have reached a point of dispute that the team needs to look at. You can use rustbot to add the I-libs-api-nominated label. |
Rendered