You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Rust <1.84中合并同原切片的子切片是否可行?如何解决Miri报错?

问题描述

我定义了一个存储切片的结构体:

struct S<'a> {
    slice: &'a [u8],
}

我尝试编写代码合并两个该结构体的实例——显然只有当两个切片来自同一原始数据时才能成功。我通过检查指针范围(即[指针, 指针+长度])确认它们属于同一内存区域后,使用unsafe { core::slice::from_raw_parts(lower_ptr, new_length_elements) }重建切片,单元测试正常但Miri报错:

error: Undefined Behavior: trying to retag from <718383> for SharedReadOnly permission at alloc
486[0x14], but that tag does not exist in the borrow stack for this location
    --> ~/.rustup/toolchains/nightly-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/slice/raw.rs:138:9                                             |
138  |         &*ptr::slice_from_raw_parts(data, len)
     |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
     |         |
     |         trying to retag from <718383> for SharedReadOnly permission at alloc486[0x14], but that tag does not exist in the borrow stack for this location
     |         this error occurs as part of retag at alloc486[0x8..0x18]
     |
     = help: this indicates a potential bug in the program: it performed an invalid operation, but the Stacked Borrows rules it violated are still experimental
     = help: see https://github.com/rust-lang/unsafe-code-guidelines/blob/master/wip/stacked-borrows.md for further information
help: <718383> was created by a SharedReadOnly retag at offsets [0x8..0x14]
    --> example/src/lib.rs:229:24
     |
229  |         let lower_ptr = self.slice.as_ptr();
     |                        ^^^^^^^^^^^^^^^^^^^
     = note: BACKTRACE (of the first span) on thread `tests::merge`:
     = note: inside `std::slice::from_raw_parts::<_, u32>` at ~/.rustup/toolchains/nightly-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/slice/raw.rs:138:9: 138:47

我想知道:能否从同一内存区域的两个子切片重建切片?如何操作才能避免Miri报错?

解决方案

可以从同一内存区域的两个子切片合并出更大的切片,但你的写法违反了Rust的**栈式借用(Stacked Borrows)**规则,导致Miri报错。

错误原因

你用self.slice.as_ptr()拿到的指针,其权限只覆盖self.slice对应的子区间。当你用这个指针创建覆盖更大范围的切片时,新切片的内存范围超出了原指针的权限标签覆盖的区域,Miri就会检测到这个未定义行为。

正确做法

要获取能覆盖整个原始内存区域的合法指针和生命周期,不能只依赖子切片的指针,得确保你拿到的是原始数据的完整借用。

方法1:在结构体中保留原始数据的引用

修改结构体,同时存储原始数据的完整引用,这样合并时可以直接基于原始引用创建新切片:

struct S<'a> {
    original: &'a [u8],
    slice: &'a [u8],
}

impl<'a> S<'a> {
    fn merge(&self, other: &S<'a>) -> Option<Self> {
        // 先检查两个子切片是否属于同一原始数据
        if !std::ptr::eq(self.original.as_ptr(), other.original.as_ptr()) {
            return None;
        }

        // 计算合并后的起始和结束位置
        let self_start = self.slice.as_ptr() as usize;
        let self_end = self_start + self.slice.len();
        let other_start = other.slice.as_ptr() as usize;
        let other_end = other_start + other.slice.len();

        let start = self_start.min(other_start);
        let end = self_end.max(other_end);

        // 基于原始引用的指针计算偏移,创建新切片
        let offset = start - self.original.as_ptr() as usize;
        let len = end - start;
        let merged_slice = &self.original[offset..offset+len];

        Some(Self {
            original: self.original,
            slice: merged_slice,
        })
    }
}

方法2:通过std::slice::from_raw_parts时,确保权限覆盖完整范围

如果你不想修改结构体,必须确保用来创建新切片的指针,其权限覆盖合并后的整个区间。可以通过获取原始数据的完整借用(比如从某个拥有原始数据的变量处),再基于它的指针来创建新切片,而不是用子切片的指针:

impl<'a> S<'a> {
    fn merge(&self, other: &S<'a>, original: &'a [u8]) -> Option<Self> {
        // 验证两个子切片都属于原始数据
        let self_in_bounds = {
            let start = self.slice.as_ptr() as usize;
            let end = start + self.slice.len();
            let orig_start = original.as_ptr() as usize;
            let orig_end = orig_start + original.len();
            start >= orig_start && end <= orig_end
        };
        let other_in_bounds = {
            let start = other.slice.as_ptr() as usize;
            let end = start + other.slice.len();
            let orig_start = original.as_ptr() as usize;
            let orig_end = orig_start + original.len();
            start >= orig_start && end <= orig_end
        };

        if !self_in_bounds || !other_in_bounds {
            return None;
        }

        // 计算合并后的范围
        let self_start = self.slice.as_ptr() as usize;
        let self_end = self_start + self.slice.len();
        let other_start = other.slice.as_ptr() as usize;
        let other_end = other_start + other.slice.len();

        let start = self_start.min(other_start);
        let len = self_end.max(other_end) - start;

        // 基于原始数据的指针创建新切片,确保权限覆盖完整范围
        let merged_slice = unsafe {
            std::slice::from_raw_parts(
                original.as_ptr().add(start - original.as_ptr() as usize),
                len
            )
        };

        Some(Self { slice: merged_slice })
    }
}

关键要点

  • 栈式借用规则要求,指针的权限标签必须覆盖你要访问的所有内存区域。子切片的指针只拥有对应子区间的权限,不能直接用来创建更大范围的切片。
  • 必须基于覆盖完整目标范围的合法借用来创建新切片,要么保留原始数据的引用,要么从拥有原始数据的地方获取合法指针。

内容的提问来源于stack exchange,提问作者big_gie

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 05:37:02