查看原文
其他

Rust 生态蜜蜂|Unsafe 随堂小测题解 Part 7(完结)

张汉东 觉学社 2022-11-12
前言
因觉学,求绝学。 Rust 生态蜜蜂,是觉学社公众号开启的一个付费专栏。生态蜜蜂,顾名思义,是从 Rust 生态的中,汲取养分,供我们成长。计划从2022年7月第二周开始,到2022年12月最后一周结束。预计至少二十篇周刊,外加一篇Rust年度报告总结抢读版。本专栏可以在公众号菜单「生态蜜蜂」中直接进入。海外用户可以去 https://rustbee.zhubai.love/ 订阅。

本篇是对第三篇随堂小测[1]中第14题和第15题的题解。

题目与题解

第 14 题

  • 以下是类型 IntoIter 的公开接口实现,其中存在什么问题?(10分)
  • 请针对该类型的各公开接口构造测试,使以下代码触发 UB。(10分)
// 该 ManuallyDrop 类型被用于禁止编译器自动调用其包装类型 T 的析构函数
use std::mem::ManuallyDrop;
use std::{ptr, slice};

#[derive(Clone)]
pub struct IntoIter<T> {
 // 构造一个堆上存储的切片类型
    boxed_slice: Box<[ManuallyDrop<T>]>,
    cur: usize,
}

impl<T> IntoIter<T> {
    pub fn new(boxed_slice: Box<[T]>) -> Self {
        let boxed_slice: Box<[ManuallyDrop<T>]> = unsafe {
            let mut b = ManuallyDrop::new(boxed_slice);
            let data: *mut ManuallyDrop<T> = b.as_mut_ptr().cast();
            let len: usize = b.len();
            // 通过data 和 len 构造切片
Box::from_raw(slice::from_raw_parts_mut(data, len))
        };
        Self {
            boxed_slice,
            cur: 0,
        }
    }
}

// 实现迭代器
impl<T> Iterator for IntoIter<T> {
    type Item = T;

    fn next(&mut self) -> Option<Self::Item> {
        unsafe {
            let item: ManuallyDrop<T> = ptr::read(self.boxed_slice.get(self.cur)?);
            self.cur += 1// 迭代游标指向下一个 Item
            Some(ManuallyDrop::into_inner(item))
        }
    }
}

impl<T> Drop for IntoIter<T> {
    fn drop(&mut self) {
        unsafe {
            let data: *mut T = self.boxed_slice.as_mut_ptr().add(self.cur).cast();
            let len: usize = self.boxed_slice.len() - self.cur;
            // 通过计算 data 和 len得到 slice
            let slice: *mut [T] = slice::from_raw_parts_mut(data, len);
            // 原地释放slice 内存
            ptr::drop_in_place(slice);
        }
    }
}

上面代码是创建了一个堆上的切片,然后为其实现迭代器和Drop,其中用到了 `ManuallyDrop` 类型[2] 。该类型可以屏蔽编译器自动调用其包装类型 T 的析构函数。但是如果你调用了它提供了ManuallyDrop::into_inner 函数则会让 T 继续被自动析构。

基于这个思路,我们来构造 UB 代码:

fn main(){
 let box_slice = Box::new([vec![1], vec![2], vec![3]]);
 let mut iter = IntoIter::new(box_slice);
 iter.next(); // 会导致 box_slice 第一个 Vec<i32> 被自动析构
 // Undefined Behavior: pointer to alloc1711 was dereferenced after this allocation got freed
 let iter_clone = iter.clone(); 
 for i in iter_clone {
  println!("{i:?}");
 }
}

这个示例用 Miri 执行的时候会抛出 // error: Undefined Behavior: pointer to alloc1711 was dereferenced after this allocation got freed  这样的错误。

其实原因就在于:main函数中创建的 iter 在执行 next() 方法之后,其内部的第一个 Vec<i32> 被自动析构了。然而下一行 iter.clone() 又将 iter 深度拷贝了一次。

iter 的内存布局大概是这样:

// stack: [] 
// heap: ()
// layout: 
// [ptr] -> (box slice ptr) -> (ManuallyDrop[Vec], ManuallyDrop[Vec], ManuallyDrop[Vec])

clone() 方法会被一层一层地调用,第一个元素的 Vec 内存已经被释放了,再clone 它的时候就会发生解引用已释放内存的问题,这是一个未定义行为,所以在代码执行的时候还不容易出现错误,但是靠 Miri 可以检测出来。

在 《Rust 编码规范》中有一个迭代器的规则 G.TRA.BLN.02 不要为迭代器实现Copy 特质[3] 。原因就是一般会存在有改变状态的迭代器,如果实现 Copy,则可能会被意外隐式复制,违反 Rust 编译器可变借用独占原则,可能会导致一些意外行为。

所以,通常可以只为迭代器实现 Clone,需要复制的时候显式拷贝。就像本例中的问题,clone 是显式的,如果出现问题,也容易排查。

第 15 题

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存