【译】Ringbahn的两个内存Bug
原文链接: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)”类型在总体上是正确的选择,但这表明在某些情况下,额外的线性检查是非常有价值的。)
参考资料
ringbahn: https://github.com/withoutboats/ringbahn
【译】Ringbahn的两个内存Bug的更多相关文章
- C++中两块内存重叠的string的copy方法
如果两段内存重叠,用memcpy函数可能会导致行为未定义. 而memmove函数能够避免这种问题,下面是一种实现方式: #include <iostream> using namespac ...
- 两次内存断点法寻找OEP
所谓“两次内存断点法寻找OEP”,按照<加密与解密*第三版>上的解释来说,就是这样的.一般的外壳会依次对.text..rdata..data..rsrc区块进行解压(解密)处理,所以,可以 ...
- 关于VAD的两种内存隐藏方式
Windows内核分析索引目录:https://www.cnblogs.com/onetrainee/p/11675224.html 技术学习来源:火哥(QQ:471194425) 内存在0环的两种内 ...
- 脱壳实践之寻找OEP——两次内存断点法
0x00 前言 对于加壳程序第一件事就是要找到OEP(oringinal Entry point),由于加壳的缘故,当PE文件载入OD或者其他调试软件时进入的的往往是壳程序的入口地址.所以要进行逆 ...
- 一个诡异的MySQL查询超时问题,居然隐藏着存在了两年的BUG
这一周线上碰到一个诡异的BUG. 线上有个定时任务,这个任务需要查询一个表几天范围内的一些数据做一些处理,每隔十分钟执行一次,直至成功. 通过日志发现,从凌晨5:26分开始到5:56任务执行了三次,三 ...
- Erlang 程序引发共享内存 bug 的一个例子
虽然 Erlang 的广告说得非常好,functional.share-nothing.消息传递,blah blah 的,好像用 Erlang 写并发程序就高枕无忧了,但是由于 Erlang 信奉高度 ...
- (原创)spring mvc和jersey rest 组合使用时单例对像实例化两次的BUG及解决办法
项目中没用spring 的restTemplate 而是采用 jersey来做rest 的实现,一直用着,也没发现有什么不对,后来加入了,以quartz用硬编码方式实现,结果启动项目的时候报错 ,具体 ...
- react-router3.x hashHistory render两次的bug,及解决方案
先写一个简单App页面,其实就是简单修改了react-router的官方例子中的animations例子,修改了两个地方: 1.路由方式由browserHistory修改为hashHistory 2. ...
- 译:Missing index DMV的 bug可能会使你失去理智---慎重看待缺失索引DMV中的信息
注: 本文译自https://www.sqlskills.com/blogs/paul/missing-index-dmvs-bug-that-could-cost-your-sanity/ 原文作者 ...
随机推荐
- hystrix 源码分析以及属性的配置
一.feign与hystix结合 1.1测试环境搭建 架构如图: 非常简单,就是Order服务通过feign调用product服务的一个获取商品信息的一个接口: package com.yang.xi ...
- linux(centos)环境下安装rabbitMq
1.由于rabbitMq是用Erlang语言写的,因此要先安装Erlang环境 下载Erlang :http://www.rabbitmq.com/releases/erlang/erlang-19. ...
- Keil ARm新建项目
一.新建一个工程 选好芯片后确认,完成创建 二.新建一个文件 保存为后缀名为*.c的文件 三.把文件添加进项目里面 四.测试 发现有警告 五.给项目添加特定的文件,去除警告或错误 现在保存项目的文件夹 ...
- Java之微信支付(扫码支付模式二)案例实战
摘要:最近的一个项目中涉及到了支付业务,其中用到了微信支付和支付宝支付,在做的过程中也遇到些问题,所以现在总结梳理一下,分享给有需要的人,也为自己以后回顾留个思路. 一:微信支付接入准备工作: 首先, ...
- Androng,一个针对Android的Pong克隆
下载application from Android market 下载source - 532 KB 内容 IntroductionAndroid游戏开发 活动视图绘图使用CanvasAnimati ...
- JVM系列【4】内存模型
JVM系列笔记目录 虚拟机的基础概念 class文件结构 class文件加载过程 jvm内存模型 JVM常用指令 GC与调优 硬件层数据一致性 - 存储器层次结构 从L6-L0 空间由大变小,速度由慢 ...
- VBScript 教程
VBScript 教程 VB 不区分大小写 变量 普通变量 关键词声明 Dim.Public.Private 赋值动态创建 name = "hello" Option Explic ...
- VMware安装的Linux系统忘记密码 怎么修改root密码
因为昨天新安装过虚拟机设置了新的密码,再加上我好长时间没有用自己旧的虚拟机,导致忘记了密码,原来虽然知道在单用模式下,找回密码,但是确实是自己从来都没有做过,还好我们组大手飞翔哥告诉了我,怎么找回ro ...
- linux(centos8):用systemctl管理war包形式的jenkins(java 14 / jenkins 2.257)
一,如何安装jenkins? 参见: https://www.cnblogs.com/architectforest/p/13685904.html 说明:刘宏缔的架构森林是一个专注架构的博客,地址: ...
- phpstorm10.0.3 下载与激活
phpstorm10.0.3 百度网盘下载 提取码: kqvc 激活服务器: http://jetbrains.tencent.click/ (2016-09-19 可用) http://owo. ...