V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
darluc
V2EX  ›  Go 编程语言

「译文」理解现实中 Go 语言的并发漏洞

  •  
  •   darluc · 2019-07-26 22:52:11 +08:00 · 2752 次点击
    这是一个创建于 1727 天前的主题,其中的信息可能已经有所发展或是发生改变。

    查看全文

    对于编程(数据)模型的设计不仅会使得一些问题变得易于(或更难)解决,也会导致某些类型的漏洞更容易(或更难)产生、侦测和修复。本文的研究对象就是 Go 语言的并发机制。在深入研究之前先思考一下会更有意思,你现在对 Go 语言可能会有以下几点认识:

    • Go 语言明显是被设计为服务于并发编程的,想要使其变得更简单且不易出错
    • Go 语言确实让并发编程变更简单且不易出错了
    • Go 程序大量使用 channel 的消息传递机制,相较于共享内存的同步机制,它更不容易出错
    • Go 程序的并发漏洞更少
    • Go 语言内置的对于死锁和数据竞争的侦测能捕获所有(绝大多数)的代码漏洞

    这些陈述中的第一点是毋庸置疑的。至于其它几点,你可以先参考本文的研究数据,再重新评估一下你还会有多坚决地继续持有这些观点...

    我们使用真实的 Go 程序应用来进行对于并发漏洞的第一个系统性研究。我们研究了六个 Go 的软件(项目),包括:Docker,Kubernetes 和 gRPC。我们一共分析了 171 个并发相关的漏洞,其中超过一半是非传统的、Go 语言特有的问题。除了造成这些漏洞的根本原因外,我们还研究了它们的修复方式,通过实验重现这些漏洞,并使用两个公开的 Go 漏洞侦测工具进行了漏洞扫描。

    这六个用于研究的应用是:Docker,Kubernetes,etcd,CockroachDB,gRPC 以及 BoltDB,显然这些都是现实世界中重量级的 Go 代码。

    在继续研究它们的并发漏洞之前,我们先从研究这些应用实际是如何使用 Go 的并发原语开始。这些漏洞可以从两个主要的维度进行分类:表现行为(阻塞或非阻塞),以及造成问题的并发原语的机制(共享内存或消息传递)。我们先快速回顾一下 Go 语言的主要并发机制。

    Go 的并发机制

    Go 语言的一个主要设计目的,就是改进传统的多线程编程方式,简化并发编程使其不易出错。为了达到这个目的,Go 语言将它的多线程设计汇聚在了两点原则上:1 )使线程(称之为 goroutines,go 协程)变得轻量且易于创建; 2 )使用显式的消息传送(通过 channels 实现)进行线程通信。

    Go 协程是轻量的用户态线程(「绿色」线程)。在函数调用(包括匿名函数)前面加上 go 关键字,就能创建一个协程。匿名函数可以访问到在其之前申明的本地变量,而且它们是被共享使用的。Channels 用于在协程之间传送数据和状态,而且可以使用缓冲或不使用缓冲。当使用无缓冲 channel 的时候,一个协程在发送(或者接收)时会被阻塞,直到其它的协程进行了数据接收(或者发送)。select 语句可以让一个 go 协程同时监听多个 channel,如果多于一个 channel 可用的时候,Go 会随机选择一个分支执行。Go 语言还支持传统的同步机制原语包括互斥,条件变量和原子变量。

    Go 并发原语在实践中的应用

    这六个应用都大量使用了 Go 协程,尤其是用于匿名方法。

    在研究 gRPC 的时候,由于它既有 C 的实现也有 Go 的实现,比较起来结果就很有趣。下面的表格展示了处理相同数量的请求时,使用 gRPC-Go 和 gRPC-C 创建的协程数量比率。

    在对比表格中,go 协程相比 C 版本创建的线程有更短的生命周期,但是创建的频度更高。这种高频繁使用协程的行为是 Go 语言所推崇的。

    如果我们审视所有这些应用对并发原语的使用统计,会有一个更加令人惊讶的发现,共享内存的同步操作仍然比消息传递使用得多。

    最常出现的消息传递原语是 chan,它的使用量中在所有应用中占比 18.5% 到 43%。所以,现在的情形是传统的共享内存方式的通信还是被大量使用,与大量的消息传递原语同时并存。从漏洞的角度来看,我们拥有了不漏洞类型发生的可能性数据:共享内存通信造成的,消息传递造成的以及两者共同作用造成的!

    Go 并发漏洞

    作者搜索了这些应用的 Github 提交历史,从中找到了修复并发漏洞的提交(共 3211 个)。从这些漏洞中随机选取了 171 个用于研究。

    这些漏洞被分为阻塞漏洞和非阻塞漏洞。当一个或多个协程在执行中意外卡主无法推进时,阻塞漏洞就产生了。这个定义比死锁更宽泛,包含了循环等待以外的情况,但是不包括对其它非协程提供资源的等待。其中包含 85 个阻塞漏洞和 86 个非阻塞漏洞。

    我们还从另一个维度对漏洞进行了划分,看它们是否与共享内存保护相关( 105 个)还是和消息传递相关( 66 个)。

    阻塞型漏洞

    首先让我们来看看阻塞漏洞,其中的 42% 都与共享内存相关,另外 58% 与消息传递相关。上文提到过共享内存原语实际上比消息传递原语使用得更多。

    与普遍认为的消息传递不易犯错相比,我们的研究显示更多的阻塞漏洞是由错误的消息传递造成的,而不是由错误的共享内存保护造成的。

    共享内存相关的漏洞包括一般的常见情况,和因 Go 语言中 RWMutext 和 Wait 的实现而产生的新情况。

    对于消息传送相关的漏洞,许多都是因为 channel 丢失了发送或接受方,或者忘记了关闭 channel。

    所有消息传递引起的阻塞漏洞都与 Go 特有的消息传递语法相关,比如 channel。这些漏洞很难发现,尤其是当消息传递和其它的同步原语一起使用的时候。与一般的认识不同,消息传递会比共享内存方式造成更多的阻塞漏洞。

    在调查了这些漏洞的修复之后,会发现弄明白漏洞产生的原因之后,它们修复起来都相当简单,而且修复的类型都与造成漏洞的起因相关。这说明在 Go 语言中,使用全自动或半自动的工具修复阻塞型漏洞是很有前景的一个方向。

    Go 的内置死锁探测器只能检测到此次研究中 21 个阻塞漏洞中的两个。

    非阻塞型漏洞

    与消息传递相比,非阻塞型漏洞更多是由于共享内存的错误使用造成的。有一半的非阻塞漏洞符合「传统」的内存共享漏洞模式。还有一些漏洞是由于缺乏对 Go 语言特性的理解,尤其是前置申明的本地变量,在协程中被调用的匿名函数共享使用,以及 WaitGroup 的语法。

    Go 语言为简化多线程编程而引入的新编程模型和新工具库,可能造成更多的并发漏洞。

    消息传送型的非阻塞漏洞则相对不那么常见,“编程语言中这些复杂的消息传递机制,与其它的语言特性组合起来,可能造成这些漏洞很难被发现”

    有趣的是,修复共享内存漏洞的程序员,更喜欢使用消息传送机制来修复这些问题。

    Go 语言的数据竞争探测器可以探测出此次研究中一半的非阻塞漏洞。

    查看全文

    lhx2008
        1
    lhx2008  
       2019-07-27 15:16:21 +08:00 via Android
    emmm,在说啥?
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   5928 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 27ms · UTC 02:28 · PVG 10:28 · LAX 19:28 · JFK 22:28
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.