前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Rust漫画 #3 | 二次元 Rust Meetup 讨论会:Rewrite it in Rust 是否有害?

Rust漫画 #3 | 二次元 Rust Meetup 讨论会:Rewrite it in Rust 是否有害?

作者头像
张汉东
发布2023-10-25 12:38:54
3390
发布2023-10-25 12:38:54
举报
文章被收录于专栏:Rust 编程Rust 编程

前言

创意:张汉东 绘画:ChatGPT DALL•E3

创意来源:本人学习 Rust 过程中的一些踩坑、成长与思考。

如果大家喜欢,欢迎点赞、转发和赞赏,每一位读者对认可是我持续创作的动力。

漫画赏析

你好啊,作为一名程序员,参加线下的 Meetup 技术交流会也许是你唯一的社交活动。无论是线上还是线下,请都不要错过。今天,也许是你参加的第一次二次元 Rust Meetup 。

噢,我是小明,非常欢迎您参加本次 Rust Developers Meetup !请进来吧!小心别把 Ferris crab 踩到 。。。

今天 Meetup 的讨论主题是:用 Rust 重写项目是否有必要?用 Rust 重写是否有害?

欢迎您参加,并积极展开讨论吧!

“请回复评论 或 你可以另外写文章来参与讨论!

漫画解析

在 Rust 语言 1.0 发布的两三年内,Rust 社区中出现了一批狂热粉丝,他们经常跑去 GitHub 上一些知名开源项目下,逢人就说,请用 Rust 重写,或者,质问为什么项目不用 Rust 重写之类的话,于是社区就形成了一个梗:RIIT,Rewrite it in Rust

此举当然遭到了很多人的反感,甚至有人吐槽:“这些 Rust 狂热粉丝让我好几年都不敢碰 Rust 语言”。其实这类事情,不仅仅在 Rust 语言社区发生过,任何群体里,都会有狂热粉,会质问别人为什么做的和他们不一样。

之前还有人也作了一幅漫画来讽刺 RIIR,内容大概是俩人在卫生间相遇,其中一人安利另外一个人项目用 Rust 重写。但其实现实中,在洗手间安利或询问 Rust 使用情况的场景,大多数发生在 Rust 线下交流会,如本期漫画第五幅所示。

抛开这个梗,我们今天要严肃地讨论一下 Rust 重写的必要性!

《Rewrite it in Rust 有害?》论文解读

