
보안 지향 Go 툴체인으로, 최첨단 퍼징 기능에 중점을 둡니다.
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) { ... })를 실행하세요.X + Y - Z는 X / U + Z - 14가 될 수 있지만 X + Yè - Z는 되지 않습니다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이 작업은 이전에 개발된 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/)은 건너뜁니다.
이에 대한 관련 블로그 게시물은 여기에서 읽을 수 있습니다.
특정 보고를 억제하려면 연산과 같은 줄이나 바로 위 줄에 마커를 추가하세요:
overflow_false_positivetruncation_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가 호출될 때 패닉이 발생합니다(쉼표로 구분된 목록).
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).
--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로 퍼즈 캠페인을 중지합니다.
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_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을 사용하여 각 주소를 으로 변환합니다.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 checks after each execution) │
└───────────────┬───────────────────────────────────────────────────────────┘
v
┌───────────────────────────────────────────────────────────────────────────┐
│ 4) On "catch-leaks: detected goroutine leak" │
│ - copies seed to │
│ - stops the fuzz campaign (treat as bug/crash) │
└───────────────────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────────────────┐
│ 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 N times │
│ - fuzz loop: corpus seed -> grammar mutate -> exec harness │
│ - new coverage inputs are added to the on-disk corpus () │
└───────────────────────────────────────────────────────────────────────────┘
libaflUnmarshalArgs / libaflDecodeValue인코딩 규칙(높은 수준):
bool: 1바이트 (0 또는 1)int/uint는 8바이트)float32 = 4바이트, float64 = 8바이트)string: uvarint(len) 다음에 원시 문자열 바이트[]byte: uvarint(len) 다음에 원시 바이트uvarint(len) 다음에 각 요소 인코딩0 = nil, 1 = 존재함) 다음에 포인팅하는 값 인코딩file:linegit blame --line-porcelain을 실행하여 각 줄의 committer-time을 가져옵니다.git_recency_map.bin을 u64 head_time + u64 N + N * u64 timestamps(리틀 엔디언) 형식으로 작성합니다. 매핑되지 않은 항목은 타임스탬프 0을 사용합니다.go.uber.org/goleakoutput/leaks/</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
바이트 수준 퍼징은 훌륭하지만, 파서와 파일 형식은 종종 구조화된 입력을 필요로 합니다. --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"]
]
generateoutput/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의 문법 퍼징 기능을 사용한 차등 퍼징 캠페인을 통해 발견되었습니다.