RFC-0078:Fuchsia Fuzzing 核心核心涵蓋範圍

RFC-0078:Fuchsia Fuzzing 的核心清除器涵蓋範圍
狀態已接受
區域
  • Kernel
說明

匯出 Fuchsia 核心程式碼涵蓋範圍,以便搭配 Syzkaller 進行模糊測試。

作者
審查人員
提交日期 (年-月-日)2021-02-26
審查日期 (年-月-日)2021-03-25

摘要

這項變更會導入新的系統呼叫,以便收集及轉移核心程式碼涵蓋範圍資料。系統呼叫只會在現有 sancov 建構中實作。其他建構變體不會受到影響 (新系統呼叫會傳回 ZX_ERR_NOT_SUPPORTED)。核心涵蓋範圍的初始用戶端是系統呼叫模糊測試引擎 Syzkaller。這項提案的概念驗證已實作,並附上單元測試 (請參閱後代變更),以評估其效力。

背景

Syzkaller 是以涵蓋範圍為導向的 Kernel Fuzzer。這項工具會產生系統呼叫序列來測試作業系統,並根據涵蓋範圍資訊突變這些序列,判斷哪些序列有用。Fuchsia 已使用 Syzkaller,但目前的整合作業不會收集程式碼涵蓋範圍資料。

在 Fuchsia 上,Syzkaller 會以 HostFuzzer 模式執行,模糊測試引擎 (syz-fuzzer) 位於模糊測試 VM 外部,並與模糊測試代理程式 (syz-executor) 通訊,後者會執行一連串的系統呼叫,並將涵蓋範圍傳回引擎。

您可以使用 Clang 的 SanitizerCoverage (sancov) 檢測,取得核心程式碼涵蓋範圍。這項插碼作業會在每個基本區塊中新增 __sanitizer_cov_trace_pc_guard 的呼叫,然後實作該函式,追蹤已造訪的程式計數器 (PC)。Linux 自 2016 年起就支援這項功能,實作方式是保留涵蓋 PC 的每個執行緒清單。

Zircon 支援 sancov 建構變體,也適用於 Zircon 核心,並以 sancov 格式 (命中 PC 的稀疏表格) 匯出即時 VMO。不過,Syzkaller 會同時執行多個程式,並查看每個系統呼叫的涵蓋範圍,因此我們需要更精細的資訊。

需求條件

成功實作後,必須以對模糊測試引擎有用的方式匯出核心程式碼涵蓋範圍資料。目前的主要消費者是 Syzkaller,因此許多需求都是由 Syzkaller 的架構和假設所規定。請遵循以下規定:

  • 執行緒層級的精細程度:Syzkaller 的 syz-executor 會使用執行緒集區執行系統呼叫,並收集每個系統呼叫的涵蓋範圍,也就是核心在每個系統呼叫環境中執行的程式碼。

    它會採用一組命中次數的程式計數器。每個工作執行緒的虛擬程式碼如下 (每個工作都是系統呼叫):

Thread:
    Enable Coverage
    While True:
        Job <- wait for job event
        Start Tracking Coverage
        Execute Job
        Collect Coverage
        Signal job done

Syzkaller 採用的策略表示,只需要從參與處理 syz-executor 工作站執行緒啟動的系統呼叫的那些核心執行緒,收集核心涵蓋範圍資訊。

  • 快速:收集涵蓋範圍資料並傳輸至模糊測試引擎所需的時間應盡量縮短,因為 syz-executor 會在執行每個系統呼叫後查詢涵蓋範圍資訊。更快速的收集和轉移流程,讓 Syzkaller 能在相同時間內測試更多程式。

  • 低干擾:如果測試中的系統呼叫確定執行的程式碼以外沒有涵蓋範圍訊號,Syzkaller 的效能最高。完全檢測為 syz-executor 執行緒提供服務的 Kernel 執行緒,幾乎可以達到這個目標,但排程器程式碼和服務中斷會引入雜訊。成功導入後,應盡可能減少干擾。

  • 僅供測試:在一般建構版本中,不應啟用收集及匯出涵蓋範圍的功能。這些檔案只能用於使用 sancov 變數的建構作業。涵蓋範圍收集介面不保證穩定性,僅適用於 Syskaller 等模糊測試引擎。非 sancov 建構作業的記憶體用量或執行階段效能不得受到影響。

不在範圍內

