Hướng dẫn này giới thiệu những kiến thức cơ bản về fuzzing (kỹ thuật sinh dữ liệu đầu vào ngẫu nhiên để dò lỗi) trong Go. Với fuzzing, dữ liệu ngẫu nhiên được đưa vào chạy với test của bạn nhằm tìm ra các lỗ hổng hoặc các input gây crash. Một vài ví dụ về lỗ hổng mà fuzzing có thể tìm ra là SQL injection, buffer overflow, denial of service và cross-site scripting.
Trong hướng dẫn này, bạn sẽ viết một fuzz test cho một hàm đơn giản, chạy lệnh go, rồi debug và sửa các vấn đề trong mã nguồn.
Nếu cần trợ giúp về thuật ngữ xuyên suốt hướng dẫn này, xem bảng thuật ngữ Go Fuzzing.
Bạn sẽ đi qua các phần sau:
- Tạo một thư mục cho mã nguồn của bạn.
- Thêm mã nguồn để test.
- Thêm một unit test.
- Thêm một fuzz test.
- Sửa hai lỗi.
- Khám phá thêm tài nguyên khác.
Lưu ý: Với các hướng dẫn khác, xem Tutorials.
Lưu ý: Go fuzzing hiện hỗ trợ một tập con các kiểu dữ liệu dựng sẵn, được liệt kê trong tài liệu Go Fuzzing; sẽ có thêm nhiều kiểu dựng sẵn khác được hỗ trợ trong tương lai.
Điều kiện tiên quyết
- Go. Chúng tôi khuyến nghị dùng phiên bản Go mới nhất để theo hướng dẫn này. Xem hướng dẫn cài đặt tại Installing Go.
- Một công cụ để chỉnh sửa mã nguồn. Bất kỳ trình soạn thảo văn bản nào bạn có đều dùng được.
- Một cửa sổ dòng lệnh. Go hoạt động tốt với mọi terminal trên Linux và Mac, và với PowerShell hoặc cmd trên Windows.
- Một môi trường hỗ trợ fuzzing. Go fuzzing với coverage instrumentation hiện chỉ khả dụng trên kiến trúc AMD64 và ARM64.
Tạo một thư mục cho mã nguồn của bạn
Trước tiên, hãy tạo một thư mục cho mã nguồn mà bạn sẽ viết.
-
Mở cửa sổ dòng lệnh và chuyển về thư mục home của bạn.
Trên Linux hoặc Mac:
$ cdTrên Windows:
C:\> cd %HOMEPATH%Phần còn lại của hướng dẫn sẽ hiển thị dấu $ làm dấu nhắc lệnh. Các câu lệnh bạn dùng cũng sẽ chạy được trên Windows.
-
Từ dấu nhắc lệnh, tạo một thư mục cho mã nguồn của bạn tên là fuzz.
$ mkdir fuzz $ cd fuzz -
Tạo một module để chứa mã nguồn của bạn.
Chạy câu lệnh
go mod init, truyền cho nó module path của mã nguồn mới.$ go mod init example/fuzz go: creating new go.mod: module example/fuzzLưu ý: Với mã nguồn dùng trong môi trường thật, bạn sẽ chỉ định một module path phù hợp hơn với nhu cầu của mình. Để biết thêm, hãy xem Managing dependencies.
Tiếp theo, bạn sẽ thêm một đoạn mã đơn giản để đảo ngược một chuỗi, thứ mà chúng ta sẽ fuzz sau đó.
Thêm mã nguồn để test
Ở bước này, bạn sẽ thêm một hàm để đảo ngược một chuỗi.
Viết mã nguồn
-
Dùng trình soạn thảo văn bản, tạo một tệp tên là main.go trong thư mục fuzz.
-
Vào main.go, ở đầu tệp, dán khai báo package sau đây.
package mainMột chương trình độc lập (khác với thư viện) luôn nằm trong package
main. -
Bên dưới khai báo package, dán khai báo hàm sau đây.
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) }Hàm này sẽ nhận vào một
string, lặp qua từngbytemột, và trả về chuỗi đã được đảo ngược ở cuối.Lưu ý: Mã nguồn này dựa trên hàm
stringutil.Reversetrong golang.org/x/example. -
Ở đầu main.go, bên dưới khai báo package, dán hàm
mainsau đây để khởi tạo một chuỗi, đảo ngược nó, in kết quả ra, và lặp lại.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) }Hàm này sẽ chạy vài thao tác
Reverse, rồi in kết quả ra dòng lệnh. Điều này có thể hữu ích để thấy mã nguồn hoạt động thực tế, và có thể hữu ích cho việc debug. -
Hàm
maindùng package fmt, nên bạn sẽ cần import nó.Những dòng mã đầu tiên sẽ trông như thế này:
package main import "fmt"
Chạy mã nguồn
Từ dòng lệnh trong thư mục chứa main.go, hãy chạy mã nguồn.
$ 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"
Bạn có thể thấy chuỗi ban đầu, kết quả của việc đảo ngược nó, rồi kết quả của việc đảo ngược lần nữa, vốn tương đương với chuỗi ban đầu.
Giờ mã nguồn đã chạy được, đã đến lúc test nó.
Thêm một unit test
Ở bước này, bạn sẽ viết một unit test cơ bản cho hàm Reverse.
Viết mã nguồn
-
Dùng trình soạn thảo văn bản, tạo một tệp tên là reverse_test.go trong thư mục fuzz.
-
Dán đoạn mã sau vào 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) } } }Test đơn giản này sẽ khẳng định rằng các chuỗi input được liệt kê sẽ được đảo ngược đúng.
Chạy mã nguồn
Chạy unit test bằng go test
$ go test
PASS
ok example/fuzz 0.013s
Tiếp theo, bạn sẽ đổi unit test này thành một fuzz test.
Thêm một fuzz test
Unit test có hạn chế của nó, cụ thể là mỗi input đều phải do lập trình viên thêm vào test. Một lợi ích của fuzzing là nó tự nghĩ ra input cho mã nguồn của bạn, và có thể phát hiện những trường hợp biên (edge case) mà các test case bạn nghĩ ra không chạm tới.
Trong phần này, bạn sẽ chuyển unit test thành một fuzz test để có thể sinh ra nhiều input hơn với ít công sức hơn!
Lưu ý rằng bạn có thể giữ unit test, benchmark và fuzz test trong cùng một tệp *_test.go, nhưng với ví dụ này bạn sẽ chuyển unit test thành fuzz test.
Viết mã nguồn
Trong trình soạn thảo văn bản, thay thế unit test trong reverse_test.go bằng fuzz test sau đây.
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)
}
})
}
Fuzzing cũng có một vài hạn chế. Trong unit test, bạn có thể dự đoán được kết quả mong đợi của hàm Reverse, và kiểm chứng rằng kết quả thực tế khớp với mong đợi đó.
Ví dụ, trong test case Reverse("Hello, world"), unit test chỉ định giá trị trả về là "dlrow ,olleH".
Khi fuzzing, bạn không thể dự đoán kết quả mong đợi, vì bạn không kiểm soát được các input.
Tuy nhiên, có một vài tính chất của hàm Reverse mà bạn có thể kiểm chứng trong một fuzz test. Hai tính chất được kiểm tra trong fuzz test này là:
- Đảo ngược một chuỗi hai lần thì giữ nguyên giá trị ban đầu
- Chuỗi đã đảo ngược vẫn giữ trạng thái là UTF-8 hợp lệ.
Hãy để ý các khác biệt về cú pháp giữa unit test và fuzz test:
- Hàm bắt đầu bằng FuzzXxx thay vì TestXxx, và nhận
*testing.Fthay vì*testing.T - Ở chỗ bạn thường thấy lời gọi
t.Run, thì thay vào đó bạn thấyf.Fuzz, vốn nhận vào một fuzz target function mà các tham số là*testing.Tvà các kiểu dữ liệu cần fuzz. Các input từ unit test của bạn được cung cấp làm seed corpus input thông quaf.Add.
Hãy đảm bảo package mới, unicode/utf8, đã được import.
package main
import (
"testing"
"unicode/utf8"
)
Khi unit test đã được chuyển thành fuzz test, đã đến lúc chạy lại test.
Chạy mã nguồn
-
Chạy fuzz test mà không fuzz nó, để chắc chắn rằng các seed input đều pass.
$ go test PASS ok example/fuzz 0.013sBạn cũng có thể chạy
go test -run=FuzzReversenếu bạn có các test khác trong tệp đó và chỉ muốn chạy fuzz test. -
Chạy
FuzzReversecó fuzzing, để xem liệu có chuỗi input sinh ngẫu nhiên nào gây lỗi hay không. Việc này được thực hiện bằnggo testvới một flag mới,-fuzz, đặt bằng tham sốFuzz. Hãy sao chép câu lệnh bên dưới.$ go test -fuzz=FuzzMột flag hữu ích khác là
-fuzztime, giới hạn thời gian fuzzing chạy. Ví dụ, chỉ định-fuzztime 10sở test bên dưới sẽ có nghĩa là, miễn là không có lỗi nào xảy ra sớm hơn, thì test sẽ mặc định thoát sau khi đã trôi qua 10 giây. Xem phần này của tài liệu cmd/go để xem các flag testing khác.Giờ hãy chạy câu lệnh bạn vừa sao chép.
$ 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.030sMột lỗi đã xảy ra trong lúc fuzzing, và input gây ra vấn đề được ghi vào một tệp seed corpus; tệp này sẽ được chạy trong lần gọi
go testtiếp theo, kể cả khi không có flag-fuzz. Để xem input đã gây lỗi, hãy mở tệp corpus được ghi vào thư mục testdata/fuzz/FuzzReverse bằng một trình soạn thảo văn bản. Tệp seed corpus của bạn có thể chứa một chuỗi khác, nhưng định dạng thì giống nhau.go test fuzz v1 string("泃")Dòng đầu tiên của tệp corpus cho biết phiên bản mã hoá. Mỗi dòng tiếp theo biểu diễn giá trị của từng kiểu dữ liệu tạo nên mục corpus đó. Vì fuzz target chỉ nhận 1 input, nên chỉ có 1 giá trị sau dòng phiên bản.
-
Chạy lại
go testmà không có flag-fuzz; mục seed corpus gây lỗi mới sẽ được dùng:$ 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.016sVì test của chúng ta đã fail, đã đến lúc debug.
Sửa lỗi chuỗi không hợp lệ
Trong phần này, bạn sẽ debug lỗi này và sửa bug.
Bạn cứ thoải mái dành chút thời gian suy nghĩ và tự thử sửa vấn đề trước khi đi tiếp.
Chẩn đoán lỗi
Có một vài cách khác nhau để bạn debug lỗi này. Nếu bạn dùng VS Code làm trình soạn thảo văn bản, bạn có thể thiết lập trình debug để điều tra.
Trong hướng dẫn này, chúng tôi sẽ ghi log những thông tin debug hữu ích ra terminal của bạn.
Trước hết, hãy xem tài liệu của utf8.ValidString.
ValidString reports whether s consists entirely of valid UTF-8-encoded runes.
Hàm Reverse hiện tại đảo ngược chuỗi theo từng byte, và đó chính là vấn đề của chúng ta. Để giữ nguyên các rune được mã hoá UTF-8 của chuỗi ban đầu, thay vào đó chúng ta phải đảo ngược chuỗi theo từng rune.
Để xem xét vì sao input (trong trường hợp này là ký tự tiếng Trung 泃) lại khiến Reverse tạo ra một chuỗi không hợp lệ khi đảo ngược, bạn có thể kiểm tra số lượng rune trong chuỗi đã đảo ngược.
Viết mã nguồn
Trong trình soạn thảo văn bản, thay thế fuzz target bên trong FuzzReverse bằng đoạn sau.
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)
}
})
Dòng t.Logf này sẽ in ra dòng lệnh nếu có lỗi xảy ra, hoặc nếu bạn chạy test với -v, điều này có thể giúp bạn debug chính vấn đề này.
Chạy mã nguồn
Chạy test bằng 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
Toàn bộ seed corpus dùng các chuỗi mà trong đó mỗi ký tự đều chỉ chiếm một byte. Tuy nhiên, những ký tự như 泃 có thể cần nhiều byte. Vì vậy, đảo ngược chuỗi theo từng byte sẽ làm hỏng các ký tự nhiều byte.
Lưu ý: Nếu bạn tò mò về cách Go xử lý chuỗi, hãy đọc bài blog Strings, bytes, runes and characters in Go để hiểu sâu hơn.
Với hiểu biết tốt hơn về bug này, hãy sửa lỗi trong hàm Reverse.
Sửa lỗi
Để sửa hàm Reverse, hãy duyệt chuỗi theo từng rune, thay vì theo từng byte.
Viết mã nguồn
Trong trình soạn thảo văn bản, thay thế hàm Reverse() hiện có bằng đoạn sau.
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)
}
Khác biệt mấu chốt là giờ Reverse lặp qua từng rune trong chuỗi, thay vì từng byte. Lưu ý rằng đây chỉ là một ví dụ, và nó không xử lý đúng ký tự tổ hợp.
Chạy mã nguồn
-
Chạy test bằng
go test$ go test PASS ok example/fuzz 0.016sGiờ test đã pass!
-
Fuzz lại lần nữa bằng
go test -fuzz, để xem có bug mới nào không.$ 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.032sChúng ta thấy rằng chuỗi khác với chuỗi ban đầu sau khi bị đảo ngược hai lần. Lần này chính input là unicode không hợp lệ. Sao điều đó lại có thể xảy ra nếu chúng ta đang fuzz với các chuỗi?
Hãy debug lại lần nữa.
Sửa lỗi đảo ngược hai lần
Trong phần này, bạn sẽ debug lỗi đảo ngược hai lần và sửa bug.
Bạn cứ thoải mái dành chút thời gian suy nghĩ và tự thử sửa vấn đề trước khi đi tiếp.
Chẩn đoán lỗi
Như lần trước, có vài cách để bạn debug lỗi này. Trong trường hợp này, dùng trình debug sẽ là một cách rất tốt.
Trong hướng dẫn này, chúng tôi sẽ ghi log những thông tin debug hữu ích trong hàm Reverse.
Hãy nhìn kỹ chuỗi đã đảo ngược để phát hiện lỗi. Trong Go, một chuỗi là một slice byte chỉ đọc, và có thể chứa những byte không phải UTF-8 hợp lệ. Chuỗi ban đầu là một byte slice với một byte, '\x91'. Khi chuỗi input được chuyển sang []rune, Go mã hoá byte slice đó sang UTF-8, và thay thế byte kia bằng ký tự UTF-8 �. Khi chúng ta so sánh ký tự UTF-8 thay thế đó với byte slice input, rõ ràng chúng không bằng nhau.
Viết mã nguồn
-
Trong trình soạn thảo văn bản, thay thế hàm
Reversebằng đoạn sau.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) }Điều này sẽ giúp chúng ta hiểu có gì sai khi chuyển chuỗi thành một slice các rune.
Chạy mã nguồn
Lần này, chúng ta chỉ muốn chạy test đang fail để xem log. Để làm việc đó, chúng ta sẽ dùng go test -run.
Để chạy một mục corpus cụ thể trong FuzzXxx/testdata, bạn có thể truyền {FuzzTestName}/{filename} cho -run. Điều này có thể hữu ích khi debug. Trong trường hợp này, đặt flag -run bằng đúng mã hash của test đang fail. Sao chép và dán mã hash duy nhất từ terminal của bạn; nó sẽ khác với mã bên dưới.
$ 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
Biết rằng input là unicode không hợp lệ, hãy sửa lỗi trong hàm Reverse của chúng ta.
Sửa lỗi
Để khắc phục vấn đề này, hãy trả về một lỗi nếu input của Reverse không phải UTF-8 hợp lệ.
Viết mã nguồn
-
Trong trình soạn thảo văn bản, thay thế hàm
Reversehiện có bằng đoạn sau.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 }Thay đổi này sẽ trả về một lỗi nếu chuỗi input chứa các ký tự không phải UTF-8 hợp lệ.
-
Vì hàm Reverse giờ trả về một lỗi, hãy sửa hàm
mainđể bỏ qua giá trị lỗi thêm vào đó. Thay thế hàmmainhiện có bằng đoạn sau.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) }Những lời gọi
Reversenày sẽ trả về lỗi nil, vì chuỗi input là UTF-8 hợp lệ. -
Bạn sẽ cần import các package errors và unicode/utf8. Câu lệnh import trong main.go sẽ trông như sau.
import ( "errors" "fmt" "unicode/utf8" ) -
Sửa tệp reverse_test.go để kiểm tra lỗi và bỏ qua test bằng cách return nếu có lỗi được sinh ra.
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) } }) }Thay vì return, bạn cũng có thể gọi
t.Skip()để dừng việc thực thi với fuzz input đó.
Chạy mã nguồn
-
Chạy test bằng go test
$ go test PASS ok example/fuzz 0.019s -
Fuzz nó bằng
go test -fuzz=Fuzz, rồi sau khi vài giây trôi qua, dừng fuzzing bằngctrl-C. Fuzz test sẽ chạy cho tới khi gặp một input gây lỗi, trừ khi bạn truyền flag-fuzztime. Mặc định là chạy mãi mãi nếu không có lỗi nào xảy ra, và tiến trình có thể bị ngắt bằngctrl-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
-
Fuzz nó bằng
go test -fuzz=Fuzz -fuzztime 30s, lệnh này sẽ fuzz trong 30 giây rồi thoát nếu không tìm thấy lỗi nào.$ 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.025sFuzzing đã pass!
Ngoài flag
-fuzz, một số flag mới đã được thêm vàogo testvà có thể xem trong tài liệu.Xem Go Fuzzing để biết thêm thông tin về các thuật ngữ dùng trong output của fuzzing. Ví dụ, “new interesting” chỉ những input mở rộng được code coverage của corpus thuộc fuzz test hiện có. Số lượng input “new interesting” thường tăng mạnh khi fuzzing mới bắt đầu, vọt lên vài lần khi các nhánh mã mới được phát hiện, rồi giảm dần theo thời gian.
Kết luận
Làm tốt lắm! Bạn vừa mới làm quen với fuzzing trong Go.
Bước tiếp theo là chọn một hàm trong mã nguồn của bạn mà bạn muốn fuzz, và thử xem sao! Nếu fuzzing tìm ra một bug trong mã nguồn của bạn, hãy cân nhắc thêm nó vào trophy case.
Nếu bạn gặp vấn đề nào đó hoặc có ý tưởng về một tính năng, hãy tạo một issue.
Để thảo luận và góp ý chung về tính năng này, bạn cũng có thể tham gia kênh #fuzzing trong Gophers Slack.
Hãy xem tài liệu tại go.dev/security/fuzz để đọc thêm.
Mã nguồn hoàn chỉnh
-– 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)
}
})
}