原文链接:https://without.boats/blog/two-memory-bugs-from-ringbahn/

原文标题:Two Memory Bugs From Ringbahn


公众号:Rust 碎碎念


翻译: Praying

在实现ringbahn[1]的时候,我引入了至少两个 bugs,这些 bugs 引发了内存安全错误,导致段错误,分配器中止以及匪夷所思的未定义行为。我已经修复了我所能找到的 bugs,现在我也无法证明代码库中是否有更多的内存安全问题(当然,这并不意味着没有),我想记录下这两个 bugs,因为它们有一个共同点:它们都是由析构器(destructor)引起的。

Bug #1: 析构器在赋值后运行(二次释放)

这是一个比较经典的 bug,基本上每个正在写 unsafe 代码的人都会知道。有 bug 的代码看起来像下面这样:

let data = self.read_buf.buf.as_mut_ptr();
let len = self.read_buf.buf.len();
self.completion.cancel(Cancellation::buffer(data, len));
self.read_buf = Buffer::new();

这段代码是Ring类型(API 后来改变了)上的 cancellation 的实现的一部分。在 ringbahn 中,如果 IO 当前正在运行,它正在使用一个 buffer,但是程序取消了它对这个 IO 的关注,这个 IO 的完成会被一个 cancellation 对象处理,该对象将被用于在 IO 完成时清理对应的 buffer。这个代码构造了一个 cancellation,并将其传递给 completion,并且使用一个新的 buffer 来替换原有的 buffer。

这听起来很好,但是会出现问题,因为赋值(assignment)的语义。在 Rust 中,当一个字段被重新赋值时,这个字段的前一个值会调用析构器。因此,在这段代码中,当我们将read_buf重新赋值时,我们传递给 cancellation 的 buffer 立即就被释放了。然后,当 IO 完成时,我们再次释放了这个 buffer,导致了二次释放。

相同的 bug 在这个文件中出现了两次:类似的一段代码以相同的方式取消关注的读取事件。

解决方案:ptr::write

解决方法是把最后一行代码使用ptr::write的调用来替换:

let data = self.read_buf.buf.as_mut_ptr();
let len = self.read_buf.buf.len();
self.completion.cancel(Cancellation::buffer(data, len));
ptr::write(&mut self.read_buf, Buffer::new());

函数ptr::write行为很像赋值:它把第二个参数的值写到第一个参数的地址。但是,和赋值不同,它不会运行前一个值的析构器。这是ptr::write和一个赋值操作最大的不同。

这看起来不是很直观,但它是在写 unsafe 代码时一个需要记住的重要技巧:如果你想要要对一个值重新赋值,但是你不想运行前一个值的析构器,你需要使用ptr::write

Bug #2: 析构器引用一个释放过的对象(释放后使用)

第二个 Bug 出现于下面这段代码中:

let mut state = self.state.as_ref().lock();
if matches!(&*state, State::Completed(_)) {
    callback.cancel();
    self.deallocate();
} else {
    *state = State::Cancelled(callback);
}

这段代码实现了我们在前面的代码示例中调用的Completion::cancel方法。 一个 completion 的state字段是一个NonNull<Mutex<State>>,它指向一个表示 completion 的状态的枚举。当一个 completion 被取消的时候,我们获取一个在 completion 上的锁,然后检查它是否完成。如果它还没有完成,我们存储一个回调(callback)用于完成时调用。但是如果它已经完成(意味着这个 IO 完成和我们对它的取消并发地进行),我们调用回调(通过它的cancel方法)并且然后析构这个完成,清理和这个 IO 事件相关的所有资源。

问题在于,state变量是一个MutexGuard,包装了(wrapping)了对我们的 completion 的状态的锁定访问。state变量的析构器会到函数完成时才会调用,并且当它调用的时候,它将会修改已经锁定的 Mutex 的状态。但是,当我们调用self.deallocate时我们已经释放了那个 mutex。这意味着,此时出于其他目的而修改任意的已被使用的状态会是一个释放后使用(use after free)。

这个 bug 也发生了两次:在 completion 模块的两一个函数,我们也统一在丢弃 mutexguard 之前释放了这个 completion。

解决方案:mem::drop

解决方案是在释放 completion 之前,插入一个mem::drop的调用,如此一来,析构器就能够被保证在 mutex 释放之前运行。现在代码像下面这样:

let mut state = self.state.as_ref().lock();
if matches!(&*state, State::Completed(_)) {
    callback.cancel();
    drop(state);
    self.deallocate();
} else {
    *state = State::Cancelled(callback);
}

这将析构器按照正确的顺序排序,因此对 mutex 状态的写操作会在 mutex 被释放之前发生。

从技术上来讲,我们同样能够mem::forget这个 MutexGuard:因为我们正在释放这个 Mutex,所以我们指定其他试图尝试获取锁的行为都不会发生,并且释放锁也是白费功夫。我是在写这篇博客的时候才有了这个想法。

对此我们还能做什么?

我觉得有趣的是,我在我的代码中发现的两个内存错误都是因为运行了析构器。从某种意义上来说,这并不奇怪:对面前的代码进行推理是一回事,但对编译器隐式插入在你的程序中的代码进行推理又是另一回事。

在 safe 代码中,析构器很好,也是 Rust 的强大能力之一:正如 Yehuda Katz 以前写的那样,能够让(程序员)在大多数情况下可以不担心资源清理是非常棒的。但 unsafe 代码是另一回事,其中关于别名的保证会变得相当混乱。如果能在一些作用域中开启一个 lint,用以警告我是否析构器被插入到我的代码中,那就太好了。

