原文链接: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. TP 3.2 图片处理类

    TP 3.2 图片处理类 <?php namespace Common\Common; class ImageEdit{ /** * [上传图片并生成缩略图] * @param [type] $ ...

  2. React手稿之State Hooks of Hooks

    React Hooks React在16.7.0-alpha.0版本中提到了Hooks的概念,目前还是Proposal阶段. 官方也陈述,接下来的90%的工作会投入到React Hooks中. 从目前 ...

  3. jquery,Datatables插件使用,做根据【日期段】筛选数据的功能 jsp

     时间格式为yyyymmdd,通过转换为int类型进行比较大小 画面: jsp代码: 1 //日期显示控件,使用h-ui框架 2 3 <div class="text-c"& ...

  4. 开源 C#工作流管理平台

    { font-family: 宋体; panose-1: 2 1 6 0 3 1 1 1 1 1 } @font-face { font-family: "Cambria Math" ...

  5. A4988两相四线步进电机驱动模块使用经验

    1.A4988模块可以驱动两相四线步进电机,模块引脚及接线图如下: 2.步进电机引线如下: 3.引脚: ENABLE:低电平有效,用于打开和关闭场效应管的输出: RESET:低电平有效,芯片复位: S ...

  6. java进阶(26)--ForEach

    JDK5.0后新特性 一.普通for循环

  7. Python 的映射数据类型有哪些?零基础小白入门学习必看

    1 映射类关系 Python 的 collections.abc 模块内拥有 Mapping 和 MutableMapping 这两个抽象基类,它们为 dict 和其他类似的类型提供了接口定义. mu ...

  8. git冲突的表现

    <<<<<<< HEAD b789 ======= b45678910 >>>>>>> 6853e5ff961e68 ...

  9. 手写一个HTTP框架:两个类实现基本的IoC功能

    jsoncat: 仿 Spring Boot 但不同于 Spring Boot 的一个轻量级的 HTTP 框架 国庆节的时候,我就已经把 jsoncat 的 IoC 功能给写了,具体可以看这篇文章&l ...

  10. “3D引擎和图形学技术点思路讲解”线上直播培训班报名开始啦(完全免费)

    大家好,我开了一个线上的直播课程培训班,完全免费,欢迎大家报名! 本课程重点教授"光线追踪"方面的实现思路. 我的相关经验 5年3D引擎开发经验 Wonder-WebGL 3D引擎 ...