使用Semgrep发现Go协程泄漏 - Trail of Bits博客
Alex Useche, 安全工程师 2021年11月8日 semgrep 最初发布于2021年5月10日
虽然在Java或C++中编写多线程代码可能会让计算机科学专业的学生重新考虑职业选择,但在Go中异步调用函数只需在函数调用前加上go关键字。然而,编写并发Go代码也存在风险,因为恶意的并发错误可能会悄悄潜入应用程序。在不知不觉中,可能会有数千个挂起的goroutine拖慢应用程序,最终导致崩溃。
这篇博客文章提供了一个可用于错误搜寻的Semgrep规则,并包含一个我们在审计中使用的专业Semgrep规则库的链接。它还解释了如何使用其中一个规则来发现Go中特别棘手的一类错误:goroutine泄漏。
本文描述的技术灵感来自GCatch,这是一个使用过程间分析和Z3求解器来检测可能导致挂起goroutine的通道误用错误的工具。该工具的技术和开发特别令人兴奋,因为缺乏对由特定Go结构(如通道)不正确使用引起的并发错误的研究。
尽管设置此类工具、运行它并在实际环境中使用的过程本身很复杂,但这是值得的。当我们仔细分析GCatch报告的已确认错误时,我们注意到了它们起源的模式。然后我们能够使用这些模式来发现识别这些错误实例的替代方法。正如我们将看到的,Semgrep是这项工作的好工具,因为它速度快且能够轻松调整Semgrep规则。
Goroutine泄漏解释
也许Go中最著名的并发错误是竞争条件,这通常是由于在循环中使用goroutine时内存别名不当造成的。另一方面,Goroutine泄漏也是常见的并发错误,但很少被讨论。这部分是因为goroutine泄漏的后果只有在发生多次后才变得明显;泄漏开始以明显的方式影响性能和可靠性。
Goroutine泄漏通常是由于不正确使用通道来同步goroutine之间传递的消息而导致的。当应该使用缓冲通道的情况下使用无缓冲通道进行逻辑时,经常会出现此问题。这种类型的错误可能导致goroutine挂起在内存中,并最终耗尽系统资源,导致系统崩溃或拒绝服务状况。
让我们看一个实际例子:
|
|
在上面的代码中,第21行的通道写操作阻塞了包含它的匿名goroutine。第19行声明的goroutine将被阻塞,直到在dataChan上发生读操作。这是因为在使用无缓冲通道时,读写操作会阻塞goroutine,并且每个写操作都必须有相应的读操作。
有两种情况会导致匿名goroutine泄漏:
- 如果第二种情况
case <- time.After(timeout)
在第24行的读操作之前发生,requestData函数将退出,其中的匿名goroutine将被泄漏 - 如果两种情况同时触发,调度器将随机选择两种情况之一。如果选择了第二种情况,匿名goroutine将被泄漏
运行代码时,您将获得以下输出:
|
|
挂起的goroutine是第19行的匿名goroutine。
使用缓冲通道可以解决上述问题。虽然读写无缓冲通道会导致goroutine阻塞,但向缓冲通道执行发送(写)操作仅当通道缓冲区已满时才会导致阻塞。类似地,接收操作仅当通道缓冲区为空时才会导致阻塞。
为了防止goroutine泄漏,我们需要做的就是在第17行的通道中添加一个长度,得到以下代码:
|
|
运行更新后的程序后,我们可以确认不再有挂起的goroutines:
|
|
这个错误可能看起来很小,但在某些情况下,它可能导致goroutine泄漏。有关goroutine泄漏的示例,请参阅Kubernetes存储库中的此PR。在运行1,496个goroutine时,补丁作者经历了由于goroutine泄漏导致的API服务器崩溃。
发现错误
调试并发问题的过程非常复杂,以至于像Semgrep这样的工具可能看起来不适合。然而,当我们仔细检查在野外发现的常见Go并发错误时,我们确定了可以轻松利用来创建Semgrep规则的模式。这些规则使我们能够找到甚至这种类型的复杂错误,主要是因为Go并发错误通常可以用几组简单模式来描述。
在使用Semgrep之前,重要的是要认识到它可以解决的问题类型的限制。在搜索并发错误时,最重要的限制是Semgrep无法进行过程间分析。这意味着我们需要针对包含在单个函数中的错误。在Go中工作时这是一个可管理的问题,不会阻止我们使用Semgrep,因为Go程序员经常依赖在单个函数内定义的匿名goroutine。
现在我们可以开始构建我们的Semgrep规则,基于以下goroutine泄漏的典型表现:
- 声明了一个类型为T的无缓冲通道C
- 在匿名goroutine G中执行对通道C的写/发送操作
- 在select块(或G之外的其他位置)读取/接收C
- 程序遵循一个执行路径,其中在封闭函数终止之前不发生C的读操作
最后一步通常会导致goroutine泄漏。
由上述条件导致的错误往往会在代码中产生模式,我们可以使用Semgrep检测这些模式。无论这些模式采取什么形式,程序中都会声明一个无缓冲通道,我们需要分析:
|
|
我们还需要排除通道声明为缓冲通道的情况:
|
|
要检测我们示例中的goroutine泄漏,我们可以使用以下模式。