GWP-ASan¶
簡介¶
GWP-ASan 是一個取樣的記憶體配置器框架,旨在協助在生產環境中尋找釋放後使用和堆積緩衝區溢位錯誤。它非正式地是一個遞迴縮寫,代表 “GWP-ASan Will Provide Allocation SANity”(GWP-ASan 將提供記憶體配置健全性)。
GWP-ASan 基於經典的 Electric Fence Malloc Debugger,並進行了關鍵的改編。值得注意的是,我們僅選擇非常小比例的記憶體配置進行取樣,並僅對這些取樣的記憶體配置應用保護頁面。取樣比例非常小,足以讓我們擁有非常低的效能開銷。
存在一個小的、可調整的記憶體開銷,在程序的生命週期內是固定的。使用預設設定時,每個程序約為 ~40KiB,具體取決於您的平均記憶體配置大小。
GWP-ASan vs. ASan¶
與 AddressSanitizer 不同,GWP-ASan 不會造成顯著的效能開銷。ASan 通常需要使用專用的金絲雀值才能在生產環境中可行,因此通常是不切實際的。此外,ASan 的執行時期並非以安全性考量為出發點開發,使得編譯後的二進制文件更容易受到漏洞利用。
然而,GWP-ASan 僅能偵測到 ASan 偵測到的記憶體問題的子集。此外,GWP-ASan 的錯誤偵測能力僅為機率性的。因此,我們建議在測試以及任何需要保證錯誤偵測比 2 倍執行速度減慢/二進制文件大小膨脹更重要的地方,使用 ASan 而非 GWP-ASan。對於大多數生產環境而言,這種影響過於巨大且安全性至關重要,因此 GWP-ASan 被證明非常有用。
設計¶
請注意: GWP-ASan 的實作在很大程度上處於變動中,這些細節可能會發生變化。目前還有其他 GWP-ASan 的實作,例如 Chromium 中提供的實作。長期支援目標是確保在合理範圍內的功能對等性,並支援 compiler-rt 作為參考實作。
記憶體配置器支援¶
GWP-ASan 不是傳統記憶體配置器的替代品。相反,它的工作原理是在支援的記憶體配置器中插入存根,以便在選擇取樣記憶體配置時將其重新導向到 GWP-ASan。這些存根通常在 malloc()
、free()
和 realloc()
的實作中實作。這些存根非常小,這使得在大多數記憶體配置器中使用 GWP-ASan 相當容易。這些存根遵循相同的通用模式(以下是 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
設定為您想要預設設定的選項字串。如果您正在建置作為 compiler-rt/LLVM 建置一部分的 GWP-ASan,請在 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_${OPTION_NAME}=${VALUE}(例如 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";
}
以下選項可用
選項 |
預設值 |
描述 |
Enabled |
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>
) 進行回溯。回溯器以 function+offset
格式而不是正常的 binary+offset
格式提供人類可讀的堆疊追蹤。為了使用 addr2line 或類似工具恢復確切的行號,我們必須將 function+offset
轉換為 binary+offset
。compiler-rt/lib/gwp_asan/scripts/symbolize.sh
提供了一個 helper 腳本。使用此腳本將嘗試符號化每一行可能的行,如果任何操作失敗,則回退到先前的輸出。這會產生以下輸出
$ 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