튜토리얼: 퍼징 시작하기
튜토리얼: 퍼징 시작하기
이 튜토리얼에서는 Go에서 퍼징(fuzzing)의 기초를 배웁니다. 퍼징은 말 그대로 임의(random)의 데이터를 테스트에 계속 던져서, 취약점이나 크래시를 일으키는 입력을 찾아내는 기법이에요. 퍼징으로 찾아낼 수 있는 취약점에는 SQL 인젝션, 버퍼 오버플로, 서비스 거부(denial of service), 크로스 사이트 스크립팅(XSS) 공격 같은 것들이 있습니다.
이 튜토리얼에서는 간단한 함수 하나를 대상으로 퍼즈 테스트(fuzz test)를 작성하고, go 명령을 실행하며, 코드에서 버그를 찾아 고치는 흐름을 직접 해볼 거예요.
출처: Go 공식 문서
튜토리얼 전반에 등장하는 용어가 궁금하다면 Go Fuzzing 용어집을 참고하세요.
다음 순서로 진행됩니다:
참고: 다른 튜토리얼은 Tutorials에서 볼 수 있어요.
참고: Go 퍼징은 현재 내장 타입의 일부만 지원하며, 지원 목록은 Go Fuzzing 문서에 나와 있습니다. 앞으로 더 많은 내장 타입이 추가될 예정이에요.
준비 사항 (Prerequisites)
- Go. 튜토리얼을 따라 하려면 최신 버전의 Go를 쓰는 걸 권장해요. 설치 방법은 Installing Go에서 확인할 수 있습니다.
- 코드 편집 도구. 가지고 있는 텍스트 에디터면 무엇이든 괜찮아요.
- 명령 터미널. Go는 Linux와 Mac의 어떤 터미널에서도 잘 동작하고, Windows에서는 PowerShell이나 cmd에서 잘 동작해요.
- 퍼징을 지원하는 환경. 커버리지 계측(instrumentation)을 이용한 Go 퍼징은 현재 AMD64와 ARM64 아키텍처에서만 사용할 수 있어요.
코드용 폴더 만들기
먼저, 앞으로 작성할 코드를 담을 폴더부터 만들게요.
- 명령 프롬프트를 열고 홈 디렉터리로 이동합니다.
Linux나 Mac이라면:
$ cd
Windows라면:
C:\> cd %HOMEPATH%
튜토리얼의 나머지 부분에서는 프롬프트를 $로 표시할게요. 사용하는 명령은 Windows에서도 그대로 동작합니다.
- 명령 프롬프트에서 코드를 담을
fuzz라는 디렉터리를 만들고 이동해요.
$ mkdir fuzz
$ cd fuzz
- 코드를 담을 모듈(module)을 생성합니다.
go mod init 명령에 새 코드의 모듈 경로를 지정해서 실행해요.
$ go mod init example/fuzz
go: creating new go.mod: module example/fuzz
참고: 실무 코드라면 자신의 필요에 더 맞는 모듈 경로를 지정하게 됩니다. 자세한 내용은 Managing dependencies를 꼭 확인해 보세요.
이제, 나중에 퍼징할 간단한 문자열 뒤집기 코드를 추가할 거예요.
테스트할 코드 추가하기
이 단계에서는 문자열을 뒤집는 함수를 추가합니다.
코드 작성
- 텍스트 에디터로
fuzz디렉터리에main.go파일을 만들어요. main.go파일 맨 위에 다음 패키지 선언을 붙여 넣습니다.
package main
라이브러리가 아닌 단독으로 실행되는 프로그램(standalone program)은 항상 main 패키지에 있어요.
- 패키지 선언 아래에 다음 함수 선언을 붙여 넣습니다.
func Reverse(s string) string {
b := []byte(s)
for i, j := 0, len(b)-1; i < len(b)/2; i, j = i+1, j-1 {
b[i], b[j] = b[j], b[i]
}
return string(b)
}
이 함수는 string을 받아서 byte 단위로 하나씩 순회한 뒤, 마지막에 뒤집힌 문자열을 반환해요.
참고: 이 코드는 golang.org/x/example의 stringutil.Reverse 함수를 바탕으로 한 것입니다.
main.go맨 위, 패키지 선언 아래에 다음main함수를 붙여 넣어서, 문자열을 초기화하고 뒤집고, 결과를 출력하는 과정을 반복하도록 해요.
func main() {
input := "The quick brown fox jumped over the lazy dog"
rev := Reverse(input)
doubleRev := Reverse(rev)
fmt.Printf("original: %q\n", input)
fmt.Printf("reversed: %q\n", rev)
fmt.Printf("reversed again: %q\n", doubleRev)
}
이 함수는 Reverse 연산을 몇 번 실행한 뒤 그 결과를 명령줄에 출력해요. 코드가 실제로 어떻게 동작하는지 확인하거나, 디버깅할 때 도움이 될 수 있어요.
main함수는 fmt 패키지를 사용하므로 import 해야 합니다.
코드의 처음 몇 줄은 다음과 같아야 해요:
package main
import "fmt"
코드 실행
main.go가 있는 디렉터리에서 명령줄로 코드를 실행해 봅시다.
$ go run .
original: "The quick brown fox jumped over the lazy dog"
reversed: "god yzal eht revo depmuj xof nworb kciuq ehT"
reversed again: "The quick brown fox jumped over the lazy dog"
원래 문자열, 뒤집은 결과, 다시 뒤집은 결과(원래와 동일)를 순서대로 볼 수 있어요.
코드가 동작하니 이제 테스트를 작성할 차례입니다.
유닛 테스트 추가하기
이 단계에서는 Reverse 함수를 위한 기본적인 유닛 테스트를 작성합니다.
코드 작성
- 텍스트 에디터로
fuzz디렉터리에reverse_test.go파일을 만들어요. - 다음 코드를
reverse_test.go에 붙여 넣습니다.
package main
import (
"testing"
)
func TestReverse(t *testing.T) {
testcases := []struct {
in, want string
}{
{"Hello, world", "dlrow ,olleH"},
{" ", " "},
{"!12345", "54321!"},
}
for _, tc := range testcases {
rev := Reverse(tc.in)
if rev != tc.want {
t.Errorf("Reverse: %q, want %q", rev, tc.want)
}
}
}
이 간단한 테스트는 목록에 있는 입력 문자열들이 올바르게 뒤집히는지 확인합니다.
코드 실행
go test로 유닛 테스트를 실행해 보세요.
$ go test
PASS
ok example/fuzz 0.013s
이제 이 유닛 테스트를 퍼즈 테스트로 바꿔 볼 거예요.
퍼즈 테스트 추가하기
유닛 테스트에는 한계가 있어요. 바로 각 입력을 개발자가 직접 테스트에 추가해야 한다는 점이죠. 퍼징의 장점 중 하나는 코드를 위한 입력을 퍼징이 알아서 만들어 낸다는 겁니다. 그래서 여러분이 생각해 낸 테스트 케이스로는 닿지 못했던 경계(edge case)를 발견할 수도 있어요.
이번 섹션에서는 유닛 테스트를 퍼즈 테스트로 바꿔서, 더 적은 노력으로 더 많은 입력을 만들어 보겠습니다!
유닛 테스트, 벤치마크, 퍼즈 테스트를 같은 *_test.go 파일에 두는 것도 가능하지만, 이 예제에서는 유닛 테스트를 퍼즈 테스트로 변환하기로 해요.
코드 작성
텍스트 에디터에서 reverse_test.go의 유닛 테스트를 다음 퍼즈 테스트로 교체하세요.
func FuzzReverse(f *testing.F) {
testcases := []string{"Hello, world", " ", "!12345"}
for _, tc := range testcases {
f.Add(tc) // Use f.Add to provide a seed corpus
}
f.Fuzz(func(t *testing.T, orig string) {
rev := Reverse(orig)
doubleRev := Reverse(rev)
if orig != doubleRev {
t.Errorf("Before: %q, after: %q", orig, doubleRev)
}
if utf8.ValidString(orig) && !utf8.ValidString(rev) {
t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
}
})
}
퍼징에도 약간의 한계가 있습니다. 유닛 테스트에서는 Reverse 함수의 예상 출력을 미리 정해 두고, 실제 출력이 그 기대에 부합하는지 확인할 수 있었죠.
예를 들어 유닛 테스트의 Reverse("Hello, world") 케이스는 반환값을 "dlrow ,olleH"로 지정합니다.
퍼징을 할 때는 입력을 우리가 통제할 수 없기 때문에 예상 출력을 미리 알 수 없어요.
하지만 Reverse 함수에는 퍼즈 테스트에서 검증할 수 있는 속성(property)이 몇 가지 있습니다. 이 퍼즈 테스트에서 확인하는 두 속성은:
- 문자열을 두 번 뒤집으면 원래 값이 보존된다
- 뒤집힌 문자열도 여전히 유효한 UTF-8 상태를 유지한다
유닛 테스트와 퍼즈 테스트의 문법 차이도 눈여겨보세요:
- 함수 이름이
TestXxx대신FuzzXxx로 시작하고,*testing.T대신*testing.F를 받아요 t.Run이 실행되던 자리에f.Fuzz가 들어가는데, 여기에는 퍼징할 타입과*testing.T를 파라미터로 받는 퍼즈 타깃(fuzz target) 함수를 넘겨줍니다. 유닛 테스트의 입력은f.Add로 시드 코퍼스(seed corpus) 입력으로 제공돼요.
새 패키지 unicode/utf8이 import 되어 있는지 확인하세요.
package main
import (
"testing"
"unicode/utf8"
)
유닛 테스트를 퍼즈 테스트로 바꿨으니, 다시 테스트를 실행해 볼게요.
코드 실행
- 퍼징 없이 퍼즈 테스트를 실행해서 시드 입력이 통과하는지 확인해요.
$ go test
PASS
ok example/fuzz 0.013s
같은 파일에 다른 테스트가 있고 퍼즈 테스트만 실행하고 싶다면 go test -run=FuzzReverse를 쓸 수도 있어요.
-fuzz플래그로 퍼징을 실행해서, 임의로 생성된 문자열 입력 중 실패를 유발하는 것이 있는지 확인해 봅시다.go test에 새 플래그인-fuzz를Fuzz파라미터로 지정해서 실행하는 방식이에요. 아래 명령을 복사하세요.
$ go test -fuzz=Fuzz
유용한 플래그가 하나 더 있는데, 바로 퍼징에 걸리는 시간을 제한하는 -fuzztime이에요. 예를 들어 아래 테스트에서 -fuzztime 10s라고 지정하면, 그 전에 실패가 없었다면 테스트는 기본적으로 10초가 지나면 종료돼요. 다른 테스트 플래그는 이 섹션의 cmd/go 문서에서 확인할 수 있습니다.
이제 방금 복사한 명령을 실행해 보세요.
$ go test -fuzz=Fuzz
fuzz: elapsed: 0s, gathering baseline coverage: 0/3 completed
fuzz: elapsed: 0s, gathering baseline coverage: 3/3 completed, now fuzzing with 8 workers
fuzz: minimizing 38-byte failing input file...
--- FAIL: FuzzReverse (0.01s)
--- FAIL: FuzzReverse (0.00s)
reverse_test.go:20: Reverse produced invalid UTF-8 string "\x9c\xdd"
Failing input written to testdata/fuzz/FuzzReverse/af69258a12129d6cbba438df5d5f25ba0ec050461c116f777e77ea7c9a0d217a
To re-run:
go test -run=FuzzReverse/af69258a12129d6cbba438df5d5f25ba0ec050461c116f777e77ea7c9a0d217a
FAIL
exit status 1
FAIL example/fuzz 0.030s
퍼징 중에 실패가 발생했고, 문제를 일으킨 입력은 시드 코퍼스 파일로 저장됐어요. 이 파일은 다음에 -fuzz 플래그 없이 go test를 실행해도 함께 실행됩니다. 실패를 유발한 입력을 보려면 testdata/fuzz/FuzzReverse 디렉터리에 저장된 코퍼스 파일을 텍스트 에디터로 열어 보세요. 시드 코퍼스 파일의 문자열은 다를 수 있지만, 형식은 동일할 거예요.
go test fuzz v1
string("泃")
코퍼스 파일의 첫 줄은 인코딩 버전을 나타내요. 이후 각 줄은 코퍼스 항목을 구성하는 각 타입의 값을 의미합니다. 퍼즈 타깃이 입력을 하나만 받으므로, 버전 다음에는 값이 하나만 있게 되죠.
-fuzz플래그 없이go test를 다시 실행하면, 새로 저장된 실패 시드 코퍼스 항목이 사용됩니다:
$ go test
--- FAIL: FuzzReverse (0.00s)
--- FAIL: FuzzReverse/af69258a12129d6cbba438df5d5f25ba0ec050461c116f777e77ea7c9a0d217a (0.00s)
reverse_test.go:20: Reverse produced invalid string
FAIL
exit status 1
FAIL example/fuzz 0.016s
테스트가 실패했으니 이제 디버깅할 시간입니다.
잘못된 문자열 에러 고치기
이 섹션에서는 실패를 디버깅하고 버그를 고쳐 볼 거예요.
진행하기 전에 잠시 스스로 생각해 보고 직접 문제를 해결해 보는 시간을 가져도 좋습니다.
에러 진단하기
이 에러를 디버깅하는 방법은 여러 가지가 있어요. 텍스트 에디터로 VS Code를 쓴다면 디버거를 설정해서 조사해 볼 수 있습니다.
이 튜토리얼에서는 유용한 디버깅 정보를 터미널에 로그로 찍는 방식을 쓸게요.
먼저 utf8.ValidString 문서를 살펴볼게요.
ValidString reports whether s consists entirely of valid UTF-8-encoded runes.
현재 Reverse 함수는 문자열을 byte 단위로 뒤집는데, 여기에 문제가 있습니다. 원래 문자열의 UTF-8 인코딩된 rune들을 보존하려면, 문자열을 byte가 아니라 rune 단위로 뒤집어야 해요.
입력(여기서는 중국어 문자 泃)이 왜 Reverse로 하여금 뒤집었을 때 잘못된 문자열을 만들게 하는지 알아보려면, 뒤집힌 문자열의 rune 개수를 확인해 볼 수 있어요.
코드 작성
텍스트 에디터에서 FuzzReverse 안의 퍼즈 타깃을 다음으로 교체해요.
f.Fuzz(func(t *testing.T, orig string) {
rev := Reverse(orig)
doubleRev := Reverse(rev)
t.Logf("Number of runes: orig=%d, rev=%d, doubleRev=%d", utf8.RuneCountInString(orig), utf8.RuneCountInString(rev), utf8.RuneCountInString(doubleRev))
if orig != doubleRev {
t.Errorf("Before: %q, after: %q", orig, doubleRev)
}
if utf8.ValidString(orig) && !utf8.ValidString(rev) {
t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
}
})
이 t.Logf 줄은 에러가 발생하거나 -v 플래그로 테스트를 실행하면 명령줄에 출력되는데, 이 특정 문제를 디버깅하는 데 도움이 됩니다.
코드 실행
go test로 테스트를 실행해 보세요.
$ go test
--- FAIL: FuzzReverse (0.00s)
--- FAIL: FuzzReverse/28f36ef487f23e6c7a81ebdaa9feffe2f2b02b4cddaa6252e87f69863046a5e0 (0.00s)
reverse_test.go:16: Number of runes: orig=1, rev=3, doubleRev=1
reverse_test.go:21: Reverse produced invalid UTF-8 string "\x83\xb3\xe6"
FAIL
exit status 1
FAIL example/fuzz 0.598s
전체 시드 코퍼스가 모든 문자가 1바이트인 문자열을 사용하고 있었죠. 그런데 泃 같은 문자는 여러 바이트를 필요로 합니다. 그래서 문자열을 byte 단위로 뒤집으면 멀티바이트 문자가 깨져버리는 거예요.
참고: Go가 문자열을 어떻게 다루는지 더 궁금하다면 블로그 글 Strings, bytes, runes and characters in Go를 읽어 보세요. 더 깊이 이해할 수 있을 거예요.
버그를 더 잘 이해했으니, Reverse 함수의 에러를 고쳐 봅시다.
에러 고치기
Reverse 함수를 고치려면, 문자열을 byte가 아니라 rune 단위로 순회하도록 만들게요.
코드 작성
텍스트 에디터에서 기존 Reverse() 함수를 다음으로 교체해요.
func Reverse(s string) string {
r := []rune(s)
for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
return string(r)
}
핵심 차이는 Reverse가 문자열의 각 byte가 아니라 각 rune을 순회한다는 점이에요. 이건 단지 예시일 뿐이고, 결합 문자(combining character)는 제대로 처리하지 못한다는 점을 알아두세요.
코드 실행
go test로 테스트를 실행해 보세요.
$ go test
PASS
ok example/fuzz 0.016s
이제 테스트가 통과합니다!
go test -fuzz로 다시 퍼징을 돌려, 새 버그가 있는지 확인해 봅시다.
$ go test -fuzz=Fuzz
fuzz: elapsed: 0s, gathering baseline coverage: 0/37 completed
fuzz: minimizing 506-byte failing input file...
fuzz: elapsed: 0s, gathering baseline coverage: 5/37 completed
--- FAIL: FuzzReverse (0.02s)
--- FAIL: FuzzReverse (0.00s)
reverse_test.go:33: Before: "\x91", after: "�"
Failing input written to testdata/fuzz/FuzzReverse/1ffc28f7538e29d79fce69fef20ce5ea72648529a9ca10bea392bcff28cd015c
To re-run:
go test -run=FuzzReverse/1ffc28f7538e29d79fce69fef20ce5ea72648529a9ca10bea392bcff28cd015c
FAIL
exit status 1
FAIL example/fuzz 0.032s
두 번 뒤집은 뒤 문자열이 원본과 달라진 걸 볼 수 있어요. 이번엔 입력 자체가 유효하지 않은 유니코드예요. 문자열로 퍼징하는데 어떻게 이런 일이 가능할까요?
다시 디버깅해 봅시다.
이중 뒤집기 에러 고치기
이 섹션에서는 두 번 뒤집기(double reverse) 실패를 디버깅하고 버그를 고칩니다.
진행하기 전에 잠시 스스로 생각해 보고 직접 문제를 해결해 보는 시간을 가져도 좋아요.
에러 진단하기
앞서처럼 이 실패를 디버깅하는 방법은 여러 가지가 있어요. 이 경우엔 디버거를 쓰는 게 아주 좋은 접근이에요.
이 튜토리얼에서는 Reverse 함수 안에 유용한 디버깅 정보를 로그로 찍을게요.
뒤집힌 문자열을 자세히 살펴보면 에러를 찾을 수 있어요. Go에서 문자열은 읽기 전용인 바이트 슬라이스이며, 유효하지 않은 UTF-8인 바이트를 포함할 수 있습니다. 원래 문자열은 한 바이트인 바이트 슬라이스, '\x91'이에요. 입력 문자열을 []rune으로 만들면 Go는 바이트 슬라이스를 UTF-8로 인코딩하면서, 그 바이트를 UTF-8 문자 �로 대체해요. 이렇게 치환된 UTF-8 문자를 입력 바이트 슬라이스와 비교하면, 당연히 같지 않죠.
코드 작성
- 텍스트 에디터에서
Reverse함수를 다음으로 교체해요.
func Reverse(s string) string {
fmt.Printf("input: %q\n", s)
r := []rune(s)
fmt.Printf("runes: %q\n", r)
for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
return string(r)
}
이 코드는 문자열을 rune 슬라이스로 변환할 때 무엇이 잘못되는지 이해하는 데 도움이 될 거예요.
코드 실행
이번에는 로그를 확인하기 위해 실패한 테스트만 실행하고 싶어요. 이럴 때 go test -run을 사용합니다.
FuzzXxx/testdata 안의 특정 코퍼스 항목을 실행하려면 -run에 {FuzzTestName}/{filename}을 지정하면 돼요. 디버깅할 때 유용합니다. 이 경우 -run 플래그를 실패한 테스트의 정확한 해시로 설정하세요. 터미널에서 고유한 해시를 복사해서 붙여 넣는데, 아래 것과는 다를 거예요.
$ go test -run=FuzzReverse/28f36ef487f23e6c7a81ebdaa9feffe2f2b02b4cddaa6252e87f69863046a5e0
input: "\x91"
runes: ['�']
input: "�"
runes: ['�']
--- FAIL: FuzzReverse (0.00s)
--- FAIL: FuzzReverse/28f36ef487f23e6c7a81ebdaa9feffe2f2b02b4cddaa6252e87f69863046a5e0 (0.00s)
reverse_test.go:16: Number of runes: orig=1, rev=1, doubleRev=1
reverse_test.go:18: Before: "\x91", after: "�"
FAIL
exit status 1
FAIL example/fuzz 0.145s
입력이 유효하지 않은 유니코드라는 걸 알았으니, Reverse 함수의 에러를 고쳐 봅시다.
에러 고치기
이 문제를 고치려면, Reverse의 입력이 유효한 UTF-8이 아닐 때 에러를 반환하도록 만들게요.
코드 작성
- 텍스트 에디터에서 기존
Reverse함수를 다음으로 교체해요.
func Reverse(s string) (string, error) {
if !utf8.ValidString(s) {
return s, errors.New("input is not valid UTF-8")
}
r := []rune(s)
for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
return string(r), nil
}
이 변경으로 입력 문자열에 유효하지 않은 UTF-8 문자가 포함되어 있으면 에러를 반환하게 됩니다.
Reverse함수가 이제 에러를 반환하므로,main함수를 수정해서 추가된 에러 값을 무시하도록 해요. 기존main함수를 다음으로 교체하세요.
func main() {
input := "The quick brown fox jumped over the lazy dog"
rev, revErr := Reverse(input)
doubleRev, doubleRevErr := Reverse(rev)
fmt.Printf("original: %q\n", input)
fmt.Printf("reversed: %q, err: %v\n", rev, revErr)
fmt.Printf("reversed again: %q, err: %v\n", doubleRev, doubleRevErr)
}
입력 문자열이 유효한 UTF-8이므로, 이 Reverse 호출들은 nil 에러를 반환해야 합니다.
errors와unicode/utf8패키지를 import 해야 해요.main.go의 import 문은 다음과 같아야 합니다.
import (
"errors"
"fmt"
"unicode/utf8"
)
reverse_test.go파일을 수정해서 에러를 확인하고, 에러가 반환되면 테스트를 건너뛰도록 해요.
func FuzzReverse(f *testing.F) {
testcases := []string {"Hello, world", " ", "!12345"}
for _, tc := range testcases {
f.Add(tc) // Use f.Add to provide a seed corpus
}
f.Fuzz(func(t *testing.T, orig string) {
rev, err1 := Reverse(orig)
if err1 != nil {
return
}
doubleRev, err2 := Reverse(rev)
if err2 != nil {
return
}
if orig != doubleRev {
t.Errorf("Before: %q, after: %q", orig, doubleRev)
}
if utf8.ValidString(orig) && !utf8.ValidString(rev) {
t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
}
})
}
return을 쓰는 대신 t.Skip()을 호출해서 해당 퍼즈 입력의 실행을 중단할 수도 있어요.
코드 실행
go test로 테스트를 실행해 보세요.
$ go test
PASS
ok example/fuzz 0.019s
go test -fuzz=Fuzz로 퍼징을 돌리고, 몇 초가 지나면ctrl-C로 퍼징을 중단해요.-fuzztime플래그를 주지 않으면 퍼즈 테스트는 실패하는 입력을 만날 때까지 실행됩니다. 기본적으로 실패가 없으면 무한정 실행되는데,ctrl-C로 중단할 수 있어요.
$ go test -fuzz=Fuzz
fuzz: elapsed: 0s, gathering baseline coverage: 0/38 completed
fuzz: elapsed: 0s, gathering baseline coverage: 38/38 completed, now fuzzing with 4 workers
fuzz: elapsed: 3s, execs: 86342 (28778/sec), new interesting: 2 (total: 35)
fuzz: elapsed: 6s, execs: 193490 (35714/sec), new interesting: 4 (total: 37)
fuzz: elapsed: 9s, execs: 304390 (36961/sec), new interesting: 4 (total: 37)
...
fuzz: elapsed: 3m45s, execs: 7246222 (32357/sec), new interesting: 8 (total: 41)
^Cfuzz: elapsed: 3m48s, execs: 7335316 (31648/sec), new interesting: 8 (total: 41)
PASS
ok example/fuzz 228.000s
go test -fuzz=Fuzz -fuzztime 30s로 퍼징을 실행하면, 실패를 찾지 못한 채 30초 동안 퍼징한 뒤 종료됩니다.
$ go test -fuzz=Fuzz -fuzztime 30s
fuzz: elapsed: 0s, gathering baseline coverage: 0/5 completed
fuzz: elapsed: 0s, gathering baseline coverage: 5/5 completed, now fuzzing with 4 workers
fuzz: elapsed: 3s, execs: 80290 (26763/sec), new interesting: 12 (total: 12)
fuzz: elapsed: 6s, execs: 210803 (43501/sec), new interesting: 14 (total: 14)
fuzz: elapsed: 9s, execs: 292882 (27360/sec), new interesting: 14 (total: 14)
fuzz: elapsed: 12s, execs: 371872 (26329/sec), new interesting: 14 (total: 14)
fuzz: elapsed: 15s, execs: 517169 (48433/sec), new interesting: 15 (total: 15)
fuzz: elapsed: 18s, execs: 663276 (48699/sec), new interesting: 15 (total: 15)
fuzz: elapsed: 21s, execs: 771698 (36143/sec), new interesting: 15 (total: 15)
fuzz: elapsed: 24s, execs: 924768 (50990/sec), new interesting: 16 (total: 16)
fuzz: elapsed: 27s, execs: 1082025 (52427/sec), new interesting: 17 (total: 17)
fuzz: elapsed: 30s, execs: 1172817 (30281/sec), new interesting: 17 (total: 17)
fuzz: elapsed: 31s, execs: 1172817 (0/sec), new interesting: 17 (total: 17)
PASS
ok example/fuzz 31.025s
퍼징이 통과했어요!
-fuzz 플래그 외에도 go test에 추가된 새 플래그들이 있는데, 문서에서 확인할 수 있습니다.
퍼징 출력에 쓰이는 용어에 대한 자세한 내용은 Go Fuzzing을 참고하세요. 예를 들어 "new interesting"은 기존 퍼즈 테스트 코퍼스의 코드 커버리지를 넓히는 입력을 가리켜요. "new interesting" 입력의 개수는 퍼징이 시작되면서 급격히 늘어나고, 새로운 코드 경로가 발견되면서 몇 차례 폭증하다가, 시간이 지나면 점차 줄어드는 패턴을 보입니다.
결론 (Conclusion)
아주 잘 따라오셨어요! 이렇게 해서 Go 퍼징을 처음 접해 봤네요.
다음 단계는 여러분 코드에서 퍼징하고 싶은 함수를 하나 골라 직접 시도해 보는 거예요! 퍼징이 코드에서 버그를 찾아낸다면, trophy case에 추가해 보는 것도 좋습니다.
문제가 생기거나 기능에 대한 아이디어가 있다면 이슈를 등록해 주세요.
이 기능에 대한 토론이나 일반적인 피드백은 Gophers Slack의 #fuzzing 채널에서도 참여할 수 있어요.
더 읽을거리는 go.dev/security/fuzz 문서를 확인해 보세요.
완성된 코드 (Completed code)
— main.go —
package main
import (
"errors"
"fmt"
"unicode/utf8"
)
func main() {
input := "The quick brown fox jumped over the lazy dog"
rev, revErr := Reverse(input)
doubleRev, doubleRevErr := Reverse(rev)
fmt.Printf("original: %q\n", input)
fmt.Printf("reversed: %q, err: %v\n", rev, revErr)
fmt.Printf("reversed again: %q, err: %v\n", doubleRev, doubleRevErr)
}
func Reverse(s string) (string, error) {
if !utf8.ValidString(s) {
return s, errors.New("input is not valid UTF-8")
}
r := []rune(s)
for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
return string(r), nil
}
— reverse_test.go —
package main
import (
"testing"
"unicode/utf8"
)
func FuzzReverse(f *testing.F) {
testcases := []string{"Hello, world", " ", "!12345"}
for _, tc := range testcases {
f.Add(tc) // Use f.Add to provide a seed corpus
}
f.Fuzz(func(t *testing.T, orig string) {
rev, err1 := Reverse(orig)
if err1 != nil {
return
}
doubleRev, err2 := Reverse(rev)
if err2 != nil {
return
}
if orig != doubleRev {
t.Errorf("Before: %q, after: %q", orig, doubleRev)
}
if utf8.ValidString(orig) && !utf8.ValidString(rev) {
t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
}
})
}
더 알아보기 (Learn more)
- Go Fuzzing 문서 — 퍼징의 요구사항, 커스텀 설정, 명령줄 출력 용어까지 공식 가이드
- Tutorials — 다른 Go 튜토리얼 모음
- Strings, bytes, runes and characters in Go — Go가 문자열을 다루는 방식을 더 깊이 이해하기
- cmd/go 테스팅 플래그 문서 —
go test의 각종 플래그