Skip to content
KitploitKITPLOIT
工具博客
提交
工具博客
提交

黑客、渗透测试和网络安全工具,武装您的安全武器库!

Kitploit 是一个黑客、网络安全和渗透测试工具的目录。发现最新的项目更新,查找漏洞、分析系统、自动化测试并加强你的安全。

··订阅源·联系·隐私·© 2026 Kitploit

工具目录

分类

查看所有分类
Loading categories
failpoint — 用于 Go 的故障注入库,通过环境变量添加运行时可控的故障点,以触发 panic、休眠、返回以及条件错误路径。 | Kitploit
工具/GitHubGitHub/pingcap/failpoint
通用工具调试器混沌工程混沌工程 分类第 18 名
GitHubpingcap/failpoint

failpoint

用于 Go 的故障注入库,通过环境变量添加运行时可控的故障点,以触发 panic、休眠、返回以及条件错误路径。

查看仓库
89467301个月前Kitploit 审核通过

最受欢迎

查看全部 →

发现我们社区最常用的工具。

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享

failpoint

LICENSE Language Go Report Card Build Status Coverage Status Mentioned in Awesome Go

Golang 的 failpoints 实现。故障点用于以用户可控的方式在代码中添加可注入错误的位置。故障点是一段代码片段,仅当对应的故障点处于激活状态时才会执行。

快速开始(使用 failpoint-ctl)

  1. 从源码构建 failpoint-ctl

    root@kitploit:~
    git clone https://github.com/pingcap/failpoint.git
    cd failpoint
    make
    ls bin/failpoint-ctl
    
  2. 向你的程序注入故障点,例如:

    root@kitploit:~
    package main
    
    import "github.com/pingcap/failpoint"
    
    func main() {
        failpoint.Inject("testPanic", func() {
            panic("failpoint triggerd")
        })
    }
    
  3. 使用 failpoint-ctl enable 转换你的代码

  4. 使用 go build 构建

  5. 使用 GO_FAILPOINTS 环境变量启用故障点

    root@kitploit:~
    GO_FAILPOINTS="main/testPanic=return(true)" ./your-program
    

    注意:GO_FAILPOINTS 不适用于 InjectCall 类型的标记。

  6. 如果你使用 go run 运行测试,别忘了在命令中加入生成的 binding__failpoint_binding__.go,例如:

快速开始(使用 failpoint-toolexec)

  1. 从源码构建 failpoint-toolexec

    root@kitploit:~
    git clone https://github.com/pingcap/failpoint.git
    cd failpoint
    make
    ls bin/failpoint-toolexec
    
  2. 向你的程序注入故障点,例如:

    root@kitploit:~
    package main
    
    import "github.com/pingcap/failpoint"
    
    func main() {
        failpoint.Inject("testPanic", func() {
            panic("failpoint triggerd")
        })
    }
    
  3. 使用独立的构建缓存以避免与未使用 failpoint-toolexec 的缓存混淆,然后构建

    GOCACHE=/tmp/failpoint-cache go build -toolexec path/to/failpoint-toolexec

  4. 使用 GO_FAILPOINTS 环境变量启用故障点

    root@kitploit:~
    GO_FAILPOINTS="main/testPanic=return(true)" ./your-program
    
  5. 你也可以使用 go run 或 go test,例如:

    root@kitploit:~
    GOCACHE=/tmp/failpoint-cache GO_FAILPOINTS="main/testPanic=return(true)" go run -toolexec path/to/failpoint-toolexec your-program.go
    

设计原则

  • 用有效的 Golang 代码定义故障点,而不是注释或其他任何形式

  • 故障点没有任何额外开销

    • 不会影响常规逻辑
    • 不会导致常规代码性能回退
    • 故障点代码不会出现在最终二进制文件中
  • 故障点例程应可读可写,并应通过编译器检查

  • 由故障点定义生成的代码易于阅读

  • 与注入代码保持相同的行号(更易于调试)

  • 支持使用 context.Context 进行并行测试

