Luc*_*iel 8 unsafe initialization undefined-behavior rust
我正在开发一个库,该库帮助处理适合 FFI 边界上的指针大小 int 的类型。假设我有一个这样的结构:
use std::mem::{size_of, align_of};
struct PaddingDemo {
data: u8,
force_pad: [usize; 0]
}
assert_eq!(size_of::<PaddingDemo>(), size_of::<usize>());
assert_eq!(align_of::<PaddingDemo>(), align_of::<usize>());
Run Code Online (Sandbox Code Playgroud)
这个结构体有 1 个数据字节和 7 个填充字节。我想将此结构的一个实例打包到 a 中usize,然后在 FFI 边界的另一侧解包。因为这个库是通用的,我使用MaybeUninit和ptr::write:
use std::ptr;
use std::mem::MaybeUninit;
let data = PaddingDemo { data: 12, force_pad: [] };
// In order to ensure all the bytes are initialized,
// zero-initialize the buffer
let mut packed: MaybeUninit<usize> = MaybeUninit::zeroed();
let ptr = packed.as_mut_ptr() as *mut PaddingDemo;
let packed_int = unsafe {
std::ptr::write(ptr, data);
packed.assume_init()
};
// Attempt to trigger UB in Miri by reading the
// possibly uninitialized bytes
let copied = unsafe { ptr::read(&packed_int) };
Run Code Online (Sandbox Code Playgroud)
该assume_init调用是否触发了未定义的行为?换句话说,当将ptr::write结构复制到缓冲区时,它是否复制填充字节的未初始化状态,将初始化状态覆盖为零字节?
目前,当这个或类似的代码在 Miri 中运行时,它不会检测到任何未定义的行为。但是,根据github 上关于此问题的讨论,ptr::write据说允许复制这些填充字节,并进一步复制它们的未初始化。真的吗?的文档ptr::write根本没有谈论这个,也没有关于未初始化内存的 nomicon 部分。
假设_init 调用是否触发了未定义的行为?
是的。“未初始化”只是 Rust 抽象机中的字节可以具有的另一个值,仅次于通常的 0x00 - 0xFF。让我们将这个特殊字节写为 0xUU。(有关此主题的更多背景信息,请参阅此博客文章。) 0xUU 由副本保留,就像字节可以具有的任何其他可能值由副本保留一样。
但细节有点复杂。在 Rust 中,有两种方法可以在内存中复制数据。不幸的是,Rust 语言团队也没有明确指定这方面的细节,因此以下是我个人的解释。我认为我所说的是没有争议的,除非另有标记,但这当然可能是一个错误的印象。
一般来说,当复制一定范围的字节时,源范围只会覆盖目标范围——因此,如果源范围是“0x00 0xUU 0xUU 0xUU”,那么在复制之后,目标范围将具有确切的字节列表。
这就是memcpy/memmove在 C 中的行为(在我对标准的解释中,不幸的是,这里不是很清楚)。在 Rust 中,ptr::copy{,_nonoverlapping} 可能会执行按字节复制,但现在实际上还没有精确指定,有些人可能想说它也是类型化的。本期对此进行了一些讨论。
另一种方法是“类型化副本”,这是每次正常赋值 ( =) 以及向函数传递值或从函数传递值时发生的情况。类型化副本以某种类型解释源内存T,然后将该类型值“重新序列化”T到目标内存中。
与按字节复制的主要区别在于,与类型不相关的信息T会丢失。这基本上是一种复杂的方式,表示键入的副本“忘记”填充,并有效地将其重置为未初始化。与非打字副本相比,打字副本会丢失更多信息。非类型化副本保留底层表示,类型化副本仅保留表示的值。
因此,即使您转换0usize为PaddingDemo,该值的类型化副本也可以将其重置为“0x00 0xUU 0xUU 0xUU”(或任何其他可能的填充字节)——假设data位于偏移量 0 处,这不能保证(#[repr(C)]如果需要,请添加)那个保证)。
在您的情况下,ptr::write采用 type 的参数PaddingDemo,并且该参数通过类型化副本传递。因此,此时填充字节可能会任意更改,特别是它们可能会变为 0xUU。
usize您的代码是否具有 UB 取决于另一个因素,即 a 中是否有未初始化的字节usize是 UB。问题是,(部分)未初始化的内存范围是否代表某个整数?目前还没有,因此有了 UB。然而,这种情况是否应该如此存在着激烈的争论,而且我们最终可能会允许这样做。
不过,许多其他细节仍不清楚——例如,将“0x00 0xUU 0xUU 0xUU”转换为整数很可能会导致完全未初始化的整数,即整数可能无法保留“部分初始化”。为了在整数中保留部分初始化的字节,我们基本上必须说整数没有抽象的“值”,它只是一个(可能未初始化的)字节序列。这并不反映整数在诸如 之类的操作中如何使用/。(其中一些还取决于 LLVM 围绕poison和 的freezepoison决策;LLVM 可能会决定,在以整数类型执行加载时,如果任何输入字节为,结果就是 full poison。)因此,即使代码不是 UB 因为我们允许未初始化的整数,它可能无法按预期运行,因为您要传输的数据正在丢失。
如果您想传输原始字节,我建议使用适合于此的类型,例如MaybeUninit. 如果您使用整数类型,目标应该是传输整数值——即数字。
| 归档时间: |
|
| 查看次数: |
295 次 |
| 最近记录: |