Skip to content
KitploitKITPLOIT
도구블로그
제출
도구블로그
제출

해킹, 침투 테스트 및 사이버 보안 도구를 당신의 보안 무기고에!

Kitploit은 해킹, 사이버 보안 및 침투 테스트 도구 디렉토리입니다. 최신 프로젝트 업데이트를 발견하여 취약점을 찾고, 시스템을 분석하고, 테스트를 자동화하고, 보안을 강화하세요.

··피드·문의·개인정보·© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
failpoint — Go용 장애 주입 라이브러리로, 환경 변수를 통해 런타임에 제어 가능한 실패 지점을 추가하여 패닉, 지연, 반환 및 조건부 오류 경로를 트리거합니다. | Kitploit
도구/GitHubGitHub/pingcap/failpoint
General Purpose UtilitiesDebuggersChaos Engineering
GitHubpingcap/failpoint

failpoint

Go용 장애 주입 라이브러리로, 환경 변수를 통해 런타임에 제어 가능한 실패 지점을 추가하여 패닉, 지연, 반환 및 조건부 오류 경로를 트리거합니다.

저장소 보기
8946712일 전Kitploit 검토 완료

인기

모두 보기 →

커뮤니티에서 가장 많이 사용되는 도구를 찾아보세요.

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유

failpoint

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

Golang용 failpoints 구현입니다. Fail point는 사용자가 제어하는 방식으로 오류를 주입할 수 있는 코드 지점을 추가하는 데 사용됩니다. Fail point는 해당 failpoint가 활성화된 경우에만 실행되는 코드 스니펫입니다.

빠른 시작 (failpoint-ctl 사용)

  1. 소스에서 failpoint-ctl 빌드

    root@kitploit:~
    git clone https://github.com/pingcap/failpoint.git
    cd failpoint
    make
    ls bin/failpoint-ctl
    
  2. 프로그램에 failpoint를 주입하세요, 예:

    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 환경 변수로 failpoint를 활성화하세요

    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. 프로그램에 failpoint를 주입하세요, 예:

    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 환경 변수로 failpoint를 활성화하세요

    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 코드로 failpoint를 정의하세요. 주석이나 다른 것이 아니라.

  • Failpoint는 추가 비용이 없습니다

    • 일반 로직에는 영향을 주지 않습니다
    • 일반 코드 성능 저하를 일으키지 않습니다
    • Failpoint 코드는 최종 바이너리에 나타나지 않습니다
  • Failpoint 루틴은 쓰기/읽기가 가능하며 컴파일러가 검사해야 합니다

  • Failpoint 정의로 생성된 코드는 읽기 쉽습니다

  • 주입 코드와 줄 번호를 동일하게 유지합니다(디버깅이 더 쉬움)

  • context.Context를 사용한 병렬 테스트를 지원합니다

핵심 개념

  • Failpoint

    Failpoint는 해당 failpoint가 활성화된 경우에만 실행되는 코드 스니펫입니다. 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)
    })
    
  • 마커 함수

    • 그것은 단지 빈 함수입니다

      • 재작성자가 동등 비교(equality statement)로 재작성하도록 힌트를 줍니다
      • 재작성 규칙으로 사용할 일부 매개변수를 받습니다
      • 컴파일 타임에 인라인되며 바이너리로 아무것도 내보내지 않습니다(제로 비용)
      • 외부 범위의 변수는 캡처를 통해 클로저에서 접근할 수 있으며, 변환된 코드는 여전히 유효합니다. 모든 캡처된 변수가 IF 문의 외부 범위에 있기 때문입니다.
    • 쓰기/읽기가 쉽습니다

    • failpoint에 대한 컴파일러 검사를 도입합니다. failpoint 코드가 유효하지 않으면 일반 모드에서 컴파일할 수 없습니다.

  • 마커 함수 목록

    • 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를 주입하는 방법

  • failpoint.Inject를 호출하여 호출 지점에 failpoint를 주입할 수 있습니다. 여기서 failpoint-name은 failpoint를 트리거하는 데 사용되고 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")
    }
    