关键概念

  • 故障点

    故障点是一段代码片段,仅当对应的故障点处于激活状态时才会执行。 如果执行了 failpoint.Disable("failpoint-name-for-demo"),该闭包将永远不会执行。

    root@kitploit:~
    var outerVar = "declare in outer scope"
    failpoint.Inject("failpoint-name-for-demo", func(val failpoint.Value) {
        fmt.Println("unit-test", val, outerVar)
    })
    
  • 标记函数

    • 它只是一个空函数

      • 提示重写器使用等值语句进行重写
      • 接收一些参数作为重写规则
      • 它会在编译时内联,并且不会向二进制文件输出任何内容(零开销)
      • 闭包可以通过捕获访问外部作用域中的变量,转换后的代码仍然是合法的,因为所有被捕获的变量都位于 IF 语句的外部作用域中。
    • 易于编写/阅读

    • 引入编译器检查,如果故障点代码无效,则无法在常规模式下编译

  • 标记函数列表

    • func Inject(fpname string, fpblock func(val Value)) {}
    • func InjectContext(fpname string, ctx context.Context, fpblock func(val Value)) {}
    • func InjectCall(fpname string, args ...any) {}
    • func Break(label ...string) {}
    • func Goto(label string) {}
    • func Continue(label ...string) {}

如何向你的程序注入故障点

  • 你可以在调用点调用 failpoint.Inject 来注入故障点,其中 failpoint-name 用于触发故障点,failpoint-closure 会被展开为 IF 语句的函数体。

    root@kitploit:~
    failpoint.Inject("failpoint-name", func(val failpoint.Value) {
        failpoint.Return("unit-test", val)
    })
    

    转换后的代码如下所示:

    root@kitploit:~
    if val, _err_ := failpoint.Eval(_curpkg_("failpoint-name")); _err_ == nil {
        return "unit-test", val
    }
    
  • failpoint.Value 是通过 failpoint.Enable("failpoint-name", "return(5)") 传入的值,该值可以被忽略。

    root@kitploit:~
    failpoint.Inject("failpoint-name", func(_ failpoint.Value) {
        fmt.Println("unit-test")
    })
    

    或者

    root@kitploit:~
    failpoint.Inject("failpoint-name", func() {
        fmt.Println("unit-test")
    })
    

    转换后的代码如下所示:

    root@kitploit:~
    if _, _err_ := failpoint.Eval(_curpkg_("failpoint-name")); _err_ == nil {
        fmt.Println("unit-test")
    }
    

一些复杂的故障点示例

  • 将故障点注入 IF 初始化语句或条件表达式

    root@kitploit:~
    if a, b := func() {
        failpoint.Inject("failpoint-name", func(val failpoint.Value) {
            fmt.Println("unit-test", val)
        })
    }, func() int { return rand.Intn(200) }(); b > func() int {
        failpoint.Inject("failpoint-name", func(val failpoint.Value) int {
            return val.(int)
        })
        return rand.Intn(3000)
    }() && b < func() int {
        failpoint.Inject("failpoint-name-2", func(val failpoint.Value) {
            return rand.Intn(val.(int))
        })
        return rand.Intn(6000)
    }() {
        a()
        failpoint.Inject("failpoint-name-3", func(val failpoint.Value) {
            fmt.Println("unit-test", val)
        })
    }
    

    上述代码块将生成类似如下的代码:

    root@kitploit:~
    if a, b := func() {
        if val, _err_ := failpoint.Eval(_curpkg_("failpoint-name")); _err_ == nil {
            fmt.Println("unit-test", val)
        }
    }, func() int { return rand.Intn(200) }(); b > func() int {
        if val, _err_ := failpoint.Eval(_curpkg_("failpoint-name")); _err_ == nil {
            return val.(int)
        }
        return rand.Intn(3000)
    }() && b < func() int {
        if val, ok := failpoint.Eval(_curpkg_("failpoint-name-2")); ok {
            return rand.Intn(val.(int))
        }
        return rand.Intn(6000)
    }() {
        a()
        if val, ok := failpoint.Eval(_curpkg_("failpoint-name-3")); ok {
            fmt.Println("unit-test", val)
        }
    }
    
  • 将故障点注入 SELECT 语句,以便在故障点激活时阻塞某个 CASE

    root@kitploit:~
    func (s *StoreService) ExecuteStoreTask() {
        select {
        case <-func() chan *StoreTask {
            failpoint.Inject("priority-fp", func(_ failpoint.Value) {
                return make(chan *StoreTask)
            })
            return s.priorityHighCh
        }():
            fmt.Println("execute high priority task")
    
        case <- s.priorityNormalCh:
            fmt.Println("execute normal priority task")
    
        case <- s.priorityLowCh:
            fmt.Println("execute normal low task")
        }
    }
    

故障点命名最佳实践

如上所示,_curpkg_ 会自动将原始故障点名称包装在 failpoint.Eval 调用中。 你可以将 _curpkg_ 视作一个宏,它会自动将当前包路径添加到故障点名称前面。例如:

root@kitploit:~
package ddl // which parent package is `github.com/pingcap/tidb`