雖然有下列需求也不錯,但這些需求不在本 RFC 的範圍內。可視為「未來工作」。

  • 導入更精細的粒度和/或控制流程追蹤,例如程序層級追蹤或「接力傳遞」,追蹤通訊程序涵蓋範圍。

  • 提供機制,排除核心特定部分的涵蓋範圍收集作業。這與低噪音需求有些衝突,但合理預期核心涵蓋範圍的噪音夠低,可引導 Syskaller 進行初始實作。

  • 從核心執行緒 (而非執行受測系統呼叫的執行緒) 收集涵蓋範圍資料。

設計

現有的 sancov 變數將擴充,支援可收集及轉移核心程式碼涵蓋範圍資料的新系統呼叫。

實作

系統呼叫 (僅在 sancov 變體中啟用)

系統導入下列新系統呼叫。sancov 變數建構版本支援這些系統呼叫。

  • coverage_control(uint32_t action):要求核心開始使用新的緩衝區收集涵蓋範圍資料 (action=KCOV_CTRL_START),或停止收集涵蓋範圍資料 (action=KCOV_CTRL_STOP)。

  • coverage_collect(uintptr_t* buf, size_t count, size_t* actual, size_t* avail):要求核心將目前的涵蓋範圍資料複製到 buf 參照的使用者空間緩衝區。唯一的選用參數是 avail。這項作業不會耗用資料,資料會持續可用,直到核心緩衝區由 coverage_control(KCOV_CTRL_START) 重設為止。複製到 buf 的位元組數會儲存在 actual 中,目前可用的總位元組數則會儲存在 avail 中 (如有設定)。傳回值 ZX_ERR_NO_SPACE 表示核心的涵蓋範圍緩衝區大小不足,可能遺失涵蓋範圍資料;傳回值會提示用戶端可能需要更頻繁地收集資料,確保不會遺漏任何資料。

請注意,這些系統呼叫只會控制單一執行緒的涵蓋範圍收集作業,不會影響任何全域涵蓋範圍收集作業。這樣一來,Syzkaller 和其他模糊測試器就能確保只收集他們關心的部分涵蓋範圍。

核心記憶體需求 (僅限 sancov 變數)

  • 每個啟用涵蓋範圍的使用者執行緒各有一個 300 KiB 的緩衝區

  • ThreadDispatcher 中上述緩衝區的指標

  • ThreadDispatcher 中上述緩衝區的項目數量計數器

為執行緒啟用涵蓋範圍後,核心會分配足夠大的緩衝區來收集涵蓋範圍,直到收集作業停用或重設為止。這個緩衝區的大小日後可能會變更,一開始會是 300KiB,大約是 sancov PC 表格的大小。這項記憶體必須一律提交;方法是在核心的根 VMAR 中建立僅限核心的 VmMapping (類似於 kstack 的建立方式),並將 VMAR 控制代碼儲存在 ThreadDispatcher 中。

ThreadDispatcher 會儲存這個緩衝區的指標,以及其中的項目數量。如果執行緒超出涵蓋範圍限制,系統就不會註冊新的涵蓋範圍。coverage_control(KCOV_CTRL_START) 會將計數器重設為零 (並開始收集新的涵蓋範圍資料緩衝區),因此不需要花時間清除緩衝區。為避免在非 sancov 建構作業中新增記憶體負荷,這些 ThreadDispatcher 欄位可能會 #ifdef'd out。

Sancov 資料收集

__sanitizer_cov_trace_pc_guard 會檢查目前執行的執行緒,看看是否已啟用緩衝區,如果已啟用,則將命中的 PC 附加至清單。系統呼叫會在目前執行緒上運作,因此不會與其他執行緒發生競爭。核心執行緒在處理系統呼叫時可能會中斷,這會產生雜訊,但不會造成競爭條件。為執行緒啟用涵蓋範圍後,緩衝區會持續分配,直到執行緒遭到毀損為止。

__sanitizer_cov_trace_pc_guard執行時無法處理故障或例外狀況,因為處理常式可能會再次呼叫 __sanitizer_cov_trace_pc_guard,最後可能導致迴圈。為避免發生錯誤,用於儲存 PC 的記憶體一律必須已提交。

重入風險

__sanitizer__* 函式之間存在重入風險。我們會謹慎控管重入來源,確保不會造成問題。請特別注意以下幾點:

  • __sanitizer__* 實作不會直接或間接叫用 __sanitizer__* 函式

  • __sanitizer_* 函式不會取得任何鎖定

因此,重入的唯一來源是中斷,不會導致無限遞迴或死結。下文將說明中斷造成的涵蓋範圍資料雜訊。

