【问题标题】:Rust FFI and Aliasing: C -> Rust -> C -> Rust call stacks with C values being aliased: Undefined Behaviour?Rust FFI 和别名:C -> Rust -> C -> Rust 调用堆栈,C 值被别名:未定义的行为?
【发布时间】:2022-01-08 14:45:09
【问题描述】:

在以下情况下,我正试图围绕 Rust 的别名规则:

假设我们在 C 中有一个内存分配。我们将一个指向该分配的指针传递给 Rust。 Rust 函数对分配做一些事情,然后调用回 C 代码(没有任何参数),其中另一个 rust 函数被调用,其分配与参数相同。现在,我们假设只有第一个 Rust 函数获得可变引用。

调用堆栈如下所示:

Some C Code (owns data)
  Rust(pointer as &mut)
    Some C code (does not get parameters from Rust)
      Rust(pointer as &)

作为一个简短的示例,让我们假设以下两个文件: 测试.c

#include <stdio.h>
#include <stdlib.h>

void first_rust_function(int * ints);
void another_rust_function(const int * ints);
int * arr;
void called_from_rust() {
        another_rust_function(arr);
}
int main(int argc, char ** argv) {
        arr = malloc(3*sizeof(int));
        arr[0]=3;
        arr[1]=4;
        arr[2]=53;
        first_rust_function(arr);
        free(arr);
}

test.rs

use std::os::raw::c_int;
extern "C" { fn called_from_rust(); }
#[no_mangle]
pub extern "C" fn first_rust_function(ints : &mut [c_int;3]) {
    ints[1] = 7;
    unsafe { called_from_rust() };
}
#[no_mangle]
pub extern "C" fn another_rust_function(ints : &[c_int;3]) {
    println!("Second value: {}", ints[1])
}

(为了完整起见:运行此代码会打印“第二个值:7”)

请注意,Rust (called_from_rust()) 对 C 的回调没有任何参数。因此,Rust 编译器没有任何人可能从指向的值中读取的信息。

我的直觉告诉我这是未定义的行为,但我不确定。

我快速浏览了 Stacked Borrows,发现违反了该模型。在上面的例子中,只有Rule (protector) 被破坏了,但是如果first_rust_function(ints : &amp;mut [c_int;3]) 在调用called_from_rust() 之后仍然使用ints 也会违反其他规则。

但是,我没有找到任何官方文档说明 Stacked Borrows 是 Rust 编译器使用的别名模型,并且在 Stacked Borrows 下被认为未定义的所有内容实际上在 Rust 中都是未定义的。天真地,这看起来与将&amp;mut 强制转换为&amp; 非常相似,因此它实际上可能是理智的,但鉴于called_from_rust() 不将引用作为参数,我认为这种推理不适用。

这让我想到了实际的问题:

  • 上述代码是否调用了未定义的行为(以及为什么/为什么不?)
  • 如果未定义:如果called_from_rust() 将指针作为参数并将其向前传递:void called_from_rust(const int * i) { another_rust_function(i); },则行为是否明确?
  • 如果两个 Rust 函数都使用 &amp;mut [c_int;3] 会怎样?

【问题讨论】:

  • C 绑定应该包含 restrict 用于绑定 rust 函数的任何可变引用参数。

标签: rust undefined-behavior ffi


【解决方案1】:

上面的代码是否调用了未定义的行为?

是的,你已经破坏了 Rust 的指针别名规则。依赖 Stacked Borrows 规则有点可疑,因为正如您所暗示的那样,我认为它尚未被正式采用为 Rust 的内存访问模型(即使它只是当前语义的形式化)。然而, 一个实际而具体的规则是 LLVM 的 noalias 属性,Rust 编译器在 &amp;mut 参数上使用该属性。

noalias

这表示在函数执行期间,通过基于参数或返回值的指针值访问的内存位置也不会通过不基于参数或返回值的指针值访问。 ...

因此,由于您在执行该函数期间从基于first_rust_function 中的ints 的指针访问another_rust_function 中的ints[1],因此这是违规行为。鉴于这种未定义的行为,我相信编译器完全有权让代码打印“第二个值:4”。


如果called_from_rust() 将指针作为参数并将其向前传递:void called_from_rust(const int * i) { another_rust_function(i); },则行为是否明确?

是的,这样可以很好地定义它。您可以看到这一点,因为 Rust 借用检查器可以看到该值可以在 called_from_rust() 中使用,并将防止在该调用周围不当使用 ints


如果两个 Rust 函数都使用 &amp;mut [c_int;3] 会怎样?

如果您使用上述修复方法,即第二次借用基于第一次,则没有问题。如果你不这样做,那就更糟了。

【讨论】:

  • 非常感谢!我不确定我是否正确理解了所有内容,但这个确认让我放心。
  • noalias 的标准化程度低于 Stacked Borrows。 noalias 是一个实现细节,而 Stacked Borrows 虽然以前尚未标准化,但已达成一致。
  • "noalias is an implementation detail" - 当然,但它被使用,它被使用的是documented,并代替Rust 将任何东西形式化,它可以作为指针别名的未定义行为和非未定义行为的实用指南。
  • Stacked Borrows 比 noalias 更严格,因此,虽然打破 noalias 是 UB,但我认为打破 Stacked Borrows 也是如此(Miri 遵循这种模式)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2018-01-14
  • 1970-01-01
  • 1970-01-01
  • 2014-01-24
  • 2021-03-17
  • 2015-05-14
  • 1970-01-01
相关资源
最近更新 更多