Understanding rio code

quoting GitHub - spacejam/rio: pure rust io_uring library, built on libc, thread & async friendly, misuse resistant

async fn proxy(ring: &rio::Rio, a: &TcpStream, b: &TcpStream) -> io::Result<()> {
    let buf = vec![0_u8; 512];
    loop {
        let read_bytes = ring.read_at(a, &buf, 0).await?;
        let buf = &buf[..read_bytes];
        ring.write_at(b, &buf, 0).await?;
    }
}

the part I don't understand: how is it that both read_at and write_at use & buf? I expect at least one of them to be a & mut buf.

I mean you don't always need to have &mut to have mutability. UnsafeCell being the prime example.

Well, it's just because rio uses unsafe code to write through an immutable reference. It is very clearly undefined behavior in this case.

4 Likes

In this case, this is indeed undefined behavior, since Rio converts the buffer to libc::iovec, which is basically a "pointer + length", and writes through that pointer (required for read_at) are, indeed, undefined behavior.

The problem is that read_at expects &impl AsIoVecMut, whereas it should really expect either &mut impl AsIoVecMut or just impl AsIoVecMut directly. This trait is implemented wherever AsMut<u8> is implemented, so &Vec<u8> would be out of question.

3 Likes

@alice @Cerber-Ursi

Would this particular undefined behaviour issue be fixed by changing

from

    pub fn read_at<'a, F, B>(
        &'a self,
        file: &'a F,
        iov: &'a B,
        at: u64,
    ) -> Completion<'a, usize>
    where
        F: AsRawFd,
        B: AsIoVec + AsIoVecMut,
    {
        self.read_at_ordered(file, iov, at, Ordering::None)
    }

to

    pub fn read_at<'a, F, B>(
        &'a self,
        file: &'a F,
        iov: &'a mut B,
        at: u64,
    ) -> Completion<'a, usize>
    where
        F: AsRawFd,
        B: AsIoVec + AsIoVecMut,
    {
        self.read_at_ordered(file, iov, at, Ordering::None)
    }

Not sure, since the problem is not just here - it's in the fact that AsIoVecMut doesn't provide the method which goes through &mut self, i.e., even if we use &mut B, Rio will internally convert via the &B.

1 Like

Put another way, are you saying something like this:

Look at:

pub trait AsIoVec {
    /// Returns the address of this object.
    fn into_new_iovec(&self) -> libc::iovec;
}

part of the problem here is that in C land libc::iovec can be used for both read and write. In Rust land, since this is coming from a &self and not a &mut self, this libc::iovec should only be used for reading.

So, a necessary, but possibly insufficient starting point is we should have

pub struct IOVec_ReadOnly(libc::iovec)
pub struct IOVec_ReadWrite(libc::iovec)

pub trait AsIoVec {
    /// Returns the address of this object.
    fn into_new_iovec(&self) -> IOVec_ReadOnly,
}

pub trait AsIoMutVec {
    /// Returns the address of this object.
    fn into_new_iovec(&mut self) -> IOVec_ReadWrite,
}

then, in the C ffi calls, the IOVec_ReadOnly should only be used for reading, while the IoVec_ReadWrite can be used for both read and write.

Is this what you are hinting at ?

Yes, that's what I'm talking about. It's possible to use the same type for both read and write, as long as it can be proven that iovec used for writing was created from exclusive reference, but this is indeed less safe (i.e. provides less compile-time verification) then your idea.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.