原文链接: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. modelviewset views

    Python 1.4创建user/serializers.py写序列化器 from rest_ framework import serializers from user .models impor ...

  2. Solidity智能合约面向对象编程(一、类的创建)

    Solidity编写智能合约 1 pragma solidity ^0.4.4;//版本声明 ^代表向上兼容 pragma代表版本声明 solidity 代表开发语言 2 //定义类 3 contra ...

  3. c#数据处理总结(分组、交并差与递归)

    前言:最近项目比较忙,完全没有时间写下总结笔记,今天抽出时间来写下笔记,供写后台的你来做数据处理后台代码编写的参考. 一.分组 var GroupForList = numberList.GroupB ...

  4. .NetCore.RazorPages 获取访客的公网IP与局域网IP

    dotnet.core 获取访客的公网IP与局域网IP 现在奉上代码 public void OnGet() {var ip = Content(HttpContext.Connection.Remo ...

  5. 014 01 Android 零基础入门 01 Java基础语法 02 Java常量与变量 08 “字符型”字面值

    014 01 Android 零基础入门 01 Java基础语法 02 Java常量与变量 08 "字符型"字面值 字符型 字面值如何表示? 两个关键:单引号(必须是英文单引号). ...

  6. SPI通信基础学习

    SPI是"Serial Peripheral Interface"的缩写,即"串行外设接口",是摩托罗拉公司推出的一种串行接口通信协议. 接线的示意图: SPI ...

  7. QTree1 【题解】

    题目背景 数据规模和spoj上有所不同 题目描述 给定一棵n个节点的树,有两个操作: CHANGE i ti 把第i条边的边权变成ti QUERY a b 输出从a到b的路径中最大的边权,当a=b的时 ...

  8. ASP。NET Core Blazor CRUD使用实体框架和Web API

    下载source code - 1.7 MB 介绍 *请查看我的Youtube视频链接来学习ASP.NET Core Blazor CRUD使用实体框架和Web API. 在本文中,我们将了解如何为A ...

  9. python实现单链表及链表常用功能

    单链表及增删实现 单链表高级功能实现:反序,找中间结点,检测环等 参考: https://github.com/wangzheng0822/algo

  10. 小白使用Hystrix

    Hystrix是什么东西?百度一下: 没错,hystrix是豪猪的意思,作为SpringCloud微服务系统中保持服务稳定的重要组件,正如它的名字一样,它对整个系统起到了保护的作用. 在许多文章当中把 ...