Consider the following code:
use std::{cell::UnsafeCell, io, net::TcpStream, sync::Arc};
use native_tls::TlsStream;
#[derive(Debug)]
pub struct TcpStreamRecv(Arc<UnsafeCell<TlsStream<TcpStream>>>);
unsafe impl Send for TcpStreamRecv {}
unsafe impl Sync for TcpStreamRecv {}
impl io::Read for TcpStreamRecv {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
unsafe { &mut *self.0.get() }.read(buf)
}
}
#[derive(Debug)]
pub struct TcpStreamSend(Arc<UnsafeCell<TlsStream<TcpStream>>>);
unsafe impl Send for TcpStreamSend {}
unsafe impl Sync for TcpStreamSend {}
impl io::Write for TcpStreamSend {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
unsafe { &mut *self.0.get() }.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
unsafe { &mut *self.0.get() }.flush()
}
}
pub fn tcp_split(stream: TlsStream<TcpStream>) -> (TcpStreamSend, TcpStreamRecv) {
let inner = Arc::new(UnsafeCell::new(stream));
let send = TcpStreamSend(inner.clone());
let recv = TcpStreamRecv(inner);
(send, recv)
}
My reasoning is as folows:
Safe code can obtain a TcpStreamSend and a TcpStreamRecv with the same underlying TlsStream.
If each of these is sent to a separate thread, it is possible to call TcpStreamSend::write
and TcpStreamRecv::read
at the same time. Each of these functions obtains a &mut
reference to the underlying TlsStream.
Therefore, since it is illegal to have 2 mutable references at the same time, this code can cause UB and should be considered unsound. Is this correct?
My coworker assured me that "if it works, it works", and the code indeed appears to function normally most of the time, apart from some random occasional panics. In my understanding, however, it can cause unpredictable problems anywhere in our codebase, and should be rewritten immediately. Did I miss something?
By the way, I did some research and the code seems to be inspired by this SO answer which is more convoluted, but, as far as I can see, equally bad.