使用Semgrep检测Goroutine泄漏问题

本文详细介绍了如何利用Semgrep静态分析工具检测Go语言中常见的goroutine泄漏问题,包括问题原理分析、实际代码示例演示以及定制化Semgrep规则编写方法,帮助开发者避免因通道使用不当导致的资源耗尽风险。

使用Semgrep发现Goroutine泄漏

在Java或C++中编写多线程代码可能会让计算机专业学生重新考虑职业选择,而在Go语言中异步调用函数只需在函数调用前添加go关键字。然而编写并发Go代码也存在风险,恶性的并发错误可能悄悄潜入应用程序。不知不觉中,数千个挂起的goroutine会拖慢应用性能,最终导致崩溃。

本文提供的Semgrep规则可用于错误排查,并包含我们在审计中使用的专用Semgrep规则库链接。同时解释了如何使用其中一条规则来发现Go中特别棘手的一类错误:goroutine泄漏。

Goroutine泄漏原理

Go中最著名的并发错误是竞态条件,而goroutine泄漏虽然常见却很少被讨论。部分原因是goroutine泄漏的后果只有在多次发生后才会显现,开始以明显的方式影响性能和可靠性。

goroutine泄漏通常源于不正确地使用通道来同步goroutine间的消息传递。当应该使用缓冲通道时却使用了无缓冲通道,这类问题经常发生。这种错误可能导致goroutine挂起在内存中,最终耗尽系统资源,导致系统崩溃或拒绝服务。

实际案例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import (
  "fmt"
  "runtime"
  "time"
)

func main() {
  requestData(1)
  time.Sleep(time.Second * 1)
  fmt.Printf("Number of hanging goroutines: %d", runtime.NumGoroutine() - 1)
}

func requestData(timeout time.Duration) string {
 dataChan := make(chan string)

go func() {
     newData := requestFromSlowServer()
     dataChan <- newData // 阻塞
 }()
 select {
 case result := <- dataChan:
     fmt.Printf("[+] request returned: %s", result)
     return result
 case <- time.After(timeout):
     fmt.Println("[!] request timeout!")
         return ""
 }
}

在上述代码中,第21行的通道写操作阻塞了包含它的匿名goroutine。第19行声明的goroutine将被阻塞,直到在dataChan上发生读操作。这是因为使用无缓冲通道时读写操作会阻塞goroutine,每个写操作必须有一个对应的读操作。

导致匿名goroutine泄漏的两种场景:

  1. 如果第二个case在24行的读操作之前发生,requestData函数将退出,其中的匿名goroutine会被泄漏
  2. 如果两个case同时触发,调度器会随机选择其中一个。如果选择第二个case,匿名goroutine将被泄漏

使用缓冲通道可以解决上述问题。只需在第17行为通道添加长度:

1
2
func requestData(timeout time.Duration) string {
 dataChan := make(chan string, 1)

发现错误

调试并发问题非常复杂,但当我们仔细检查野外发现的常见Go并发错误时,我们确定了可以轻松利用的模式来创建Semgrep规则。这些规则使我们能够发现这类复杂错误,因为Go并发错误通常可以用几组简单模式来描述。

构建Semgrep规则时需要考虑goroutine泄漏的典型表现:

  1. 声明了类型为T的无缓冲通道C
  2. 在匿名goroutine G中执行对通道C的写/发送操作
  3. 在select块(或G之外的其他位置)读取/接收C
  4. 程序遵循一个执行路径,其中C的读操作在封闭函数终止前没有发生

检测示例中的goroutine泄漏可以使用以下模式:

1
2
3
4
5
6
- pattern-inside: |
    $CHANNEL := make(...)
    ...
- pattern-not-inside: |
    $CHANNEL := make(..., $T)
    ...
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计