`UnsafeCell` 在没有锁定的情况下跨线程共享 - 这可能会导致 UB,对吗?

问题描述 投票:0回答:1

考虑以下代码:

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)
}

我的推理如下:

安全代码可以获得具有相同底层 TlsStream 的 TcpStreamSend 和 TcpStreamRecv。

如果每个都发送到单独的线程,则可以同时调用

TcpStreamSend::write
TcpStreamRecv::read
。这些函数中的每一个都会获得对底层 TlsStream 的
&mut
引用。

因此,由于同时拥有 2 个可变引用是非法的,因此这段代码可能会导致 UB,应该被认为是不健全的。这是正确的吗?

我的同事向我保证“如果它有效,它就有效”,并且代码确实在大多数情况下都可以正常运行,除了一些随机的偶尔的恐慌之外。然而,根据我的理解,它可能会在我们的代码库中的任何地方导致不可预测的问题,应该立即重写。我错过了什么吗?

顺便说一句,我做了一些研究,代码似乎受到了这个SO答案的启发,它更复杂,但是,据我所知,同样糟糕。

rust undefined-behavior unsafe
1个回答
0
投票

绝对不健全。

在 Rust 中同时存在两个重叠的

&mut
引用是未定义的行为,如果两个线程同时调用
read
write
,就会发生这种情况。即使对于非线程代码来说这也是一个问题 - Rust 假设没有
&mut
引用可以别名。

在没有锁的情况下分割

TlsStream
是不可能的。即使
TcpStream
中的读取和写入实现是独立的,
TlsStream
也可能需要根据来自服务器的消息更改其写入方式(例如重新生成密钥),这将要求读取器更改写入器使用的状态。

© www.soinside.com 2019 - 2024. All rights reserved.