func demo() {
	// _curpkg_("the-original-failpoint-name") will be expanded as `github.com/pingcap/tidb/ddl/the-original-failpoint-name`
	if val, ok := failpoint.Eval(_curpkg_("the-original-failpoint-name")); ok {...}
}

你不需要在应用程序中关心 _curpkg_。它会在运行 failpoint-ctl enable 后自动生成,并在 failpoint-ctl disable 时被删除。

由于一个包中的所有故障点共享同一个命名空间,我们需要小心避免名称冲突。以下是一些推荐的命名规则,以改善这种情况。

  • 保持名称在当前子包中唯一

  • 为故障点使用自解释的名称

    你可以通过环境变量启用故障点

    root@kitploit:~
    GO_FAILPOINTS="github.com/pingcap/tidb/ddl/renameTableErr=return(100);github.com/pingcap/tidb/planner/core/illegalPushDown=return(true);github.com/pingcap/pd/server/schedulers/balanceLeaderFailed=return(true)"
    

实现细节

  1. 定义一组标记函数
  2. 解析导入,并剪除未导入故障点的源文件
  3. 遍历 AST 以查找标记函数调用
  4. 标记函数调用将被重写为 IF 语句,该语句调用 failpoint.Eval 来判断故障点是否激活,并在故障点启用时执行故障点代码

rewrite-demo

致谢

  • 感谢 gofail 提供了最初的实现。