(顺便提一下,对于类型理论的爱好者来说,这个 lint 会有效地把 Rust 的 “仿射(affine)”类型变成 “线性(linear)”类型。我认为 Rust 选择将不可复制的类型变成 “仿射(affine)”类型在总体上是正确的选择,但这表明在某些情况下,额外的线性检查是非常有价值的。)

参考资料

[1]

ringbahn: https://github.com/withoutboats/ringbahn

【译】Ringbahn的两个内存Bug的更多相关文章

  1. C++中两块内存重叠的string的copy方法

    如果两段内存重叠,用memcpy函数可能会导致行为未定义. 而memmove函数能够避免这种问题,下面是一种实现方式: #include <iostream> using namespac ...

  2. 两次内存断点法寻找OEP

    所谓“两次内存断点法寻找OEP”,按照<加密与解密*第三版>上的解释来说,就是这样的.一般的外壳会依次对.text..rdata..data..rsrc区块进行解压(解密)处理,所以,可以 ...

  3. 关于VAD的两种内存隐藏方式

    Windows内核分析索引目录:https://www.cnblogs.com/onetrainee/p/11675224.html 技术学习来源:火哥(QQ:471194425) 内存在0环的两种内 ...

  4. 脱壳实践之寻找OEP——两次内存断点法

      0x00 前言 对于加壳程序第一件事就是要找到OEP(oringinal Entry point),由于加壳的缘故,当PE文件载入OD或者其他调试软件时进入的的往往是壳程序的入口地址.所以要进行逆 ...

  5. 一个诡异的MySQL查询超时问题,居然隐藏着存在了两年的BUG

    这一周线上碰到一个诡异的BUG. 线上有个定时任务,这个任务需要查询一个表几天范围内的一些数据做一些处理,每隔十分钟执行一次,直至成功. 通过日志发现,从凌晨5:26分开始到5:56任务执行了三次,三 ...

  6. Erlang 程序引发共享内存 bug 的一个例子

    虽然 Erlang 的广告说得非常好,functional.share-nothing.消息传递,blah blah 的,好像用 Erlang 写并发程序就高枕无忧了,但是由于 Erlang 信奉高度 ...

  7. (原创)spring mvc和jersey rest 组合使用时单例对像实例化两次的BUG及解决办法

    项目中没用spring 的restTemplate 而是采用 jersey来做rest 的实现,一直用着,也没发现有什么不对,后来加入了,以quartz用硬编码方式实现,结果启动项目的时候报错 ,具体 ...

  8. react-router3.x hashHistory render两次的bug,及解决方案

    先写一个简单App页面,其实就是简单修改了react-router的官方例子中的animations例子,修改了两个地方: 1.路由方式由browserHistory修改为hashHistory 2. ...

  9. 译:Missing index DMV的 bug可能会使你失去理智---慎重看待缺失索引DMV中的信息

    注: 本文译自https://www.sqlskills.com/blogs/paul/missing-index-dmvs-bug-that-could-cost-your-sanity/ 原文作者 ...

随机推荐

  1. linux上部署springboot项目

    1.安装jdk,请参考个人博客linux安装jdk 2.安装mysql,请参考个人博客 linux安装mysql 3.项目打包(使用idea) 打开项目,点击idea右边Maven Projects菜 ...

  2. 部署项目到服务器 & 搭建博客网站

    搭建博客网站 作为名程序员,或者是网络编程爱好者,拥有一个自己的博客网站再好不过,本篇文章手把手教你部署自己的网站

  3. matlab中num2str 将数字转换为字符数组

    参考:https://ww2.mathworks.cn/help/matlab/ref/num2str.html?searchHighlight=num2str&s_tid=doc_srcht ...

  4. tomcat:tomcat安装(在一台电脑上安装两个tomcat)

    1.安装前的说明 (1)在安装第二个tomcat之前,我们要知道安装一台tomcat的时候需要在电脑上添加两个系统变量 然后在path中配置: (2)这个时候我们就要思考了,当安装第二台服务器的时候首 ...

  5. RHSA-2017:1931-中危: bash 安全和BUG修复更新(代码执行)

    [root@localhost ~]# cat /etc/redhat-release CentOS Linux release 7.2.1511 (Core) 修复命令: 使用root账号登陆She ...

  6. 通过MapReduce降低服务响应时间

    在微服务中开发中,api网关扮演对外提供restful api的角色,而api的数据往往会依赖其他服务,复杂的api更是会依赖多个甚至数十个服务.虽然单个被依赖服务的耗时一般都比较低,但如果多个服务串 ...

  7. 爬虫之Selenium

    简介 selenium最初是一个自动化测试工具,而爬虫中使用它主要是为了解决requests无法直接执行JavaScript代码的问题 selenium本质是通过驱动浏览器,完全模拟浏览器的操作,比如 ...

  8. 如何把C++的源代码改写成C代码?而C改C++只需一步!

    ★ 如何把C++的源代码改写成C代码? C++解释器比C语言解释器占用的存储空间要大,想要在某些特定场合兼容C++代码,同时为了节省有限的存储空间,降低成本,也为了提高效率,将用C++语言写的源程序用 ...

  9. 单调队列+线性dp题Watching Fireworks is Fun (CF372C)

    一.Watching Fireworks is Fun(紫题) 题目:一个城镇有n个区域,从左到右1编号为n,每个区域之间距离1个单位距离节日中有m个烟火要放,给定放的地点ai,时间ti当时你在x,那 ...

  10. Linux系统编程—信号捕捉

    前面我们学习了信号产生的几种方式,而对于信号的处理有如下几种方式: 默认处理方式: 忽略: 捕捉. 信号的捕捉,说白了就是抓到一个信号后,执行我们指定的函数,或者执行我们指定的动作.下面详细介绍两个信 ...