복잡한 failpoint 데모

  • IF INITIAL 문 또는 CONDITIONAL 표현식에 failpoint 주입

    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 문에 failpoint를 주입하여 failpoint가 활성화되면 하나의 CASE를 차단하도록 합니다

    root@kitploit:~

Failpoint 이름 모범 사례

위에서 보았듯이 _curpkg_는 failpoint.Eval 호출에서 원래 failpoint 이름을 자동으로 감쌉니다. _curpkg_는 현재 패키지 경로를 failpoint 이름 앞에 자동으로 추가하는 매크로라고 생각할 수 있습니다. 예를 들어,

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로 삭제됩니다.

패키지의 모든 failpoint가 동일한 네임스페이스를 공유하므로 이름 충돌을 피하기 위해 주의해야 합니다. 이 상황을 개선하기 위해 권장되는 몇 가지 명명 규칙이 있습니다.

  • 현재 하위 패키지에서 이름을 고유하게 유지하세요

  • failpoint에 자명한 이름을 사용하세요

    환경 변수로 failpoint를 활성화할 수 있습니다

    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. import를 파싱하고 failpoint를 import하지 않는 소스 파일을 제거합니다
  3. AST를 탐색하여 마커 함수 호출을 찾습니다
  4. 마커 함수 호출은 failpoint.Eval을 호출하는 IF 문으로 재작성되어 failpoint가 활성화되었는지 확인하고, failpoint가 활성화된 경우 failpoint 코드를 실행합니다

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) {}
  • 지원되는 failpoint 환경 변수

    failpoint는 다음 패턴의 환경 변수를 export하여 활성화할 수 있습니다. 이는 freebsd failpoint SYSCTL VARIABLES와 매우 유사합니다.

    참고: InjectCall은 환경 변수로 활성화할 수 없습니다.

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

    <type> 인자는 수행할 동작을 지정하며, 다음 중 하나일 수 있습니다:

    • off: 아무 작업도 수행하지 않음 (failpoint 코드를 트리거하지 않음)
    • return: 지정된 인자로 failpoint 트리거
    • sleep: 지정된 밀리초 수만큼 sleep
    • panic: 패닉
    • break: gdb를 실행하고 디버거로 중단
    • print: inject 변수의 failpoint 경로 출력
    • pause: failpoint가 비활성화될 때까지 일시 중지
  • 또한 failpoint 클로저는 context.Context를 받는 함수일 수 있습니다. 병렬 테스트나 다른 경우에 failpoint가 활성화될지 제어하는 것처럼 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를 무시할 수 있으며, 이 경우 위의 비컨텍스트 버전과 동일한 코드가 생성됩니다. 예를 들어,

    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 코드가 소스 코드를 오염시키지 않도록 할 수 있습니다. examples를 참조하세요.

  • failpoint.WithHook으로 failpoint를 제어할 수 있습니다

    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))
    }
    
  • 루프 컨텍스트에서 failpoint를 사용한다면 다른 마커 함수를 사용하게 될 수도 있습니다.

    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
                        }
                    }
                }
            }
        }
    
  • 왜 failpoint 마커 함수를 사용하는 대신 label, break, continue, fallthrough를 직접 사용하지 않는지 의문이 들 수 있습니다.

    • Golang에서는 식별자나 라벨과 같은 사용되지 않는 심볼이 허용되지 않습니다. 어떤 라벨이 failpoint 클로저에서만 사용된다면 유효하지 않습니다. 예를 들어,

      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 코드에서 유효하지 않습니다.

  • 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") } }

    위 코드 블록은 다음과 같은 코드를 생성합니다:

    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 분기를 동적으로 확장하도록 failpoint 주입

    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")
    }
    
  • 더 복잡한 failpoint

    • 주입할 수 있는 더 복잡한 failpoint 지점이 있습니다
      • 루프 INITIAL 문, CONDITIONAL 표현식 및 POST 문
      • RANGE 문
      • SWITCH INITIAL 문
      • …
    • 함수를 호출할 수 있는 모든 곳