Go Fuzzing

Go 從 1.18 版本開始在其標準工具鏈中支援模糊測試。原生 Go 模糊測試 受 OSS-Fuzz 支援

嘗試一下 Go 模糊測試教程

概覽

模糊測試是一種自動化測試,它不斷地操縱程式的輸入以查詢 bug。Go 模糊測試使用覆蓋率引導智慧地遍歷被模糊測試的程式碼,從而向用戶發現並報告故障。由於它可以觸及人類通常會忽略的邊緣情況,因此模糊測試對於發現安全漏洞和弱點尤其有價值。

下面是一個 模糊測試 的示例,重點介紹了其主要組成部分。

Example code showing the overall fuzz test, with a fuzz target within
it. Before the fuzz target is a corpus addition with f.Add, and the parameters
of the fuzz target are highlighted as the fuzzing arguments. Example code showing the overall fuzz test, with a fuzz target within
it. Before the fuzz target is a corpus addition with f.Add, and the parameters
of the fuzz target are highlighted as the fuzzing arguments.

編寫模糊測試

要求

以下是模糊測試必須遵守的規則。

  • 模糊測試必須是一個命名為 FuzzXxx 的函式,它只接受一個 *testing.F 引數,並且沒有返回值。
  • 模糊測試必須位於 *_test.go 檔案中才能執行。
  • 一個 模糊目標 必須是對 (*testing.F).Fuzz 的方法呼叫,它接受一個 *testing.T 作為第一個引數,後跟模糊引數。沒有返回值。
  • 每個模糊測試必須有且只有一個模糊目標。
  • 所有 種子語料庫 條目必須具有與 模糊引數 相同的型別,並按相同順序排列。這對於呼叫 (*testing.F).Add 和模糊測試的 testdata/fuzz 目錄中的任何語料庫檔案都適用。
  • 模糊引數只能是以下型別
    • string, []byte
    • int, int8, int16, int32/rune, int64
    • uint, uint8/byte, uint16, uint32, uint64
    • float32, float64
    • bool

建議

以下是一些建議,可以幫助您充分利用模糊測試。

  • 模糊目標應快速且確定性,以便模糊引擎能夠高效工作,並輕鬆重現新的故障和程式碼覆蓋率。
  • 由於模糊目標在多個工作程序中以非確定性順序並行呼叫,因此模糊目標的狀1態不應在每次呼叫結束後持續存在,並且模糊目標的行為不應依賴於全域性狀態。

執行模糊測試

執行模糊測試有兩種模式:作為單元測試(預設 go test),或使用模糊測試(go test -fuzz=FuzzTestName)。

預設情況下,模糊測試的執行方式與單元測試類似。每個 種子語料庫條目 都將針對模糊目標進行測試,並在退出前報告任何故障。

要啟用模糊測試,請執行 go test 並使用 -fuzz 標誌,提供一個匹配單個模糊測試的正則表示式。預設情況下,該包中的所有其他測試將在模糊測試開始之前執行。這是為了確保模糊測試不會報告任何已被現有測試捕獲的問題。

請注意,您需要自行決定模糊測試的執行時間。如果模糊測試未發現任何錯誤,其執行可能會無限期地執行。將來將支援使用 OSS-Fuzz 等工具持續執行這些模糊測試,請參閱 Issue #50192

注意: 應該在支援覆蓋率插樁的平臺(目前是 AMD64 和 ARM64)上執行模糊測試,這樣語料庫才能在執行時有意義地增長,並且在模糊測試時可以覆蓋更多程式碼。

命令列輸出

在模糊測試進行期間,模糊引擎 會生成新的輸入並針對提供的模糊目標執行它們。預設情況下,它會一直執行,直到找到 失敗的輸入,或者使用者取消該過程(例如,使用 Ctrl^C)。

輸出大致如下所示

~ 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

前幾行表明,在模糊測試開始之前,“基線覆蓋率”已收集完畢。

為了收集基線覆蓋率,模糊引擎會執行 種子語料庫生成的語料庫,以確保沒有發生錯誤,並瞭解現有語料庫已提供的程式碼覆蓋率。

接下來的幾行提供了對當前模糊執行的洞察

  • elapsed:自程序開始以來經過的時間
  • execs:已針對模糊目標執行的輸入總數(並顯示自上次日誌行以來的平均每秒執行數)
  • new interesting:在此次模糊執行期間新增到生成語料庫中的“有趣”輸入的總數(以及整個語料庫的總大小)

為了使輸入被認為是“有趣”,它必須擴充套件現有生成語料庫無法達到的程式碼覆蓋範圍。通常,“新有趣”輸入的數量在開始時會快速增長,然後逐漸放緩,並可能隨著新分支的發現而出現偶爾的爆發。

隨著語料庫中的輸入開始覆蓋更多程式碼行,您應該會看到“新有趣”的數量隨時間推移而逐漸減少,如果模糊引擎找到新的程式碼路徑,則會出現偶爾的爆發。

失敗的輸入

模糊測試期間可能由於多種原因發生故障

  • 程式碼或測試中發生了 panic。
  • 模糊目標呼叫了 t.Fail,無論是直接呼叫還是透過 t.Errort.Fatal 等方法。
  • 發生了不可恢復的錯誤,例如 os.Exit 或堆疊溢位。
  • 模糊目標花費了太長時間才能完成。目前,模糊目標的執行超時時間為 1 秒。這可能是由於死鎖或無限迴圈,或者由於程式碼中的預期行為。這也是 建議使模糊目標快速 的原因之一。