涵蓋範圍資料中的雜訊

在這個設計的脈絡下,涵蓋範圍資料預期會出現下列雜訊來源:

  • 緩衝區重設後,但在傳回前執行的 coverage_control(KCOV_CTRL_START) 程式碼

  • 在停止收集資料前執行的程式碼 (coverage_control(KCOV_CTRL_STOP))

  • coverage_collect 中的程式碼會在資料複製到用戶端緩衝區完成前執行

  • 在處理目標使用者執行緒的系統呼叫時執行的中斷

這些雜訊來源大多是近乎確定性的 (也就是說,每個收集到的涵蓋範圍批次都會出現相同的程式碼路徑)。您可以使用 sancov 機制,將特定程式碼加入拒絕清單,藉此排除部分來源,但這些雜訊來源夠小且可預測,因此初始實作不會承擔管理拒絕清單的複雜性。

效能

sancov 變體版本的建構作業效能不會受到影響。只有使用新系統呼叫的 sancov 變體版本建構作業,才會受到這項異動影響。在這種情況下,預期效能會維持不變,但當執行緒啟用核心涵蓋範圍時,效能會突然下降,因為該執行緒執行的每個系統呼叫都會在核心中執行的每個基本區塊上觸發緩衝區寫入。這是可接受的效能下降,因為只有在執行核心模糊測試引擎以收集這項資料時,才會發生這種情況。

安全性考量

一般而言,核心程式碼位址被視為非常敏感的資訊,對攻擊者來說極具價值。在sancov (僅供測試,非正式版) 變數建構版本,啟用會顯示這項資訊的系統呼叫,即可降低在正式版裝置上洩漏這項資訊的風險。

隱私權注意事項

這項提案不涉及收集或處理使用者資料。

測試

測試分為兩個階段:

  1. 單元測試會擷取從 Zircon 映像檔中擷取的系統呼叫位址,並在多種情況下檢查各種系統呼叫是否存在 (或不存在),例如單一系統呼叫、多個系統呼叫、多個通訊執行緒、執行緒當機等。
  2. 整合測試會在 VM 上執行,因為核心符號資訊不適用於使用者空間程式。虛擬機器會將涵蓋範圍資料匯出至主機環境,並使用 sancov 程式碼涵蓋範圍工具,確認 PC 是否屬於預期的核心函式。

除了初步實作外,我們也計畫進行單元測試,並確保新的系統呼叫會在非 sancov 建構變體中傳回 ZX_ERR_NOT_SUPPORTED

說明文件

新的系統呼叫會以一般方式整合至 Zircon 文件,並附上注意事項和建構說明,解釋系統呼叫與 sancov 建構變體之間的關係。

缺點、替代方案和未知事項

我們曾考慮下列替代方案,但最終在設計和實作過程中予以拒絕:

  • 資料格式:其中一個替代方案是保留目前的 Sancov 格式,但透過系統呼叫逐一匯出每個執行緒。雖然這樣做可行,但效率不彰,因為每次都必須將整個 400 KiB 的 PC 表格複製到使用者空間,而單一系統呼叫期間實際命中的 PC 清單通常遠遠少於這個大小 (例如,具有 2 個控制代碼的 1 KiB 緩衝區的 zx_channel_read 會收集 163 個 PC,而 zx_channel_write 會收集 127 個 PC,相較於大約 51,000 個 PC 總數)。

  • 插樁方法:有兩種編譯器插樁涵蓋範圍替代方案,分別是 Intel 處理器追蹤或 QEMU 插樁。這些替代方案或許可行,但設定起來相當費力,而且不如 Clang 的 SanitizerCoverage 插樁彈性。

  • API 設計:我們原本的設計是在核心和使用者空間之間共用 VMO,而不是使用個別的 cover_collect 方法將涵蓋範圍資訊複製到使用者空間。不過,我們決定不這麼做,因為 Zircon 團隊不建議這麼做:vmo 不應在核心和使用者空間之間共用,但這麼做的好處是我們不需要將涵蓋範圍從核心複製到使用者空間。

  • 測試方法:我們考慮採用成本較高 (但可能更徹底) 的測試方法:在主機上執行測試並啟動 VM。測試會執行一連串的系統呼叫,然後從 VM 中外洩涵蓋範圍,並使用 sancov 程式碼涵蓋範圍工具,驗證 PC 是否屬於預期的核心函式。

既有技術和參考資料