GWP-ASan

簡介

GWP-ASan 是一個抽樣配置器框架,可協助在生產環境中尋找釋放後使用和堆積緩衝區溢位錯誤。它非正式地是一個遞迴縮寫,「**G**WP-ASan **W**ill **P**rovide **A**llocation **SAN**ity」(GWP-ASan 將提供配置健全性)。

GWP-ASan 基於經典的 電子圍欄 Malloc 除錯器,並進行了關鍵調整。值得注意的是,我們只選擇一小部分配置進行抽樣,並僅對這些抽樣的配置應用保護頁面。抽樣量很小,足以讓我們保持非常低的效能開銷。

在程序的生命週期內,有一個小的、可調整的記憶體開銷是固定的。使用預設設定,每個程序大約為 ~40KiB,具體取決於配置的平均大小。

GWP-ASan 與 ASan

AddressSanitizer 不同,GWP-ASan 不會造成顯著的效能開銷。ASan 通常需要使用專用的 canaries 才能在生產環境中運作,因此通常不切實際。

GWP-ASan 只能找到 ASan 偵測到的記憶體問題的一部分。此外,GWP-ASan 的錯誤偵測功能只是機率性的。因此,我們建議在測試中以及在保證錯誤偵測比 2 倍執行速度減慢/二進位檔案大小膨脹更有價值的任何其他地方,使用 ASan 而不是 GWP-ASan。對於大多數生產環境而言,這種影響過大,而 GWP-ASan 非常有用。

設計

**請注意:**GWP-ASan 的實作在很大程度上仍在進行中,這些細節可能會有所變更。目前還有其他 GWP-ASan 的實作,例如 Chromium 中的功能。長期支援目標是確保在合理的情況下實現功能一致性,並支援 compiler-rt 作為參考實作。

配置器支援

GWP-ASan 並非用來取代傳統配置器。相反地,它透過在支援的配置器中插入 stub 來將選定取樣的配置重新導向至 GWP-ASan。這些 stub 通常實作在 malloc()free()realloc() 的實作中。這些 stub 非常小,因此在大部分的配置器中使用 GWP-ASan 都相當簡單。這些 stub 遵循相同的通用模式(範例 malloc() 虛擬碼如下)

#ifdef INSTALL_GWP_ASAN_STUBS
  gwp_asan::GuardedPoolAllocator GWPASanAllocator;
#endif

void* YourAllocator::malloc(..) {
#ifdef INSTALL_GWP_ASAN_STUBS
  if (GWPASanAllocator.shouldSample(..))
    return GWPASanAllocator.allocate(..);
#endif

  // ... the rest of your allocator code here.
}

然後,支援的配置器只需要使用 -DINSTALL_GWP_ASAN_STUBS 編譯並連結 GWP-ASan 函式庫即可!基於效能考量,我們強烈建議靜態連結 GWP-ASan 函式庫。

受保護的配置池

GWP-ASan 的核心是受保護的配置池。每個取樣的配置都使用其專屬的「受保護」插槽來支援,這些插槽可能包含一或多個可存取的頁面。每個受保護的插槽都被兩個「保護」頁面包圍,這些頁面會被映射為不可存取。所有受保護插槽的集合構成了「受保護的配置池」。

緩衝區下溢/溢位偵測

我們透過這些保護頁面來偵測緩衝區溢位和緩衝區下溢。當記憶體存取超出配置的緩衝區時,它會觸及不可存取的保護頁面,導致記憶體例外。此例外會被內部當機處理常式攔截並處理。由於每個配置都會記錄有關其配置位置(以及由哪個執行緒配置)和解除配置的後設資料,因此我們可以提供有助於識別錯誤根本原因的資訊。

配置會隨機選擇左對齊或右對齊,以便同時偵測下溢和溢位。

釋放後使用偵測

受保護的配置池也提供釋放後使用偵測。每當解除配置取樣的配置時,我們會將其受保護的插槽映射為不可存取。因此,解除配置後的任何記憶體存取都會觸發當機處理常式,並且我們可以提供有關錯誤來源的有用資訊。

請注意,針對取樣配置的釋放後使用偵測是暫時的。為了在偵測錯誤的同時保持固定的記憶體開銷,已解除配置的插槽會隨機重複使用,以保護未來的配置。

用法

GWP-ASan 已預設在 Scudo 強化配置器 中提供,因此使用 -fsanitize=scudo 進行建置是嘗試 GWP-ASan 最快速、最簡單的方法。

選項

GWP-ASan 的組態是由支援的配置器管理。我們提供了一個通用的組態管理函式庫,Scudo 會使用它。它允許透過以下方法組態 GWP-ASan 的多個面向

  • 編譯 GWP-ASan 函式庫時,可以透過設定 -DGWP_ASAN_DEFAULT_OPTIONS 來指定預設選項字串。如果您要將 GWP-ASan 作為 compiler-rt/LLVM 組建的一部分來建置,請在 cmake 配置期間新增它(例如 cmake ... -DGWP_ASAN_DEFAULT_OPTIONS="...")。如果您要在 compiler-rt 之外建置 GWP-ASan,請確保在建置 optional/options_parser.cpp 時指定 -DGWP_ASAN_DEFAULT_OPTIONS="...")。

  • 透過在程式中定義一個 __gwp_asan_default_options 函式來返回要解析的選項字串。該函式必須具有以下原型:extern "C" const char* __gwp_asan_default_options(void),具有預設的可見性。這將會覆寫編譯時期的定義;

  • 根據配置器支援(Scudo 支援此機制):透過環境變數,包含要解析的選項字串。在 Scudo 中,這是透過 SCUDO_OPTIONS=GWP_ASAN_${選項名稱}=${值}(例如 SCUDO_OPTIONS=GWP_ASAN_SampleRate=100)。以這種方式定義的選項將會覆寫透過 __gwp_asan_default_options 進行的任何定義。

