
gosentry v0.4.1
보안 지향 Go 툴체인으로, 최첨단 퍼징 기능에 중점을 둡니다.
gosentry
gosentry는 Go 코드베이스에 대한 최첨단 퍼징 캠페인을 위해 다양한 기능을 통합한 보안 중심의 Go 툴체인 포크입니다. 이전에 go test -fuzz를 사용하고 있었다면 gosentry를 대체 도구로 사용해야 합니다.
여기에는 Go 툴체인에 기본적으로 없는 다양한 퍼징 개선 사항과 버그 탐지 기능이 포함되어 있습니다. 아래 TLDR;을 참조하세요. 관련 블로그 기사는 여기에서 읽을 수 있습니다.
TLDR (기능 및 옵션):
struct입력을 직접 퍼징합니다 (사용자 정의 파서 불필요).f.Add(Input{N: 7, S: "hi"})로 시드를 추가한 다음f.Fuzz(func(t *testing.T, in Input) { ... })를 실행하세요.- 정수 오버플로우 발생 시 패닉을 일으키고 산술 문제를 탐지합니다
- 경로 제약 해결과 같은 최첨단 퍼징 기법을 위해 LibAFL로 퍼징합니다
- 문법에서 입력을 생성/변이시켜 무의미한 변이를 방지합니다. 변이는 유효한 수학 연산을 생성합니다. 예:
X + Y - Z는X / U + Z - 14가 될 수 있지만X + Yè - Z는 되지 않습니다 - 선택된 함수(예: 치명적 오류 로거)에서 패닉을 일으키고 호출 시 크래시합니다
- 최근 변경된 라인과 새로운 커버리지에 퍼저를 집중시켜 주로 새 커밋을 대상으로 합니다
- 퍼징 시점에 데이터 레이스를 포착합니다
- 퍼징 시점에 Go 누수(leak)를 포착합니다
- 퍼징 시점에 타임아웃으로 중단된 실행을 포착합니다
- 단일 CLI로 퍼징 캠페인 코퍼스에서 HTML 커버리지 보고서를 생성합니다
목차
빌드```bash
cd src && ./make.bash # Produces ../bin/go. See GOFLAGS below.
> [!TIP]
> 기여자 문서: `docs/gosentry/index.md`를 읽고 코드 맵, 권장 개발 루프, CI 진입점 및 벤치마크 스크립트를 확인하세요.
> 이 포크는 Pull GitHub App을 사용하여 `golang/go:master`에서 `master`로 PR을 열고 자동 병합하므로 Go 툴체인 최신 업데이트에서 뒤처지지 않습니다.
## 기능 1: 구조체 인식 퍼징 (구조체를 입력으로 퍼징)
#### 개요
Go의 기본 퍼징(`go test -fuzz=...`)은 퍼징 매개변수로 소수의 스칼라 타입(`[]byte`, `string`, 숫자, ...)만 지원합니다. gosentry에서는 이러한 스칼라로 구성된 **복합 타입**(구조체, 배열, 슬라이스, 포인터)도 퍼징할 수 있습니다.
이 기능은 코드가 자연스럽게 구조화된 입력을 받고, 코퍼스를 시드하고 변형하기 위해 맞춤형 인코더/디코더를 만들고 싶지 않을 때 유용합니다.
예시는 `test/gosentry/examples/multiargs`와 `test/gosentry/examples/composite`를 참조하세요.
#### 간단한 예제```go
type Input struct {
Data []byte
S string
N int
OK bool
}
func FuzzStructInput(f *testing.F) {
// Seed the initial corpus with a Go struct (gosentry feature).
f.Add(Input{Data: []byte("A"), S: "B", N: 7, OK: true})
f.Fuzz(func(t *testing.T, in Input) {
if in.OK && in.N == 1337 && in.S == "BOOMMOOB" && bytes.Equal(in.Data, []byte("A")) {
t.Fatalf("boom")
}
})
}
구조체 시드(f.Add) 및 구조체 퍼징이 작동하는 방식 (만들어진 접착 계층)
Go의 기본 퍼저는 struct 값을 직접 퍼징할 수 없습니다(소수의 스칼라 타입 목록만 변형할 수 있음). gosentry는 작은 접착 계층을 추가합니다: 퍼즈 타겟이 (Input 같은) 복합 타입을 사용할 때, gosentry는 내부적으로 단일 []byte를 퍼징합니다. 매 실행마다 그 바이트를 구조체로 디코딩하고(필드 단위로, 슬라이스/배열/포인터에 대해 재귀적으로), 그런 다음 디코딩된 값으로 f.Fuzz 콜백을 호출합니다. 시드에도 동일한 인코딩이 사용되므로 f.Add(Input{...})는 인코딩된 []byte 코퍼스 항목이 되어 퍼저가 다른 시드처럼 재사용하고 변형할 수 있습니다.
퍼저(LibAFL 포함)는 원시 바이트를 변형하므로, 어떤 바이트 슬라이스든 "어떤" 구조체 값으로 바꾸고 계속 진행할 수 있는 디코더가 필요합니다. JSON/gob는 대부분의 임의 입력을 거부하고(커버리지에 나쁨), 내보내지지 않은(unexported) 필드도 채우지 않으며, 퍼징은 종종 불변식을 깨뜨리는 데서 이점을 얻습니다. 이 사용자 정의 형식은 작고, 빠르고, 결정적이며, 잘못된 데이터에 관대합니다.
내부적으로 이는 gosentry 자체의 단순 바이너리 형식(gob도 JSON도 아님)을 사용합니다. 코드는 src/testing/libafl.go에 있습니다:
- 인코딩:
libaflMarshalInputs/libaflAppendValue - 디코딩:
libaflUnmarshalArgs/libaflDecodeValue
인코딩 규칙(높은 수준):
bool: 1바이트 (0또는1)- 정수: 리틀엔디언 바이트 (
int/uint는 8바이트) - 부동소수점: 리틀엔디언 IEEE-754 비트 (
float32= 4바이트,float64= 8바이트) string:uvarint(len)다음에 원시 문자열 바이트[]byte:uvarint(len)다음에 원시 바이트- 기타 슬라이스:
uvarint(len)다음에 각 요소 인코딩 - 구조체: 선언 순서대로 필드 인코딩
- 포인터: 1바이트 (
0= nil,1= 존재함) 다음에 포인팅하는 값 인코딩
기능 2: 정수 오버플로 및 절단(truncation) 문제 탐지
개요
이 작업은 이전에 개발된 go-panikint에서 영감을 받았습니다. 정수 산술 연산에 대한 오버플로/언더플로 탐지와 (선택적으로) 정수 변환에 대한 타입 절단 탐지를 추가합니다. 오버플로나 절단이 감지되면 관련된 특정 연산 유형과 정수 타입을 포함한 상세한 오류 메시지와 함께 패닉이 발생합니다.
산술 연산: 부호 있는 정수와 부호 없는 정수 타입 모두에 대해 덧셈 +, 뺄셈 -, 곱셈 *, 나눗셈 /을 처리합니다. 부호 있는 정수는 int8, int16, int32를 다룹니다. 부호 없는 정수는 uint8, uint16, uint32, uint64를 다룹니다. 나눗셈의 경우 부호 있는 정수에 대한 MIN_INT / -1 오버플로 조건을 특별히 감지합니다. int64와 uintptr은 산술 연산에 대해 검사하지 않습니다.
타입 절단 탐지: 데이터 손실이 발생할 수 있는 정수 타입 변환을 감지합니다. 모든 정수 타입을 다룹니다: int8, int16, int32, int64, uint8, uint16, uint32, uint64. 플랫폼 의존적 사용으로 인해 uintptr은 제외합니다. 이 기능은 기본적으로 비활성화되어 있습니다.
오버플로 탐지는 기본적으로 활성화되어 있습니다. 비활성화하려면 ./make.bash 앞에 GOFLAGS='-gcflags=-overflowdetect=false'를 추가하세요. 절단 문제 검사기도 다음과 같이 활성화할 수 있습니다: -gcflags=-truncationdetect=true
작동 방식
이 기능은 컴파일러 SSA 생성을 패치하여 정수 산술 연산과 정수 변환에 추가 런타임 검사를 삽입합니다. 이 검사는 런타임을 호출하여 버그가 감지되면 상세한 오류 메시지와 함께 패닉을 발생시킵니다. 검사는 소스 위치 기반 필터링을 사용하여 적용되므로 사용자 코드는 계측되지만 표준 라이브러리 파일과 의존성(모듈 캐시 및 vendor/)은 건너뜁니다.
이에 대한 관련 블로그 게시물은 여기에서 읽을 수 있습니다.
오탐(false positive) 억제
특정 보고를 억제하려면 연산과 같은 줄이나 바로 위 줄에 마커를 추가하세요:
- 오버플로/언더플로:
overflow_false_positive - 절단:
truncation_false_positive
예시:```go // overflow_false_positive intentionalOverflow := a + b // truncation_false_positive x := uint8(big) sum2 := a + b // overflow_false_positive x2 := uint8(big) // truncation_false_positive
때로는 이것이 작동하지 않을 수 있는데, 그 이유는 Go가 함수를 인라인 처리하기 때문입니다. `// overflow_false_positive`만으로 충분하지 않다면 함수 시그니처 앞에 `//go:noinline`을 추가하세요.
## 기능 3: 선택된 함수에서 패닉 발생
퍼징 대상에서 특정 함수가 호출될 때 패닉을 유발하는 것에 관심이 있을 수 있습니다. 예를 들어, 일부 소프트웨어는 패닉을 일으키는 대신 `log.error` 메시지를 출력할 수 있는데, 이러한 조건은 종종 보안 연구자들이 퍼징 중에 탐지하고자 하는 상태를 나타냅니다.
그러나 이러한 오류는 일반적으로 내부적으로 처리되므로(예: 재시도 또는 일시 중지 메커니즘을 통해, 또는 로그에 메시지를 출력하여) 퍼저가 거의 인식하지 못합니다. 이 기능의 목적은 이 문제를 해결하는 것입니다.
#### 사용 방법
gosentry를 컴파일한 후 `--panic-on` 플래그를 사용하세요.```bash
./bin/go test -fuzz=FuzzHarness --use-libafl --focus-on-new-code=false --catch-races=false --catch-leaks=false --panic-on="test_go_panicon.(*Logger).Warning,test_go_panicon.(*Logger).Error"
위 예제는 (*Logger).Warning 또는 (*Logger).Error가 호출될 때 패닉이 발생합니다(쉼표로 구분된 목록).
선택한 함수에서 패닉이 발생하는 기능의 작동 방식
```text ┌───────────────────────────────────────────────────────────────────────────┐ │ 1) gosentry `go test` │ │ - parses + validates `-panic-on=...` against packages being built │ │ - forwards patterns to the compiler via `-panic-on-call=...` │ └───────────────┬───────────────────────────────────────────────────────────┘ v ┌───────────────────────────────────────────────────────────────────────────┐ │ 2) `cmd/compile` │ │ - prevents inlining of matching calls so the call stays visible │ │ - SSA pass inserts a call to `runtime.panicOnCall(...)` │ └───────────────┬───────────────────────────────────────────────────────────┘ v ┌───────────────────────────────────────────────────────────────────────────┐ │ 3) `runtime.panicOnCall` │ │ - panics with: "panic-on-call: func-name" │ └───────────────────────────────────────────────────────────────────────────┘ ``` In practice, this makes any matched call site behave like a crash/panic for fuzzers (note: only static call sites can be trapped).기능 4: LibAFL 최첨단 퍼징
LibAFL은 기존 Go 퍼저보다 훨씬 더 나은 성능을 제공합니다. 퍼징(go test -fuzz=...) 시 gosentry는 LibAFL을 기본적으로 사용합니다 (런너는 golibafl/에 있음).
안정성 참고 사항: LibAFL 모드에서 gosentry는 GODEBUG=updatemaxprocs=0을 강제하여 (런타임 자동 GOMAXPROCS 업데이트를 비활성화) 간헐적인 Linux CI 충돌("sync: inconsistent mutex state")을 방지합니다. 자세한 내용은 misc/gosentry/USE_LIBAFL.md에 있습니다.
LibAFL(기본값)을 사용할 때는 git 인식 스케줄링을 활성화할지 여부를 명시적으로 선택해야 합니다: --focus-on-new-code=true|false. 더 많은 문서는 이 Markdown 파일.에 있습니다.
선택적으로 LibAFL용 JSONC 구성 파일을 전달할 수도 있습니다 (문법 퍼징 옵션 포함), 여기.를 참조하세요.
"stop_all_fuzzers_on_panic": false 설정 시 LibAFL은 각 충돌을 저장하고 클라이언트를 다시 시작하여 퍼징을 계속합니다.```bash
./bin/go test -fuzz=FuzzHarness --focus-on-new-code=false --catch-races=false --catch-leaks=false --libafl-config=path/to/libafl.jsonc # optional --libafl-config
`-fuzztime=1m`을 사용하면 1분 후에 LibAFL 캠페인이 중지됩니다.
LibAFL 캠페인 코퍼스에서 커버리지 리포트를 생성하는 방법은 [Feature 8](#feature-8-generate-go-coverage-reports-from-fuzzing-campaign)에 문서화되어 있습니다.
문법 기반 퍼징(Nautilus)은 [Feature 7](#feature-7-grammar-based-fuzzing-nautilus)에 문서화되어 있습니다.
<details>
<summary><strong>Go + LibAFL이 어떻게 연결되는지</strong></summary>```text
┌───────────────────────────────────────────────────────────────────────────┐
│ 1) gosentry `go test` │
│ - captures `testing.F.Fuzz(...)` callback │
│ - generates extra source file: `_libaflmain.go` │
└───────────────┬───────────────────────────────────────────────────────────┘
v
┌───────────────────────────────────────────────────────────────────────────┐
│ 2) Generated bridge: `_libaflmain.go` │
│ - provides libFuzzer-style C ABI entrypoints: │
│ LLVMFuzzerInitialize │
│ LLVMFuzzerTestOneInput │
│ - adapts bytes -> Go types -> calls the captured fuzz callback │
└───────────────┬───────────────────────────────────────────────────────────┘
v
┌───────────────────────────────────────────────────────────────────────────┐
│ 3) `libharness.a` (static archive on disk) contains: │
│ - compiled objects for all test package (+ dependencies) │
│ - generated `_testmain.go` + `_libaflmain.go` │
│ - LLVMFuzzerInitialize │
│ - LLVMFuzzerTestOneInput │
└───────────────┬───────────────────────────────────────────────────────────┘
v
┌───────────────────────────────────────────────────────────────────────────┐
│ 4) `golibafl/` (Rust + LibAFL) │
│ env: HARNESS_LIB=/path/to/libharness.a │
│ fuzz loop: mutate input -> LLVMFuzzerTestOneInput(data) -> observe │
└───────────────────────────────────────────────────────────────────────────┘
In --use-libafl 모드에서 gosentry는 libharness.a를 빌드하고, Rust golibafl 러너가 libFuzzer 진입점을 통해 이를 프로세스 내에서 구동합니다. 참고: HARNESS_LIB는 임의의 하네스 아카이브 이름을 가리킬 수 있습니다(예: --catch-races에서 사용하는 libharness_race.a).
커버리지 계측 작동 방식(LibAFL + Go 타겟)
--use-libafl 모드에서 gosentry는 커버리지 계측을 활성화한 상태로 Go 하네스를 컴파일합니다. 그러면 프로그램의 서로 다른 부분이 실행될 때 변경되는 작은 카운터가 코드에 추가됩니다. 하네스가 golibafl 내부에서 시작되면 Go 런타임이 이러한 카운터를 LibAFL에 노출합니다. LibAFL은 각 입력 후 카운터를 읽어 어떤 코드가 실행되었는지 확인하고, 그 커버리지를 사용해 다음 변이를 안내합니다.
제한 사항
LibAFL을 사용하는 동기에 대해 이야기해 보겠습니다. go test -fuzz를 사용한 퍼징은 최신 퍼징 기법보다 훨씬 뒤떨어져 있습니다. 좋은 예로 AFL++의 CMPLOG/Redqueen이 있습니다. 이러한 기능은 퍼저가 특정 제약 조건을 해결할 수 있게 해줍니다. 다음 스니펫을 가정해 보겠습니다.```go
if input == "IMARANDOMSTRINGJUSTCMPLOGMEMAN" {
panic("this string is illegal")
}
SOTA 퍼저(AFL++ 또는 LibAFL 등)는 그 경우 패닉을 즉시 발견할 것입니다. 하지만 Go 네이티브 퍼저는 그렇지 않습니다. 이는 커버리지 탐색을 **크게** 제한하는 거대한 격차입니다.
아래 벤치마크는 이러한 한계를 보여줍니다. 해당 벤치마크는 [gosentry-bench-libafl 저장소](https://github.com/kevin-valerio/gosentry-bench-libafl/tree/main)를 통해 **재현**하고 개선할 수 있습니다.
##### 벤치마크 1:
아래 차트는 LibAFL과 Go 네이티브 퍼저를 사용하여 Google의 [UUID](https://github.com/google/uuid)를 퍼징하는 동안 커버된 라인 수의 변화를 보여줍니다.

##### 벤치마크 2:
아래 차트는 LibAFL과 Go 네이티브 퍼저를 사용하여 [go-ethereum](https://github.com/ethereum/go-ethereum)을 퍼징하는 동안 커버된 라인 수의 변화를 보여줍니다.

#### 예제
`test/gosentry/examples/`에 있는 일부 퍼징 하네스에서 테스트할 수 있습니다.```bash
cd test/gosentry/examples/reverse
../../../../bin/go test -fuzz=FuzzReverse --focus-on-new-code=false --catch-races=false --catch-leaks=false
Ctrl+C로 퍼즈 캠페인을 중지합니다.
LibAFL 출력 디렉터리 (캠페인 식별 / 코퍼스 재사용)
gosentry는 LibAFL의 캠페인 상태(코퍼스, 크래시 등)를 Go의 퍼즈 캐시 루트(대략 $(go env GOCACHE)/fuzz) 아래에 저장하며, 동일한 패키지 + 동일한 퍼즈 타깃(및 동일한 프로젝트 루트)에서 파생된 결정적 디렉터리에 저장합니다.
즉, 동일한 퍼즈 캠페인을 중지(Ctrl+C)했다가 다시 시작하면 기본적으로 이전 LibAFL queue/ 코퍼스에서 계속 진행됩니다.
경로는 실행 종료 시 출력됩니다:```text libafl output dir: /full/path/to/.../fuzz//libafl//
참고:
- `<harness>`는 `-fuzz`가 `FuzzXxx`(또는 `^FuzzXxx$`)와 같은 단순 식별자일 때의 퍼즈 타깃 이름이며, 그 외의 경우에는 `pattern-<hash>`입니다.
- 커버리지 생성(`--generate-coverage`)은 올바른 `queue/` 코퍼스를 찾기 위해 동일한 규칙을 사용하므로, 동일한 패키지에서 동일한 `-fuzz=...`로 실행해야 합니다.
## 기능 5: Git-blame 지향 퍼징(실험적)
#### 개요
커버리지 기반 퍼징은 새로운 경로를 탐색하는 데 뛰어나지만, 모든 커버된 코드를 동등하게 흥미로운 것으로 취급합니다. 대규모 코드베이스를 퍼징할 때는 회귀 및 버그가 더 자주 도입될 수 있는 최근 수정된 코드 쪽으로 퍼저를 편향시키고 싶을 수 있습니다. LibAFL 모드에서 gosentry는 `git blame`을 사용하여 최근에 변경된 줄을 실행하는 입력을 우선시할 수 있습니다(커버리지 가이던스를 기본 신호로 유지하면서).
이 작업은 [LibAFL-git-aware](https://github.com/kevin-valerio/LibAFL-git-aware)의 이전 작업을 기반으로 합니다. 모든 기술적 세부 사항은 해당 문서에 정리되어 있습니다.
#### 사용 방법
`--focus-on-new-code=true`로 git-aware 스케줄링을 활성화하세요:```bash
./bin/go test -fuzz=FuzzHarness --use-libafl --focus-on-new-code=true --catch-races=false --catch-leaks=false
이 모드는 커버리지 카운터를 소스 file:line으로 다시 매핑하기 위해 git(git blame 실행용)과 go tool addr2line이 필요합니다.
git-blame 기반 퍼징이 작동하는 방식
```text ┌───────────────────────────────────────────────────────────────────────────┐ │ 1) gosentry `go test -fuzz` │ │ - builds `libharness.a` (contains `go.o` + `.go.fuzzcntrs`) │ │ - runs `golibafl` with `GOLIBAFL_FOCUS_ON_NEW_CODE=1` │ └───────────────┬───────────────────────────────────────────────────────────┘ v ┌───────────────────────────────────────────────────────────────────────────┐ │ 2) `golibafl` generates a cached "git recency map" │ │ - maps coverage counters -> (file:line) via `go tool addr2line` │ │ - runs `git blame` to get a timestamp per line │ │ - stores timestamps in `git_recency_map.bin` │ └───────────────┬───────────────────────────────────────────────────────────┘ v ┌───────────────────────────────────────────────────────────────────────────┐ │ 3) LibAFL scheduler uses the recency map │ │ - coverage decides what enters the corpus │ │ - among the corpus, prioritize inputs that hit newer lines │ └───────────────────────────────────────────────────────────────────────────┘ ```gosentry가 git_recency_map.bin을 빌드하는 방법
.go.fuzzcntrs는 Go의 libFuzzer 스타일 8비트 커버리지 카운터를 보관하는 링커 섹션입니다(-gcflags=all=-d=libfuzzer로 활성화). 각 바이트는 "이 계측된 지점이 얼마나 많이 적중되었는지"를 나타냅니다. --focus-on-new-code=true인 경우 golibafl은 다음 단계로 git_recency_map.bin을 생성합니다:
libharness.a에서go.o를 추출합니다..go.fuzzcntrs섹션 크기를 읽어 카운터 개수N을 구합니다..go.fuzzcntrs심볼을 참조하는.text재배치를 스캔하여 각 카운터 인덱스의 주소를 복구합니다.go tool addr2line을 사용하여 각 주소를file:line으로 변환합니다.git blame --line-porcelain을 실행하여 각 줄의committer-time을 가져옵니다.git_recency_map.bin을u64 head_time+u64 N+N * u64 timestamps(리틀 엔디언) 형식으로 작성합니다. 매핑되지 않은 항목은 타임스탬프0을 사용합니다.
벤치마크 1 (go-ethereum / geth): 기준(baseline) 대비 git 인식(git-aware)
misc/gosentry/bench_focus_on_new_code_geth.sh --trials 5 --warmup 600 --timeout 200 명령으로 실행했습니다.```text
gitaware_5: crash (7122ms)
baseline results:
trial 1: crash (107747ms)
trial 2: crash (146415ms)
trial 3: crash (37902ms)
trial 4: crash (154034ms)
trial 5: timeout (200000ms)
baseline crashes: 4/5 (timeouts=1, errors=0)
baseline median (capped to timeout): 146.415s
git-aware results: trial 1: timeout (200000ms) trial 2: crash (87432ms) trial 3: crash (61733ms) trial 4: crash (157540ms) trial 5: crash (7122ms) git-aware crashes: 4/5 (timeouts=1, errors=0) git-aware median (capped to timeout): 87.432s
### Feature 6: 퍼즈 시점에 레이스 조건, 고루틴 누수, 행(hangs, 타임아웃) 감지
##### 확인된 행(hangs) 포착 (LibAFL 타임아웃)
LibAFL로 퍼징할 때 하네스 실행이 **타임아웃**될 수 있습니다 (예: 교착 상태, 대기 중 멈춘 고루틴, 또는 매우 느린 경로).
오탐을 줄이기 위해 gosentry는 타임아웃을 행(hang) 후보로 간주하고, 더 큰 타임아웃으로 해당 입력을 여러 번 재실행하여 확인합니다. 행이 확인되면 gosentry는 입력을 `<libafl output dir>/hangs/`에 기록하고 퍼즈 캠페인을 중단합니다 (버그/크래시로 취급).
종료 전에 `golibafl`은 크래시/행 입력을 최소화하려 시도합니다 (가능한 선에서; 행은 총 ~60초로 제한).
참고: 행 확인은 초기 코퍼스 가져오기/생성 중에도 실행되므로, 모든 입력에서 타임아웃되는 타깃도 결정적으로 감지할 수 있습니다.
이는 `--libafl-config`로 구성됩니다:
- `catch_hangs` (기본값: `true`)
- `hang_timeout_ms` (기본값: `10000`)
- `hang_confirm_runs` (기본값: `3`)
##### 데이터 레이스 포착 (`--catch-races`)
gosentry는 LibAFL `queue/` 디렉터리를 감시하고 `GORACE=halt_on_error=1`로 새로 발견된 시드를 재실행하는 별도의 `-race` 재생 루프를 실행할 수 있습니다.
재생 루프는 재생 전용(퍼즈 커버리지 계측 없음)을 위해 별도의 `-race` 하네스 아카이브를 빌드합니다.
재생 중 데이터 레이스가 감지되면 gosentry는 `catch-races:` 요약 및 재현 명령 앞에 전체 레이스 감지기 보고서를 출력합니다.
참고: Go의 레이스 감지기는 **단일 하네스 실행 내**에서만 데이터 레이스를 감지합니다 (동일 프로세스의 고루틴들이 적절한 동기화 없이 동일 메모리에 접근하는 레이스). `--catch-races`는 시드가 레이스가 있는 동시성을 유발하지 않으면 레이스를 놓칠 수 있으며, 프로세스 간 레이스는 감지하지 못합니다.
<details>
<summary><strong>데이터 레이스 모드 작동 방식</strong></summary>
이 모드는 `go test` 내부(동일한 부모 프로세스)에 작은 모니터를 시작하며, 전체 퍼즈 캠페인 동안 실행됩니다.
- 시점: 메인 LibAFL 퍼징 프로세스가 시작되기 전에 gosentry는 재생 하네스 + 러너를 빌드합니다.
- 모니터링: 퍼징이 시작되기 전에 gosentry는 `<libafl output dir>/queue/`의 초기 내용을 `seen` 집합으로 스냅샷합니다. 그런 다음 고루틴이 약 1초마다 `<libafl output dir>/queue/`를 폴링하여 새로 생성된 시드만 재생합니다 (점파일과 `*.metadata`는 건너뜀).```text
Legend: output/... = <libafl output dir>/...
┌───────────────────────────────────────────────────────────────────────────┐
│ 1) Main LibAFL fuzzing run │
│ - `golibafl` writes new seeds to `output/queue/` │
└───────────────┬───────────────────────────────────────────────────────────┘
v
┌───────────────────────────────────────────────────────────────────────────┐
│ 2) `--catch-races` sidecar setup │
│ - builds replay harness: `libharness_race.a` (`go test -race ...`) │
│ - builds replay runner: `golibafl-race` (linked against race harness) │
└───────────────┬───────────────────────────────────────────────────────────┘
v
┌───────────────────────────────────────────────────────────────────────────┐
│ 3) Replay loop │
│ - polls `output/queue/` for new seeds │
│ - runs: `GORACE=halt_on_error=1 golibafl-race run --input <seed>` │
│ (2 workers × 3 repeats per seed) │
└───────────────┬───────────────────────────────────────────────────────────┘
v
┌───────────────────────────────────────────────────────────────────────────┐
│ 4) On "DATA RACE" │
│ - prints the race detector report │
│ - copies seed to `output/races/` │
│ - stops the fuzz campaign (treat as bug/crash) │
└───────────────────────────────────────────────────────────────────────────┘
고루틴 누수 감지 (--catch-leaks)
gosentry는 LibAFL queue/ 디렉터리를 감시하고 go.uber.org/goleak을 활성화한 상태로 새로 발견된 시드를 다시 재생하는 goleak 재생 루프를 실행할 수도 있습니다.
고루틴 누수가 감지되면 gosentry는 정확한 시드 경로를 출력하고 이를 <libafl output dir>/leaks/로 복사합니다.
참고: goleak은 고루틴 누수를 위한 것이지 메모리 누수를 위한 것이 아닙니다.
고루틴 누수 모드가 동작하는 방식
이 모드는 go test 내부(동일한 부모 프로세스)에서 작은 모니터도 시작하며, 전체 퍼즈 캠페인 동안 실행됩니다.
- 모니터링: 고루틴이
<libafl output dir>/queue/를 약 1초마다 폴링하고, 각 새 시드를GOSENTRY_LIBAFL_CATCH_LEAKS=1로 다시 재생합니다(각 실행 후go.uber.org/goleak을 활성화합니다).```text Legend: output/... = /...
┌───────────────────────────────────────────────────────────────────────────┐
│ 1) Main LibAFL fuzzing run │
│ - golibafl writes new seeds to output/queue/ │
└───────────────┬───────────────────────────────────────────────────────────┘
v
┌───────────────────────────────────────────────────────────────────────────┐
│ 2) --catch-leaks sidecar setup │
│ - builds replay runner: golibafl-leak (linked against the harness) │
└───────────────┬───────────────────────────────────────────────────────────┘
v
┌───────────────────────────────────────────────────────────────────────────┐
│ 3) Replay loop │
│ - polls output/queue/ for new seeds │
│ - runs: GOSENTRY_LIBAFL_CATCH_LEAKS=1 golibafl-leak run --input <seed>│
│ (enables go.uber.org/goleak checks after each execution) │
└───────────────┬───────────────────────────────────────────────────────────┘
v
┌───────────────────────────────────────────────────────────────────────────┐
│ 4) On "catch-leaks: detected goroutine leak" │
│ - copies seed to output/leaks/ │
│ - stops the fuzz campaign (treat as bug/crash) │
└───────────────────────────────────────────────────────────────────────────┘
</details>
#### 사용 방법
`--catch-leaks=true`로 goroutine 누수 감지를 활성화하거나 `--catch-races=true`로 레이스 감지를 활성화하세요.```bash
./bin/go test -fuzz=FuzzHarness --use-libafl --focus-on-new-code=false --catch-races=true --catch-leaks=true
기능 7: 문법 기반 퍼징 (Nautilus)
개요
바이트 수준 퍼징은 훌륭하지만, 파서와 파일 형식은 종종 구조화된 입력을 필요로 합니다. --use-grammar를 사용하면 gosentry는 LibAFL의 Nautilus 문법 뮤테이터를 사용하여 사용자 제공 문법(JSON 형식)을 준수하는 입력을 생성 및 변형하고, 이를 일반 Go 퍼즈 하니스(testing.F.Fuzz)에 전달합니다.
문법 모드에서 LibAFL은 여전히 일반적인 커버리지 기반 루프(코퍼스 시드 선택 → 변형 → 실행 → 커버리지를 증가시키는 입력 유지)를 실행합니다. 러너는 Nautilus 변형(시드 → 문법 트리 → 변형 → 역파싱)에 더해 (기본적으로) 런타임 비교를 기반으로 Nautilus 리프 터미널을 재작성하는 CMPLOG 기반, I2S 유사 스테이지를 추가합니다. 이렇게 하면 입력이 문법적으로 유효하게 유지됩니다(문법 모드에서는 원시 바이트 수준 havoc/token 스테이지를 실행하지 않습니다). --libafl-config에서 nautilus_cmplog_i2s=false를 통해 CMPLOG/I2S 스테이지를 비활성화할 수 있습니다(바이트 수준 퍼징은 여전히 CMPLOG/I2S를 항상 유지합니다).
[!NOTE] 문법 모드는 일반적으로 바이트 수준 퍼징보다 느립니다. 이는 트레이드오프입니다: 더 많은 구조 vs 초당 더 적은 실행.
최상의 결과를 얻으려면 바이트 슬라이스([]byte) 또는 string을 인자로 받는 단일 인자 퍼즈 콜백을 사용하세요:```go
f.Fuzz(func(t testing.T, data []byte) { / parse data */ })
// or:
f.Fuzz(func(t testing.T, s string) { / parse s */ })
Grammar 모드는 단일 입력 인자(`[]byte` 또는 `string`)에서 가장 잘 작동합니다. 다중 인자 퍼즈 콜백은 gosentry가 기본 바이트 버퍼를 개별 값으로 디코딩하게 하므로, 원래 grammar 생성 텍스트가 그대로 유지되지 않습니다.
> [!NOTE]
> Grammar 모드는 여전히 **바이트/문자열**을 생성합니다. 구조화된 입력이 필요하거나(또는 차등 퍼징을 수행하는 경우) 하네스에서 `data`를 도메인 값으로 변환(파싱/언마샬링)해야 합니다. (Grammar 모드 외부에서 gosentry는 바이트에서 디코딩하여 복합 Go > 타입도 퍼징할 수 있습니다. [기능 1](#feature-1-struct-aware-fuzzing-fuzz-structs-as-inputs) 참조.)
`--libafl-config`를 통해 Nautilus를 조정할 수 있습니다(`--use-grammar`와 함께만 사용됨): `nautilus_max_len` 및 `nautilus_cmplog_i2s`(`misc/gosentry/libafl.config.jsonc` 참조).
<details>
<summary><strong>벤치마크: Nautilus grammar CMPLOG/I2S 스테이지(켜짐 vs 꺼짐)</strong></summary>
2026년 2월 17일에 저장소의 JSON grammar 예제(`test/gosentry/examples/grammar_json`, `FuzzGrammarJSON`, grammar `testdata/JSON.json`)를 사용하여 실행했습니다.
결과(LibAFL `UserStats`):
| mode | `nautilus_cmplog_i2s` | run time | executions | exec/sec | edges |
|---|---:|---:|---:|---:|---:|
| on | `true` | 1m-5s | 103818 | 1.586k | 388/8008 (4%) |
| off | `false` | 1m-0s | 256659 | 4.251k | 388/8008 (4%) |
참고: `edges`는 LibAFL의 커버리지 맵 엣지를 나타내며, Go 소스 라인이 아닙니다.
</details>
`GOSENTRY_VERBOSE_AFL=1`을 설정하면 생성된 몇 가지 입력을 출력합니다. `GOSENTRY_VERBOSE_AFL_ALL_INPUTS=1`을 설정하면 grammar 모드의 **모든** 실행을 `GOLIBAFL_MUTATED_INPUT "..."` 형태로 출력합니다(매우 시끄러움).
#### Grammar 작성 도우미
자체 대상 형식/프로토콜을 위한 새로운 Nautilus JSON grammar를 만들어야 한다면 gosentry에 다음이 포함되어 있습니다:
- LLM 사용 준비가 완료된 프롬프트: [misc/gosentry/nautilus/prompt.md](https://github.com/trailofbits/gosentry/blob/HEAD/misc/gosentry/nautilus/prompt.md)
- 소규모 예제 grammar 모음: [misc/gosentry/nautilus/examples/](https://github.com/trailofbits/gosentry/blob/HEAD/misc/gosentry/nautilus/examples/)
<details>
<summary><strong>Go 퍼즈 하네스 예제(JSON)</strong></summary>```go
func FuzzGrammarJSON(f *testing.F) {
f.Fuzz(func(t *testing.T, data []byte) {
dec := json.NewDecoder(bytes.NewReader(data))
dec.UseNumber()
var v any
if err := dec.Decode(&v); err != nil {
t.Fatalf("invalid JSON: %v", err)
}
if err := dec.Decode(&struct{}{}); err != io.EOF {
t.Fatalf("invalid JSON: trailing data")
}
})
}
차등 퍼징 하네스 스케치 (두 개의 파서):```go f.Fuzz(func(t *testing.T, data []byte) { gotA, errA := ParseA(data) gotB, errB := ParseB(data) if (errA == nil) != (errB == nil) { t.Fatalf("parser disagreement: A=%v B=%v", errA, errB) } _ = gotA _ = gotB })
</details>
<details>
<summary><strong>예: 문법 퍼징을 사용한 "실제 입력 언어" 퍼징 (사용자 정의 인코더 없음)</strong></summary>
이 예제는 문법에서 **유효한 표현식**을 생성하여 작은 산술 표현식 평가기를 퍼징합니다. 임시방편적인 "구조체를 바이트로" 인코딩이 없습니다. 퍼저는 코드가 일반적으로 파싱하는 것과 동일한 종류의 입력을 생성합니다.
하네스(1-인자 `string` 입력이 문법 모드에서 가장 잘 작동합니다):```go
func FuzzExprEval(f *testing.F) {
f.Add("1+2")
f.Add("(3*4)-5")
f.Fuzz(func(t *testing.T, expr string) {
// Parse+eval your language/protocol.
// You can be **sure** that `expr` will always be a valid math operation. Just decode/parse/unmarshall it afterwards.
_, _ = Eval(expr)
})
}
문법 개요 (Nautilus JSON 형식):```json [ ["Expr", "{Term}"], ["Expr", "{Term}+{Expr}"], ["Expr", "{Term}-{Expr}"], ["Term", "{Factor}"], ["Term", "{Factor}*{Term}"], ["Factor", "{Num}"], ["Factor", "({Expr})"], ["Num", "0"], ["Num", "1"], ["Num", "2"], ["Num", "3"] ]
</details>
<details>
<summary><strong>Nautilus JSON 문법 예제 (작은 JSON 부분집합)</strong></summary>
`--grammar=...`에 필요한 파일 형식은 다음과 같습니다:
- 문법은 규칙들의 JSON 배열입니다: `["NonTerm", "RHS"]`.
- 비단말(nonterminal) 이름은 대문자로 시작해야 합니다 (`Value`, `Object`, ...).
- RHS에서 `{NonTerm}`을 사용하여 다른 규칙을 참조하세요.
- `{`와 `}`는 비단말 참조를 위해 예약되어 있습니다. 리터럴 중괄호를 출력하려면 RHS 문자열에서 `\\{`와 `\\}`를 사용하세요.```json
[
["Json", "{Value}"],
["Value", "null"],
["Value", "{String}"],
["String", "\"{Chars}\""],
["Chars", ""],
["Chars", "{Char}{Chars}"],
["Char", "a"],
["Char", "b"]
]
gosentry에서 grammar fuzzing이 작동하는 방식
```text Legend: output/... = /...┌───────────────────────────────────────────────────────────────────────────┐
│ 0) gosentry go test -fuzz=FuzzXxx (LibAFL + --use-grammar) │
│ - captures your testing.F.Fuzz callback + its parameter types │
│ - builds libharness.a (libFuzzer-style entrypoints for LibAFL) │
│ - runs golibafl fuzz ... --use-grammar --grammar ... │
└───────────────┬───────────────────────────────────────────────────────────┘
v
┌───────────────────────────────────────────────────────────────────────────┐
│ 1) golibafl (Rust + LibAFL) fuzzes the Go harness in-process │
│ - loads libharness.a via HARNESS_LIB=... │
│ - observers: edges + time (+ cmplog for comparisons) │
│ - feedback/objective: coverage/time/crash (and optional hang handling) │
│ - scheduler selects a corpus seed (coverage-guided) │
└───────────────┬───────────────────────────────────────────────────────────┘
v
┌───────────────────────────────────────────────────────────────────────────┐
│ 2) Nautilus (in-process, per client) │
│ - loads the JSON grammar into a Nautilus context │
│ - fuzz loop stage: parse seed -> mutate tree -> unparse to bytes │
│ - if the seed is not parseable: fall back to generation-from-scratch │
└───────────────┬───────────────────────────────────────────────────────────┘
v
┌───────────────────────────────────────────────────────────────────────────┐
│ 3) Grammar mode stages │
│ - initial corpus: if input dir empty, call generate N times │
│ - fuzz loop: corpus seed -> grammar mutate -> exec harness │
│ - new coverage inputs are added to the on-disk corpus (output/queue/) │
└───────────────────────────────────────────────────────────────────────────┘
</details>
제한 사항 (현재 glue):
- Grammar 모드는 단일 입력 인자와 함께 사용할 때 가장 잘 작동합니다. 다중 인자 퍼즈 타깃은 기본 바이트 버퍼를 별도의 값으로 디코딩합니다.
- 아직 두 corpus 시드 간의 grammar 재조합/크로스오버는 없습니다 (변이는 단일 시드 기반입니다).
## 기능 8: 퍼징 캠페인에서 Go 커버리지 보고서 생성
LibAFL 퍼즈 캠페인을 실행한 후(또는 실행하는 동안) gosentry는 현재 LibAFL **queue corpus**를 재생하여 Go 커버리지 보고서를 생성할 수 있습니다 (퍼징 없이).```bash
# Same package + same fuzz target as your fuzz campaign:
./bin/go test -fuzz=FuzzHarness --generate-coverage .
This replays inputs from <libafl output dir>/queue/ and writes cover.out and cover.html.
트로피
이 버그들은 gosentry의 문법 퍼징 기능을 사용한 차등 퍼징 캠페인을 통해 발견되었습니다.
Optimism
- Kona와 op-node는 brotli 채널에 대해 의견이 다를 수 있음
- 알 수 없는 배치 유형이 패닉을 일으켜 kona-protocol에서 서비스 거부를 유발함
- Kona 프레임 파싱이 op-node 및 OP Stack 사양과 불일치