如果發生錯誤,模糊引擎將嘗試將輸入最小化為最小化且最易讀的、仍能產生錯誤的值。要配置此設定,請參閱 自定義設定 部分。

最小化完成後,將記錄錯誤訊息,輸出將以類似如下的內容結束

    Failing input written to testdata/fuzz/FuzzFoo/a878c3134fe0404d44eb1e662e5d8d4a24beb05c3d68354903670ff65513ff49
    To re-run:
    go test -run=FuzzFoo/a878c3134fe0404d44eb1e662e5d8d4a24beb05c3d68354903670ff65513ff49
FAIL
exit status 1
FAIL    foo 0.839s

模糊引擎將此 失敗的輸入 寫入該模糊測試的種子語料庫,現在它將預設使用 go test 執行,並在 bug 修復後作為迴歸測試。

下一步您將需要診斷問題,修復 bug,透過重新執行 go test 驗證修復,並提交補丁,同時將新的 testdata 檔案作為迴歸測試。

自定義設定

預設的 go 命令設定應適用於大多數模糊測試用例。因此,通常在命令列上執行模糊測試應該如下所示

$ go test -fuzz={FuzzTestName}

然而,go 命令在執行模糊測試時提供了一些設定。這些設定在 cmd/go 包文件 中有說明。

舉例說明

  • -fuzztime:模糊目標在退出前執行的總時間或迭代次數,預設不限制。
  • -fuzzminimizetime:在每次最小化嘗試期間執行模糊目標的次數或時間,預設 60 秒。您可以透過在模糊測試時設定 -fuzzminimizetime 0 來完全停用最小化。
  • -parallel:同時執行的模糊程序數,預設為 $GOMAXPROCS。目前,在模糊測試時設定 -cpu 無效。

語料庫檔案格式

語料庫檔案採用特殊格式編碼。這對於 種子語料庫生成的語料庫 都是相同的格式。

下面是一個語料庫檔案的示例

go test fuzz v1
[]byte("hello\\xbd\\xb2=\\xbc ⌘")
int64(572293)

第一行用於告知模糊引擎檔案的編碼版本。儘管目前沒有計劃支援未來版本的編碼格式,但設計必須支援這種可能性。

接下來的每一行都是構成語料庫條目的值,如果需要,可以直接複製到 Go 程式碼中。

在上面的示例中,我們有一個 []byte 後跟一個 int64。這些型別必須與模糊引數完全匹配,並且順序相同。針對這些型別的模糊目標將如下所示

f.Fuzz(func(*testing.T, []byte, int64) {})

指定自己的種子語料庫值的最簡單方法是使用 (*testing.F).Add 方法。在上面的示例中,它將如下所示

f.Add([]byte("hello\\xbd\\xb2=\\xbc ⌘"), int64(572293))

然而,您可能有一些大型二進位制檔案,您寧願不將它們作為程式碼複製到測試中,而是將它們保留為 testdata/fuzz/{FuzzTestName} 目錄中的獨立語料庫條目。在 golang.org/x/tools/cmd/file2fuzz file2fuzz 工具可用於將這些二進位制檔案轉換為為 []byte 編碼的語料庫檔案。

使用此工具

$ go install golang.org/x/tools/cmd/file2fuzz@latest
$ file2fuzz -h

資源

詞彙表

語料庫條目: 語料庫中的一個輸入,可在模糊測試時使用。這可以是一個特殊格式的檔案,也可以是對 (*testing.F).Add 的呼叫。

覆蓋率引導: 一種模糊測試方法,它使用程式碼覆蓋率的擴充套件來確定哪些語料庫條目值得保留供將來使用。

失敗的輸入: 失敗的輸入是語料庫中的一個條目,當針對 模糊目標 執行時,該條目會導致錯誤或 panic。

模糊目標: 模糊測試中的函式,在模糊測試期間針對語料庫條目和生成的 L 進行執行。它透過將函式傳遞給 (*testing.F).Fuzz 來提供給模糊測試。

模糊測試: 測試檔案中的一個函式,形式為 func FuzzXxx(*testing.F),可用於模糊測試。

模糊測試: 一種自動化測試,它不斷地操縱程式的輸入以查詢 bug 或 漏洞 等問題,程式碼可能容易受到這些問題的影響。

模糊引數: 將傳遞給模糊目標並由 變異器 變異的 L。

模糊引擎: 管理模糊測試的工具,包括維護語料庫、呼叫變異器、識別新覆蓋率以及報告故障。

生成的語料庫: 模糊引擎在模糊測試期間隨著時間推移而維護的語料庫,用於跟蹤進度。它儲存在 $GOCACHE/fuzz 中。這些條目僅在模糊測試時使用。

變異器: 模糊測試期間使用的工具,在將語料庫條目傳遞給模糊目標之前,隨機操縱它們。

包: 同一目錄中一起編譯的原始檔集合。請參閱 Go 語言規範中的 Packages 部分。

種子語料庫: 使用者為模糊測試提供的語料庫,可用於指導模糊引擎。它由模糊測試中 f.Add 呼叫提供的語料庫條目以及包內的 testdata/fuzz/{FuzzTestName} 目錄中的檔案組成。這些條目預設使用 go test 執行,無論是否進行模糊測試。

測試檔案: 格式為 xxx_test.go 的檔案,可能包含測試、基準測試、示例和模糊測試。

漏洞: 程式碼中對安全敏感的弱點,可能被攻擊者利用。

反饋

如果您遇到任何問題或有功能想法,請 提交 issue

有關該功能的討論和一般反饋,您還可以參與 Gophers Slack 中的#fuzzing 頻道