選項字串遵循類似於 ASan 的語法,其中不同的選項可以在同一個字串中賦值,並以冒號分隔。

例如,使用環境變數

GWP_ASAN_OPTIONS="MaxSimultaneousAllocations=16:SampleRate=5000" ./a.out

或使用函數

extern "C" const char *__gwp_asan_default_options() {
  return "MaxSimultaneousAllocations=16:SampleRate=5000";
}

以下選項可用

選項

預設值

說明

啟用

true

GWP-ASan 是否已啟用?

PerfectlyRightAlign

false

當配置向右對齊時,我們是否應該將它們完美地對齊到頁面邊界?預設情況下(false),我們會將配置大小向上捨入到最接近的 2 的冪(2、4、8、16),最大為 16 位元組對齊,以提高效能。將此設定為 true 可以找到單一位元組緩衝區溢位,但會犧牲效能,並且可能與某些架構不相容。

MaxSimultaneousAllocations

16

池中可用的同步防護配置數量。

SampleRate

5000

頁面被選中進行 GWP-ASan 採樣的機率(1 / SampleRate)。支援高達 (2^31 - 1) 的採樣率。

InstallSignalHandlers

true

在動態載入期間為 SIGSEGV 安裝 GWP-ASan 信號處理常式。這允許在報告記憶體錯誤時提供配置和解除配置的堆疊追蹤,從而提供更好的錯誤報告。GWP-ASan 的信號處理常式會將信號轉發給任何先前安裝的處理常式,安裝更多信號處理常式的使用者程式應確保它們也執行相同的操作。請注意,如果先前安裝的 SIGSEGV 處理常式是 SIG_IGN,我們會在傾印錯誤報告後終止程序。

範例

以下程式碼有一個使用已釋放記憶體的錯誤,其中 string_view 是作為對 string+ 運算子臨時結果的引用而建立的。使用已釋放記憶體的錯誤發生在第 8 行對 sv 進行解除引用時。

1: #include <iostream>
2: #include <string>
3: #include <string_view>
4:
5: int main() {
6:   std::string s = "Hellooooooooooooooo ";
7:   std::string_view sv = s + "World\n";
8:   std::cout << sv;
9: }

使用 Scudo+GWP-ASan 編譯此程式碼將會機率性地捕獲此錯誤並向我們提供詳細的錯誤報告

$ clang++ -fsanitize=scudo -g buggy_code.cpp
$ for i in `seq 1 500`; do
    SCUDO_OPTIONS="GWP_ASAN_SampleRate=100" ./a.out > /dev/null;
  done
|
| *** GWP-ASan detected a memory error ***
| Use after free at 0x7feccab26000 (0 bytes into a 41-byte allocation at 0x7feccab26000) by thread 31027 here:
|   ...
|   #9 ./a.out(_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St17basic_string_viewIS3_S4_E+0x45) [0x55585c0afa55]
|   #10 ./a.out(main+0x9f) [0x55585c0af7cf]
|   #11 /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xeb) [0x7fecc966952b]
|   #12 ./a.out(_start+0x2a) [0x55585c0867ba]
|
| 0x7feccab26000 was deallocated by thread 31027 here:
|   ...
|   #7 ./a.out(main+0x83) [0x55585c0af7b3]
|   #8 /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xeb) [0x7fecc966952b]
|   #9 ./a.out(_start+0x2a) [0x55585c0867ba]
|
| 0x7feccab26000 was allocated by thread 31027 here:
|   ...
|   #12 ./a.out(main+0x57) [0x55585c0af787]
|   #13 /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xeb) [0x7fecc966952b]
|   #14 ./a.out(_start+0x2a) [0x55585c0867ba]
|
| *** End GWP-ASan report ***
| Segmentation fault

若要符號化這些堆疊追蹤,必須採取一些措施。Scudo 目前使用 GNU 的 backtrace_symbols()(來自 <execinfo.h>)來進行堆疊回溯。堆疊回溯器以 函式+偏移量 的形式提供人類可讀的堆疊追蹤,而不是一般的 二進制+偏移量 形式。為了使用 addr2line 或類似的工具來恢復確切的行號,我們必須將 函式+偏移量 轉換為 二進制+偏移量。在 compiler-rt/lib/gwp_asan/scripts/symbolize.sh 中有一個可用的輔助腳本。使用這個腳本會嘗試符號化每一行可能的程式碼,如果失敗則會回到先前的輸出。這會產生以下輸出:

$ cat my_gwp_asan_error.txt | symbolize.sh
|
| *** GWP-ASan detected a memory error ***
| Use after free at 0x7feccab26000 (0 bytes into a 41-byte allocation at 0x7feccab26000) by thread 31027 here:
| ...
| #9 /usr/lib/gcc/x86_64-linux-gnu/8.0.1/../../../../include/c++/8.0.1/string_view:547
| #10 /tmp/buggy_code.cpp:8
|
| 0x7feccab26000 was deallocated by thread 31027 here:
| ...
| #7 /tmp/buggy_code.cpp:8
| #8 /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xeb) [0x7fecc966952b]
| #9 ./a.out(_start+0x2a) [0x55585c0867ba]
|
| 0x7feccab26000 was allocated by thread 31027 here:
| ...
| #12 /tmp/buggy_code.cpp:7
| #13 /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xeb) [0x7fecc966952b]
| #14 ./a.out(_start+0x2a) [0x55585c0867ba]
|
| *** End GWP-ASan report ***
| Segmentation fault