下载工具
root@kitploit:~
GO_FAILPOINTS="main/testPanic=return(true)" go run your-program.go binding__failpoint_binding__.go
  • func Fallthrough() {}
  • func Return(results ...interface{}) {}
  • func Label(label string) {}
  • 支持的故障点环境变量

    可以通过导出符合以下模式的环境变量来启用故障点,这与 freebsd failpoint SYSCTL VARIABLES 非常相似

    注意:InjectCall 不能通过环境变量启用。

    root@kitploit:~
    [<percent>%][<count>*]<type>[(args...)][-><more terms>]
    

    参数指定要执行的操作;可以是以下之一:

    • off:不采取任何操作(不触发故障点代码)
    • return:使用指定参数触发故障点
    • sleep:休眠指定的毫秒数
    • panic:触发 panic
    • break:执行 gdb 并进入调试器中断
    • print:打印注入变量的故障点路径
    • pause:暂停,直到故障点被禁用
  • 此外,故障点闭包可以是一个接收 context.Context 的函数。你可以利用 context.Context 做一些自定义操作,例如在并行测试或其他场景中控制故障点是否激活。例如:

    root@kitploit:~
    failpoint.InjectContext(ctx, "failpoint-name", func(val failpoint.Value) {
        fmt.Println("unit-test", val)
    })
    

    转换后的代码如下所示:

    root@kitploit:~
    if val, _err_ := failpoint.EvalContext(ctx, _curpkg_("failpoint-name")); _err_ == nil {
        fmt.Println("unit-test", val)
    }
    
  • 你可以忽略 context.Context,这将生成与上面非 context 版本相同的代码。例如:

    root@kitploit:~
    failpoint.InjectContext(nil, "failpoint-name", func(val failpoint.Value) {
        fmt.Println("unit-test", val)
    })
    

    转换为:

    root@kitploit:~
    if val, _err_ := failpoint.EvalContext(nil, _curpkg_("failpoint-name")); _err_ == nil {
        fmt.Println("unit-test", val)
    }
    
  • 你可以使用 failpoint.InjectCall 注入一个函数调用,这种类型的标记只能通过 failpoint.EnableCall 启用,并且必须在与 InjectCall 调用点相同的进程中调用。使用这种标记,你可以避免故障点代码污染你的源代码。参见 示例。

  • 你可以通过 failpoint.WithHook 控制故障点

    root@kitploit:~
    func (s *dmlSuite) TestCRUDParallel() {
        sctx := failpoint.WithHook(context.Backgroud(), func(ctx context.Context, fpname string) bool {
            return ctx.Value(fpname) != nil // Determine by ctx key
        })
        insertFailpoints = map[string]struct{} {
            "insert-record-fp": {},
            "insert-index-fp": {},
            "on-duplicate-fp": {},
        }
        ictx := failpoint.WithHook(context.Backgroud(), func(ctx context.Context, fpname string) bool {
            _, found := insertFailpoints[fpname] // Only enables some failpoints.
            return found
        })
        deleteFailpoints = map[string]struct{} {
            "tikv-is-busy-fp": {},
            "fetch-tso-timeout": {},
        }
        dctx := failpoint.WithHook(context.Backgroud(), func(ctx context.Context, fpname string) bool {
            _, found := deleteFailpoints[fpname] // Only disables failpoints. 
            return !found
        })
        // other DML parallel test cases.
        s.RunParallel(buildSelectTests(sctx))
        s.RunParallel(buildInsertTests(ictx))
        s.RunParallel(buildDeleteTests(dctx))
    }
    
  • 如果你在循环上下文中使用故障点,你可能会用到其他标记函数。

    root@kitploit:~
    failpoint.Label("outer")
    for i := 0; i < 100; i++ {
        inner:
            for j := 0; j < 1000; j++ {
                switch rand.Intn(j) + i {
                case j / 5:
                    failpoint.Break()
                case j / 7:
                    failpoint.Continue("outer")
                case j / 9:
                    failpoint.Fallthrough()
                case j / 10:
                    failpoint.Goto("outer")
                default:
                    failpoint.Inject("failpoint-name", func(val failpoint.Value) {
                        fmt.Println("unit-test", val.(int))
                        if val == j/11 {
                            failpoint.Break("inner")
                        } else {
                            failpoint.Goto("outer")
                        }
                    })
            }
        }
    }
    

    上述代码块将生成如下代码:

    root@kitploit:~
    outer:
        for i := 0; i < 100; i++ {
        inner:
            for j := 0; j < 1000; j++ {
                switch rand.Intn(j) + i {
                case j / 5:
                    break
                case j / 7:
                    continue outer
                case j / 9:
                    fallthrough
                case j / 10:
                    goto outer
                default:
                    if val, _err_ := failpoint.Eval(_curpkg_("failpoint-name")); _err_ == nil {
                        fmt.Println("unit-test", val.(int))
                        if val == j/11 {
                            break inner
                        } else {
                            goto outer
                        }
                    }
                }
            }
        }
    
  • 你可能会疑惑,为什么我们不直接使用 label、break、continue 和 fallthrough,而是使用故障点标记函数。

    • Golang 不允许存在未使用的符号,例如标识符或标签。如果某个标签只在故障点闭包中使用,那将是非法的。例如:

      root@kitploit:~
      label1: // compiler error: unused label1
          failpoint.Inject("failpoint-name", func(val failpoint.Value) {
              if val.(int) == 1000 {
                  goto label1 // illegal to use goto here
              }
              fmt.Println("unit-test", val)
          })
      
    • break 和 continue 只能在循环上下文中使用,如果直接在闭包中使用它们,在 Golang 代码中是非法的。

  • 上述代码块将生成类似如下的代码:

    root@kitploit:~
    func (s *StoreService) ExecuteStoreTask() {
        select {
        case <-func() chan *StoreTask {
            if _, ok := failpoint.Eval(_curpkg_("priority-fp")); ok {
                return make(chan *StoreTask)
            })
            return s.priorityHighCh
        }():
            fmt.Println("execute high priority task")
    
        case <- s.priorityNormalCh:
            fmt.Println("execute normal priority task")
    
        case <- s.priorityLowCh:
            fmt.Println("execute normal low task")
        }
    }
    
  • 将故障点注入到动态扩展 SWITCH CASE 分支中

    root@kitploit:~
    switch opType := operator.Type(); {
    case opType == "balance-leader":
        fmt.Println("create balance leader steps")
    
    case opType == "balance-region":
        fmt.Println("create balance region steps")
    
    case opType == "scatter-region":
        fmt.Println("create scatter region steps")
    
    case func() bool {
        failpoint.Inject("dynamic-op-type", func(val failpoint.Value) bool {
            return strings.Contains(val.(string), opType)
        })
        return false
    }():
        fmt.Println("do something")
    
    default:
        panic("unsupported operator type")
    }
    

    上述代码块将生成类似如下的代码:

    root@kitploit:~
    switch opType := operator.Type(); {
    case opType == "balance-leader":
        fmt.Println("create balance leader steps")
    
    case opType == "balance-region":
        fmt.Println("create balance region steps")
    
    case opType == "scatter-region":
        fmt.Println("create scatter region steps")
    
    case func() bool {
        if val, ok := failpoint.Eval(_curpkg_("dynamic-op-type")); ok {
            return strings.Contains(val.(string), opType)
        }
        return false
    }():
        fmt.Println("do something")
    
    default:
        panic("unsupported operator type")
    }
    
  • 更复杂的故障点

    • 还有更复杂的故障点位置可以被注入
      • 循环的初始化语句、条件表达式和 POST 语句
      • RANGE 语句
      • SWITCH 初始化语句
      • …
    • 任何可以调用函数的地方