Go hỗ trợ fuzzing (kỹ thuật sinh dữ liệu đầu vào ngẫu nhiên để dò lỗi) trong toolchain chuẩn kể từ Go 1.18. Fuzz test viết bằng Go thuần được OSS-Fuzz hỗ trợ.
Hãy thử bài hướng dẫn fuzzing với Go.
Tổng quan
Fuzzing là một dạng kiểm thử tự động, liên tục biến đổi input của một chương trình để tìm lỗi. Fuzzing trong Go dùng coverage guidance để đi xuyên qua mã nguồn đang được fuzz một cách thông minh, nhằm tìm ra và report các thất bại cho người dùng. Vì nó có thể chạm tới những trường hợp biên mà con người thường bỏ sót, fuzz test đặc biệt hữu ích để tìm ra các lỗ hổng và cách khai thác về bảo mật.
Dưới đây là ví dụ về một fuzz test, nêu bật những thành phần chính của nó.

Viết fuzz test
Yêu cầu
Dưới đây là các quy tắc mà fuzz test phải tuân theo.
- Một fuzz test phải là một hàm được đặt tên dạng
FuzzXxx, chỉ nhận một*testing.F, và không có giá trị trả về. - Fuzz test phải nằm trong các tệp *_test.go thì mới chạy được.
- Một fuzz target phải là một lời gọi method tới
(*testing.F).Fuzz, nhận một*testing.Tlàm tham số đầu tiên, tiếp theo là các đối số fuzzing. Không có giá trị trả về. - Mỗi fuzz test phải có đúng một fuzz target.
- Mọi mục trong seed corpus phải có kiểu dữ liệu giống hệt các đối số fuzzing, theo cùng thứ tự. Điều này đúng cho cả các lời gọi
(*testing.F).Addlẫn mọi tệp corpus trong thư mục testdata/fuzz của fuzz test đó. - Các đối số fuzzing chỉ có thể thuộc những kiểu sau:
string,[]byteint,int8,int16,int32/rune,int64uint,uint8/byte,uint16,uint32,uint64float32,float64bool
Gợi ý
Dưới đây là những gợi ý giúp bạn tận dụng tối đa fuzzing.
- Fuzz target nên nhanh và tất định để fuzzing engine làm việc hiệu quả, đồng thời các thất bại mới và code coverage có thể dễ dàng tái hiện.
- Vì fuzz target được gọi song song trên nhiều worker và theo thứ tự không tất định, trạng thái của một fuzz target không nên tồn tại quá thời điểm kết thúc mỗi lời gọi, và hành vi của một fuzz target không nên phụ thuộc vào trạng thái toàn cục.
Chạy fuzz test
Có hai chế độ chạy fuzz test: như một unit test (mặc định go test), hoặc kèm fuzzing (go test -fuzz=FuzzTestName).
Mặc định, fuzz test được chạy khá giống một unit test. Mỗi mục trong seed corpus sẽ được kiểm thử với fuzz target, và mọi thất bại sẽ được report trước khi thoát.
Để bật fuzzing, hãy chạy go test với cờ -fuzz, cung cấp một biểu thức chính quy khớp với đúng một fuzz test. Mặc định, mọi test khác trong package đó sẽ chạy trước khi fuzzing bắt đầu. Việc này nhằm đảm bảo fuzzing sẽ không report những vấn đề mà một test sẵn có đã bắt được.
Lưu ý rằng bạn là người quyết định chạy fuzzing trong bao lâu. Rất có thể một lần chạy fuzzing sẽ chạy mãi không dừng nếu nó không tìm thấy lỗi nào. Trong tương lai sẽ có hỗ trợ chạy các fuzz test này liên tục bằng những công cụ như OSS-Fuzz, xem Issue #50192.
Lưu ý: Nên chạy fuzzing trên nền tảng hỗ trợ coverage instrumentation (hiện tại là AMD64 và ARM64) để corpus có thể lớn dần một cách có ý nghĩa trong lúc chạy, và nhiều mã nguồn hơn được bao phủ khi fuzzing.
Đầu ra dòng lệnh
Trong lúc fuzzing đang diễn ra, fuzzing engine sinh ra các input mới và chạy chúng với fuzz target được cung cấp. Mặc định, nó tiếp tục chạy cho tới khi tìm thấy một input gây thất bại, hoặc người dùng hủy tiến trình (ví dụ bằng Ctrl^C).
Đầu ra sẽ trông đại khái như sau:
~ go test -fuzz FuzzFoo
fuzz: elapsed: 0s, gathering baseline coverage: 0/192 completed
fuzz: elapsed: 0s, gathering baseline coverage: 192/192 completed, now fuzzing with 8 workers
fuzz: elapsed: 3s, execs: 325017 (108336/sec), new interesting: 11 (total: 202)
fuzz: elapsed: 6s, execs: 680218 (118402/sec), new interesting: 12 (total: 203)
fuzz: elapsed: 9s, execs: 1039901 (119895/sec), new interesting: 19 (total: 210)
fuzz: elapsed: 12s, execs: 1386684 (115594/sec), new interesting: 21 (total: 212)
PASS
ok foo 12.692s
Những dòng đầu tiên cho biết “baseline coverage” được thu thập trước khi fuzzing bắt đầu.
Để thu thập baseline coverage, fuzzing engine thực thi cả seed corpus lẫn generated corpus, nhằm đảm bảo không có lỗi nào xảy ra và để nắm được code coverage mà corpus hiện có đã cung cấp.
Các dòng tiếp theo cho bạn cái nhìn về quá trình fuzzing đang chạy:
- elapsed: khoảng thời gian đã trôi qua kể từ khi tiến trình bắt đầu
- execs: tổng số input đã được chạy với fuzz target (kèm số execs/giây trung bình kể từ dòng log gần nhất)
- new interesting: tổng số input “thú vị” đã được thêm vào generated corpus trong lần chạy fuzzing này (kèm tổng kích thước của toàn bộ corpus)
Để một input được coi là “thú vị”, nó phải mở rộng code coverage vượt quá phạm vi mà generated corpus hiện có chạm tới được. Thông thường số lượng input thú vị mới sẽ tăng nhanh lúc đầu rồi dần chậm lại, thỉnh thoảng bùng lên khi phát hiện nhánh mới.
Bạn nên trông đợi con số “new interesting” giảm dần theo thời gian khi các input trong corpus bắt đầu bao phủ nhiều dòng mã hơn, thỉnh thoảng bùng lên nếu fuzzing engine tìm thấy một đường đi mới trong mã.
Input gây thất bại
Một thất bại có thể xảy ra trong lúc fuzzing vì nhiều lý do:
- Một panic xảy ra trong mã nguồn hoặc trong test.
- Fuzz target đã gọi
t.Fail, trực tiếp hoặc thông qua các method nhưt.Errorhayt.Fatal. - Một lỗi không thể khôi phục đã xảy ra, chẳng hạn
os.Exithoặc tràn stack. - Fuzz target mất quá nhiều thời gian để hoàn thành. Hiện tại, thời gian chờ cho một lần thực thi fuzz target là 1 giây. Điều này có thể thất bại do deadlock hoặc vòng lặp vô hạn, hoặc do hành vi cố ý trong mã nguồn. Đây là một lý do vì sao khuyến nghị rằng fuzz target của bạn nên nhanh.
Nếu có lỗi xảy ra, fuzzing engine sẽ cố gắng thu nhỏ input xuống giá trị nhỏ nhất có thể và dễ đọc nhất với con người mà vẫn tạo ra lỗi đó. Để cấu hình việc này, xem phần thiết lập tùy chỉnh.
Khi quá trình thu nhỏ hoàn tất, thông báo lỗi sẽ được ghi log, và đầu ra sẽ kết thúc với nội dung đại khái như sau:
Failing input written to testdata/fuzz/FuzzFoo/a878c3134fe0404d44eb1e662e5d8d4a24beb05c3d68354903670ff65513ff49
To re-run:
go test -run=FuzzFoo/a878c3134fe0404d44eb1e662e5d8d4a24beb05c3d68354903670ff65513ff49
FAIL
exit status 1
FAIL foo 0.839s
Fuzzing engine đã ghi input gây thất bại này vào seed corpus của fuzz test đó, và từ giờ nó sẽ được chạy mặc định với go test, đóng vai trò một test hồi quy sau khi lỗi đã được khắc phục.
Bước tiếp theo của bạn là chẩn đoán vấn đề, sửa lỗi, kiểm chứng bản sửa bằng cách chạy lại go test, rồi gửi bản vá kèm tệp testdata mới đóng vai trò test hồi quy của bạn.
Thiết lập tùy chỉnh
Các thiết lập mặc định của lệnh go sẽ phù hợp với hầu hết trường hợp dùng fuzzing. Nên thông thường, một lần chạy fuzzing trên dòng lệnh sẽ trông như sau:
$ go test -fuzz={FuzzTestName}
Tuy nhiên, lệnh go có cung cấp một vài thiết lập khi chạy fuzzing. Chúng được ghi trong tài liệu package cmd/go.
Xin nêu bật một vài thiết lập:
-fuzztime: tổng thời gian hoặc số lần lặp mà fuzz target sẽ được thực thi trước khi thoát, mặc định là không giới hạn.-fuzzminimizetime: thời gian hoặc số lần lặp mà fuzz target sẽ được thực thi trong mỗi lần thử thu nhỏ, mặc định 60 giây. Bạn có thể tắt hoàn toàn việc thu nhỏ bằng cách đặt-fuzzminimizetime 0khi fuzzing.-parallel: số tiến trình fuzzing chạy cùng lúc, mặc định$GOMAXPROCS. Hiện tại, việc đặt -cpu khi fuzzing không có tác dụng.
Định dạng tệp corpus
Các tệp corpus được mã hóa theo một định dạng đặc biệt. Đây cũng là định dạng dùng cho cả seed corpus lẫn generated corpus.
Dưới đây là ví dụ về một tệp corpus:
go test fuzz v1
[]byte("hello\\xbd\\xb2=\\xbc ⌘")
int64(572293)
Dòng đầu tiên dùng để báo cho fuzzing engine biết phiên bản mã hóa của tệp. Dù hiện tại chưa có kế hoạch nào cho các phiên bản định dạng mã hóa trong tương lai, thiết kế vẫn phải hỗ trợ khả năng đó.
Mỗi dòng tiếp theo là các giá trị tạo nên mục corpus, và có thể được sao chép trực tiếp vào mã Go nếu bạn muốn.
Trong ví dụ trên, chúng ta có một []byte theo sau là một int64. Những kiểu này phải khớp chính xác với các đối số fuzzing, theo đúng thứ tự đó. Một fuzz target cho các kiểu này sẽ trông như sau:
f.Fuzz(func(*testing.T, []byte, int64) {})
Cách dễ nhất để chỉ định các giá trị seed corpus của riêng bạn là dùng method (*testing.F).Add. Trong ví dụ trên, nó sẽ trông như sau:
f.Add([]byte("hello\\xbd\\xb2=\\xbc ⌘"), int64(572293))
Tuy nhiên, bạn có thể có những tệp nhị phân lớn mà bạn không muốn sao chép thành mã trong test của mình, mà muốn giữ chúng như từng mục seed corpus riêng lẻ trong thư mục testdata/fuzz/{FuzzTestName}. Công cụ file2fuzz tại golang.org/x/tools/cmd/file2fuzz có thể dùng để chuyển các tệp nhị phân này thành tệp corpus được mã hóa cho []byte.
Để dùng công cụ này:
$ go install golang.org/x/tools/cmd/file2fuzz@latest
$ file2fuzz -h
Tài nguyên
- Hướng dẫn:
- Hãy thử bài hướng dẫn fuzzing với Go để tìm hiểu sâu về các khái niệm mới.
- Để có một bài hướng dẫn giới thiệu ngắn gọn hơn về fuzzing với Go, vui lòng xem bài blog.
- Tài liệu:
- Chi tiết kỹ thuật:
Bảng thuật ngữ
corpus entry: Một input trong corpus có thể được dùng khi fuzzing. Nó có thể là một tệp có định dạng đặc biệt, hoặc một lời gọi tới (*testing.F).Add.
coverage guidance: Một phương pháp fuzzing dùng mức mở rộng code coverage để quyết định những corpus entry nào đáng giữ lại để dùng về sau.
failing input (input gây thất bại): Input gây thất bại là một corpus entry sẽ gây ra lỗi hoặc panic khi chạy với fuzz target.
fuzz target: Hàm của fuzz test được thực thi với các corpus entry và các giá trị được sinh ra trong lúc fuzzing. Nó được cung cấp cho fuzz test bằng cách truyền hàm đó vào (*testing.F).Fuzz.
fuzz test: Một hàm trong tệp test có dạng func FuzzXxx(*testing.F) có thể dùng để fuzzing.
fuzzing: Một dạng kiểm thử tự động liên tục biến đổi input của một chương trình để tìm ra các vấn đề như lỗi hoặc lỗ hổng mà mã nguồn có thể mắc phải.
fuzzing arguments (đối số fuzzing): Các kiểu dữ liệu sẽ được truyền vào fuzz target, và bị biến đổi bởi mutator.
fuzzing engine: Một công cụ quản lý việc fuzzing, bao gồm duy trì corpus, gọi mutator, xác định coverage mới, và report các thất bại.
generated corpus: Một corpus được fuzzing engine duy trì theo thời gian trong lúc fuzzing để theo dõi tiến độ. Nó được lưu ở $GOCACHE/fuzz. Các mục này chỉ được dùng khi fuzzing.
mutator: Một công cụ dùng khi fuzzing, biến đổi ngẫu nhiên các corpus entry trước khi truyền chúng vào fuzz target.
package: Một tập hợp các tệp mã nguồn trong cùng một thư mục được biên dịch cùng nhau. Xem phần Packages trong Đặc tả Ngôn ngữ Go.
seed corpus: Một corpus do người dùng cung cấp cho fuzz test, có thể dùng để dẫn hướng fuzzing engine. Nó gồm các corpus entry được cung cấp bởi các lời gọi f.Add bên trong fuzz test, và các tệp trong thư mục testdata/fuzz/{FuzzTestName} bên trong package. Các mục này được chạy mặc định với go test, dù có fuzzing hay không.
test file (tệp test): Một tệp có định dạng xxx_test.go, có thể chứa test, benchmark, ví dụ và fuzz test.
vulnerability (lỗ hổng): Một điểm yếu nhạy cảm về bảo mật trong mã nguồn, có thể bị kẻ tấn công khai thác.
Phản hồi
Nếu bạn gặp vấn đề hoặc có ý tưởng cho một tính năng, vui lòng gửi 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.