起因是,我最近看到今年上半年有一篇匿名作者写的论文[《"Rewrite it in Rust" Considered Harmful ?》](https://goto.ucsd.edu/~rjhala/hotos-ffi.pdf "《"Rewrite it in Rust" Considered Harmful ?》") 。文章讨论了将 C/C++ 代码迁移到 Rust 时,需要在 Rust 和 C/C++ 的接口(FFI)引入的潜在安全问题。因为不可能将全部 C/C++ 代码都用 Rust 重写,目前主流的方式就是用 Rust 重写一部分在未来还需要持续维护和发展的代码,所以与 C/Cpp 安全交互是目前无法避免的。

文章首先指出,尽管用 Rust 重写代码可以提高内存安全性,但在 Rust 和 C/C++ 组件交互的界面仍可能引入新的 Bug。Rust 和 C/C++ 在内存管理、类型系统和控制流方面存在差异,手写的“胶水代码”很容易破坏一些关键的不变量。

比如 C 语言中存在代码:

代码语言:javascript
复制
void add_twice(int *a, int *b) {
  *a += *b;
  *a += *b; 
}

C 中的这个 add_twice 函数参数会产生别名。

代码语言:javascript
复制
#[no_mangle] 
pub extern "C" fn add_twice(a: &mut i32, b: &i32) {
  *a += *b;
  *a += *b;
}

而 Rust 中 add_twice 函数的两个参数则不会产生别名,但是将改 Rust 函数导出 C-ABI 函数给 C 用,如果 C 那边传入 add_twice(&bar, &bar) 这样的调用,则会破坏 Rust 函数的别名模型,导致未定义行为。

这就是 FFI 边界上的内存安全风险。

文章对 FFI Safety 相关安全问题做了一个归类,我们依次来看看。

时空安全问题

以 rustls 库为例,它需要与 C 代码共享证书验证器对象的所有权。rustls 通过 Rust 的Arc计数引用计数智能指针来管理这些对象,以实现多方共享一个验证器。

但是 rustls 通过 FFI 暴露了对应的原始指针,需要客户端代码调 rustls_client_cert_verifier_free 来释放:

代码语言:javascript
复制
pub extern "C" fn rustls_client_cert_verifier_free(
  verifier: *const rustls_client_cert_verifier) {
  // 重新构造Arc并drop,减少引用计数
  unsafe { drop(Arc::from_raw(verifier)) }; 
}

“其实包含这个函数的代码还没有合并:https://github.com/rustls/rustls-ffi/pull/341

这样的代码确实会减少引用计数,但客户端可能错误地调用两次 free 释放同一个指针,或在释放后继续使用指针,从而造成双重释放或使用后释放问题。

“这里其实没有什么理想的解决方案,在 Android 里 Rust 给 Cpp 端共用 Arc 的做法就是直接通过 C-ABI 给 Cpp 透出回调函数来增减引用计数,而非这种 drop 方式。但是也需要 C/C++ 端不要错误调用回调函数。

异常安全问题

Rust 如果发生了跨 FFI 边界的 Panic 会造成未定义行为,但目前处理这类问题主要依赖程序员自己编码。

例如 rustls 库中,rustls_client_cert_verifier_new 函数在克隆证书存储时可能会 Panic:

代码语言:javascript
复制
pub extern "C" fn rustls_client_cert_verifier_new(
  store: *const rustls_root_cert_store) {

  let store: &RootCertStore = try_ref_from_ptr!(store);
  
  // clone可能会panic
  return Arc::into_raw(... store.clone() ...); 
}

但这个函数没有做 Panic 处理,如果 clone 发生 OOM 错误,会直接 Panic 回 C 端,造成未定义行为。

理想的解决方案是:在 FFI 边界自动捕获 Panic,并把错误信息传递给 C/C++端。但 Rust 本身没有提供这方面的支持,完全依赖程序员自己实现。

“其实反过来在 C/Cpp 端也是一样,需要自动捕获异常,传给 Rust 错误码。

Rust 类型不变量

Rust 代码往往高度依赖类型系统所保证的不变量,借此确保内存安全和代码正确性。由于 C/C++ 程序通常不一定遵循这类不变量,因此 C/C++ 在与 Rust 代码交互时可能引发冲突,这类问题在用 Rust 重写后尤其需要重点考虑

看下面这个来自 encoding_c 库的 decoder_decode_to_utf8 FFI函数:

代码语言:javascript
复制
pub unsafe extern "C" fn decoder_decode_to_utf8(
  src: *const u8, src_len: *mut usize,
  dst: *mut u8, dst_len: *mut usize) {
  
  let src_slice = from_raw_parts(src, *src_len);
  let dst_slice = from_raw_parts_mut(dst, *dst_len);

  // 解码...
}

这个函数使用 from_raw_parts 等不安全函数重建了 Rust 的 slice 类型。但是 Rust 要求src_slicedst_slice 地址不重叠(overlap)以进行编译时优化。

而这个 FFI 函数没有检查指针别名情况,C/C++调用时可能会违反这个不重叠要求,导致未定义行为。

解决方法是对 from_raw_parts 的参数进行安全判断,确保其不为空,且地址没有重叠等安全条件。

代码语言:javascript
复制
pub unsafe fn from_raw_parts_mut_safe<'a, T> (data: *mut T, len: usize) -> &'a mut [T] 
{
 // todo: requires not_null(data) && valid_cpp_alloc(data, len) 
// && not_aliased(current_refs, data, len) 
// todo: ensures add_to_current_refs(data, len);
}
其他未定义行为

文章提到的其他未定义行为包括:

  1. ABI兼容性问题:不同编译器对 ABI 级别的优化处理可能不兼容,导致跨语言调用时 ABI 参数传递出错。例如 C 编译器会将多个 32 位参数打包到 64 位寄存器中,而 Rust 不会进行这样的优化。如果两边不一致就可能出错。
  2. 空指针访问:FFI 函数中没有充分校验指针参数是否为 null 就直接解引用,可能导致空指针访问错误。
  3. 缓冲区切片不当 :没有正确检查 bounds 就通过 from_raw_parts 创建缓冲区切片,可能会访问到不属于该缓冲区的内存。
  4. 移动语义错误:Rust 的移动语义要求在移动后不能再访问变量,但 FFI 代码可能错误地继续使用已经 move 了的变量。
  5. 粘合代码问题:很多问题源自需要通过 unsafe 代码进行参数转换和重建 Rust 抽象的粘合代码。这些转换做了许多假设,容易被 C/C++ 端的非法参数破坏。

这一类问题更加隐晦和具体,但也可能导致严重后果。

小结

文章提出了一个 R3 系统来帮助解决这些安全问题,该系统主要包含两部分内容:

  1. C/C++ 端的分配追踪器(allocator tracker)

这个组件可以跟踪C/C++应用中的内存分配情况,这样 Rust 端的 FFI 代码可以查询分配的元数据,确保满足Rust的一些安全不变式。例如跟踪已经转换到 Rust REFERENCE的指针,避免C 端释放 Rust 还在使用的内存导致的错误。

  1. Rust端的细化类型系统(refinement type system)

这个类型系统为 Unsafe 的 FFI 函数添加细化类型注解,确保 Rust 端编写的 FFI 代码进行了必要的安全检查。例如要调用一个 unsafe 函数之前,必须通过分配追踪器验证指针参数的有效性。

细化类型允许在普通类型上添加 Predicate 约束,这样可以表示更严格的类型集合。类型检查器会要求明确验证那些 Predicate 才能通过。

总之,R3系统通过在FFI边界两侧增加自动化的静态和动态检查,可以大幅减少手写FFI胶水代码时引入的安全问题。

“关于 Rust 重写有害论,有人给出一个典故来类比 Rust 重写的重要性。很久之前,外科医生在进行侵入性手术之前并没有洗手的规定,因此许许多多的人因此而丧生。一位医生发现洗手可以避免这种问题,然而几乎每个人都与这位洗手倡导者进行了斗争,试图羞辱和抹黑他,试图制造胡说八道的证据来说洗手没有用,试图说服每个人洗手无关紧要,同时却对自己否认它的有效性。只有在那一代外科医生退休后,他们才一致开始洗手。这位医生叫 [Semmelweis](https://en.wikipedia.org/wiki/Ignaz_Semmelweis[1] 据称,1865年,Semmelweis 变得越来越直言不讳,最终遭遇了精神崩溃,并被同事送进了精神病院。

Rewrite it in Rust 是否有必要

从内存安全角度看,RIIR 是很有必要的

论文中提到的问题,确实是存在的。但是作者给出的 R3 系统也仅仅停留在概念层面(至少我没发现 R3 系统的存在)。

目前企业和社区中 Rust 与 C/Cpp 安全交互主要有三种方法:

  • 建立 《Rust 编码规范》 和 《Unsafe 代码安全评审指南》,并加强 Unsafe 代码安全评审
  • 建立 Rust 和 C++ 安全互操的库,比如 cxx / autocxx/ crubit[2];采用静态检测工具来发现 Unsafe 中的问题,比如 Miri 和 kani[3]
  • 建立从 Unsafe 向 Safe 逐步演化的机制,从而让 Unsafe 逐步消失。

关于第一条,各个公司应该有自己的《Rust 编码规范》,比如 Google、facebook和华为等,只不过没有开源出来。我为此很早就创建了开源的《Rust 编码规范》[4] ,最近又新创建了《Rust 代码 Review 指南》[5],欢迎大家一起贡献。

关于第二条`cxx` [6] 一直是 dtolnay 的个人项目,但是大公司比如 Google ,已经将其应用于 Android 里了。cxx 其实并不是非常通用,它也是基于 C-ABI ,提供了一些自动的安全封装。对于一些遗留的 Cpp 组件,并且不打算以及维护,但是还必须依赖它的时候,cxx 是最适合的。autocxx 是基于 cxx 的一个包装,也是 Google 参与一起搞的,另外 Google 还基于 cxx 搞了另外一个 Crubit 的库,目前应该是实验性,是为了提供更好更安全的 Rust 和 Cpp 交互。

关于第三条,建立从 Unsafe 向 Safe 逐步演化的机制具体是什么意思?因为现阶段 Unsafe 是无法被消除的,所以一个方法是,像 Rust for linux 那样,先创建一个 kernel-rs crate,这个 crate里面,对 Linux 的 kernel api 进行了封装,充分考虑了 Rust 和 C 的 FFI 边界安全条件,进行了安全抽象,对外只提供 Safe Rust API ,从而形成 kernel-rs。这样一来,Linux 内核的 Rust 开发者直接拿着 kernel-rs 开发,就不需要接触 Unsafe Rust 了。这个方法蕴含了一个思想,就是将 Unsafe 交给更专业的人来处理,其他人使用 Safe

对企业以及开源项目来说,这三种方法是可以同时实施的,以此来保证安全。相比于继续使用 C/Cpp 来说,用 Rust 重写带来的安全价值,更加丰厚。因为 Rust 在语言层面和社区文化都将促使开发者去充分的考虑安全问题,并给出最佳实践。即便无法百分百解决安全问题,那也是向百分百安全无限接近中。

Google 这类巨头已经给出了成效:Android 13 代码中引入了 150 多万行代码,消除了内存安全问题,安全 Bug 为零。话说回来,如果 Google 没有人认为现有的代码库中存在内存安全隐患,他们就不会将 C/Cpp 代码重写为 Rust ;他们之所以重写,是因为他们认为结果将会包含更少的隐患,即使考虑到FFI边界可能存在的问题。

“延伸:sudo-rs[7]ntpd-rs[8] 这两个互联网基础工具也用 Rust 重写了。

从软件工程角度来看,RIIR 是很有必要的

除了避免内存不安全(包括并发)问题之外,事实上 Rust 在其他方面也表现出色,比如避免逻辑错误。

Rust 标准库和生态系统遵循着使正确的事情变得容易,而尽可能让错误的事情变得不可能的哲学。这得益于非常表达力的类型系统。

当然,在任何语言中都可能存在逻辑错误,不建议用 Rust 重写经过实战验证的 C/Cpp/Java 应用程序。但是,如果你已经决定重写(或开始一个全新的项目),那么选择Rust不仅仅是因为内存安全,还有更多的原因。

以下是你除了内存安全之外还值得选择 Rust 的原因:

  • 工程性:Rust 的 trait 系统,促使开发者抛开继承去面向接口面向组合设计系统架构,这样可以降低系统耦合,让扩展更加容易。配合其他特性,让 Rust 代码更具可维护性。
  • 健壮性:强大的类型系统和优雅的错误处理结合,促使开发者认真思考和设计系统中的错误处理。

我这里就不一一展开了,在未来的文章或者我的书里,会对此进行详细的展开。

后记

是否选择 Rust ,是否用 Rust 重写,选择权在你!但是为你的系统选择合适的语言,决定了你的系统可以走多远,因为语言是一切的基础。

感谢阅读!

参考资料

[1]

Semmelweis: https://en.wikipedia.org/wiki/Ignaz_Semmelweis

[2]

crubit: https://github.com/google/crubit

[3]

kani: https://github.com/model-checking/kani

[4]

《Rust 编码规范》: https://github.com/Rust-Coding-Guidelines/rust-coding-guidelines-zh

[5]

《Rust 代码 Review 指南》: https://github.com/ZhangHanDong/rust-code-review-guidelines

[6]

cxx : https://github.com/dtolnay/cxx

[7]

sudo-rs: https://github.com/memorysafety/sudo-rs

[8]

ntpd-rs: https://github.com/pendulum-project/ntpd-rs

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2023-10-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 觉学社 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 漫画赏析
  • 漫画解析
    • 《Rewrite it in Rust 有害?》论文解读
      • Rewrite it in Rust 是否有必要
        • 后记
          • 参考资料
      相关产品与服务
      检测工具
      域名服务检测工具(Detection Tools)提供了全面的智能化域名诊断,包括Whois、DNS生效等特性检测,同时提供SSL证书相关特性检测,保障您的域名和网站健康。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档