Scudo 强固型分配器

簡介

Scudo 强固型分配器是一個使用者模式分配器,最初基於 LLVM Sanitizers 的 CombinedAllocator。它的目標是在保持良好效能的同時,提供針對基於堆積的漏洞的額外緩解措施。Scudo 目前是 FuchsiaAndroid(從 Android 11 開始)中的預設分配器。

“Scudo”這個名字來自義大利語中的 盾牌(西班牙語為 Escudo)。

設計

分配器

Scudo 在設計時考慮了安全性,但目標是在安全性和效能之間取得良好的平衡。它被設計為高度可調和可配置的,雖然我們提供了一些預設配置,但我們鼓勵使用者根據他們的用例提出最適合的參數。

分配器結合了幾個服務於不同目的的組件

  • 主要分配器:快速且高效,它通過將保留的記憶體區域劃分為大小相同的區塊來服務較小的分配大小。目前有兩種主要分配器的實現,分別針對 32 位和 64 位架構。它可以通过编译时选项进行配置。

  • 次要分配器:速度較慢,它通過底層作業系統的記憶體映射原語來服務較大的分配大小。次要支援的分配被保護頁包圍。它也可以通过编译时选项进行配置。

  • 執行緒特定數據註冊表:定義每個執行緒的本地緩存如何運作。目前有兩種模型實現:獨佔模型,其中每個執行緒都擁有自己的緩存(使用 ELF TLS);或共享模型,其中執行緒共享一個固定大小的緩存池。

  • 隔離區:提供了一種延遲釋放操作的方法,防止區塊立即被重用。一旦達到某些大小標準,將回收保留的區塊。這本質上是一個延遲的空閒列表,可以幫助減輕一些使用後釋放的情況。此功能在效能和記憶體佔用方面相當昂貴,主要由執行時選項控制,預設情況下處於禁用狀態。

分配標頭

分配器返回給應用程式的每一塊堆積記憶體前面都有一個標頭。這有兩個目的

  • 用於儲存有關區塊的各種信息,這些信息可用於確保堆積操作的一致性;

  • 能夠檢測潛在的損壞。 為此,標頭已進行校驗和,並且在訪問所述標頭時將檢測到標頭的損壞(請注意,如果未訪問損壞的標頭,則損壞將保持未檢測狀態)。

標頭中存儲以下信息

  • 該區塊的類別 ID,它標識了區塊所在區域,用於主要後端分配,或用於次要後端分配的 0;

  • 區塊的狀態(可用、已分配或已隔離);

  • 分配類型(malloc、new、new [] 或 memalign),用於檢測所使用的分配 API 中的潛在不匹配;

  • 該區塊的大小(主要)或未使用字節數(次要),這對於重新分配或大小調整操作是必需的;

  • 區塊的偏移量,它是從返回區塊的開頭到後端分配(“塊”)的開頭的以字節為單位的距離;

  • 16 位校驗和;

此標頭在所有受支持的平台上都適合 8 個字節,並且會為每次分配增加少量開銷。

校驗和是使用全局機密的 CRC32(通過硬件支持使其更快)、區塊指針本身以及校驗和字段歸零的 8 個字節的標頭來計算的。 它並非旨在成為加密強壯的。

標頭以原子方式加載和存儲以防止競爭。 這一點很重要,因為兩個連續的區塊可能屬於不同的線程。 我們處理本地副本並使用比較交換原語來更新堆內存中的標頭,並避免任何類型的雙重獲取。

隨機性

隨機性是分配器提供的額外安全性的關鍵因素。 分配器信任操作系統的內存映射原語,以在內存中的(大部分)不可預測的位置提供頁面,以及使用 ASLR 編譯的二進制文件。 如果其中一個假設不正確,安全性將大大降低。 Scudo 進一步隨機化如何在主節點中分配塊,可以隨機化緩存如何分配給線程。

內存回收

主要和次要分配器在回收方面具有不同的行為。 雖然可以在解除分配時取消映射次要映射的分配,但主要分配不是這樣,這可能會導致進程的 RSS 穩定增長。 為了應對這種情況,如果底層操作系統允許,則可以釋放主要分配中被連續空閒內存塊覆蓋的頁面:這通常意味著它們不會計入進程的 RSS 並且在後續訪問時填充零)。 這在解除分配路徑中完成,並且存在多個選項來調整此行為。

使用方法

平台

如果您使用的是 Fuchsia 或 Android 11 以上的版本,則您的內存分配已經由 Scudo 提供服務(請注意,Android Svelte 配置仍然使用 jemalloc)。

函式庫

分配器靜態函式庫可以通過 scudo_standalone CMake 規則從 LLVM 樹構建。 可以通過 check-scudo_standalone CMake 規則來執行相關測試。

將靜態程式庫連結到您的專案可能需要使用 whole-archive 連結器標誌(或等效標誌),具體取決於您的連結器。也可能需要其他標誌。

您連結的二進制檔案現在應該使用 Scudo 的配置和解除配置函數。

您也可以像這樣建置 Scudo

cd $LLVM/compiler-rt/lib
clang++ -fPIC -std=c++17 -msse4.2 -O2 -pthread -shared \
  -I scudo/standalone/include \
  scudo/standalone/*.cpp \
  -o $HOME/libscudo.so

然後將其與現有的二進制檔案一起使用,如下所示

LD_PRELOAD=$HOME/libscudo.so ./a.out

Clang

使用最新版本的 Clang(rL317337 之後),如果目標平台支援,則可以使用 -fsanitize=scudo 命令列參數在編譯時將「舊」版本的配置器與二進制檔案連結。目前,Scudo 唯一相容的其他消毒器是 UBSan(例如:-fsanitize=scudo,undefined)。使用 Scudo 進行編譯也會對輸出二進制檔案強制執行 PIE。

我們將來會將其轉換為獨立的 Scudo 版本。

選項

配置器的幾個方面可以透過以下方式在每個程序的基礎上進行配置

  • 在編譯時,將 SCUDO_DEFAULT_OPTIONS 定義為您希望預設設定的選項字串;

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

  • 透過環境變數 SCUDO_OPTIONS,其中包含要解析的選項字串。以這種方式定義的選項將覆蓋透過 __scudo_default_options 進行的任何定義。

  • 透過標準 mallopt API,使用 Scudo 特定的參數。

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

例如,使用環境變數

SCUDO_OPTIONS="delete_size_mismatch=false:release_to_os_interval_ms=-1" ./a.out

或使用函數

extern "C" const char *__scudo_default_options() {
  return "delete_size_mismatch=false:release_to_os_interval_ms=-1";
}

以下「字串」選項可用

選項

預設值

描述

quarantine_size_kb

0

用於延遲實際解除區塊配置的隔離區大小(以 Kb 為單位)。較低的值可能會減少記憶體使用量,但會降低緩解措施的有效性;負值將回退到預設值。將此值和 thread_local_quarantine_size_kb 都設定為零將完全停用隔離區。

quarantine_max_chunk_size

0

可以隔離的區塊大小上限(以位元組為單位)。

thread_local_quarantine_size_kb

0

每個執行緒快取的大小(以 Kb 為單位),用於卸載全域隔離區。較低的值可能會減少記憶體使用量,但可能會增加全域隔離區的爭用。將此值和 quarantine_size_kb 都設定為零將完全停用隔離區。

dealloc_type_mismatch

false

我們是否報告 malloc/delete、new/free、new/delete[] 等方面的錯誤。

delete_size_mismatch

true

我們是否報告 new 和 delete 的大小不符的錯誤。

zero_contents

false

我們是否在配置時將區塊內容歸零。

pattern_fill_contents

false

我們是否在配置時用位元組模式填充區塊內容。

may_return_null

true

非致命錯誤是否可以返回 NULL 指標(而不是終止)。

release_to_os_interval_ms

5000

可以嘗試釋放的最小間隔(以毫秒為單位)(負值表示停用回收)。

allocation_ring_buffer_size

32768

如果請求堆疊追蹤收集,則在配置環形緩衝區中保留多少個先前的配置。

此緩衝區用於為 MTE 錯誤報告提供分配和釋放堆疊追蹤。緩衝區越大,在(取消)分配和錯誤之間可能發生的無關分配就越多。如果您的同步模式 MTE 錯誤沒有(取消)分配堆疊追蹤,請嘗試增加緩衝區大小。

可以使用 scudo_malloc_set_track_allocation_stacks 函數請求堆疊追蹤收集。

可以指定其他標誌,例如,如果 Scudo 使用 GWP-ASan 支持進行編譯。

以下「mallopt」選項可用(選項在 include/scudo/interface.h 中定義)

選項

描述

M_DECAY_TIME

將釋放間隔選項設置為指定值(Android 僅允許 0 或 1 分別將間隔設置為編譯時指定的最小值和最大值)。

M_PURGE

強制立即回收內存,但不會回收所有內容。對於較小的尺寸類別,由於需要額外的時間以及可以回收的內存量較小,因此仍然有一些內存沒有被回收。該值將被忽略。

M_PURGE_ALL

與 M_PURGE 相同,但會強制釋放所有可能的內存,無論需要多長時間。該值將被忽略。

M_MEMTAG_TUNING

調整分配器對內存標記的選擇,使其更有可能檢測到某一類的內存錯誤。值參數應該是 scudo_memtag_tuning 的其中一個列舉值。

M_THREAD_DISABLE_MEM_INIT

調整每個線程的內存初始化,0 為正常行為,1 為禁用自動堆初始化。

M_CACHE_COUNT_MAX

設置可以在輔助緩存中緩存的最大條目數。

M_CACHE_SIZE_MAX

設置可以在輔助緩存中緩存的條目的最大大小。

M_TSDS_COUNT_MAX

增加可使用的 TSD 的最大數量,最高可達編譯時指定的限制。

錯誤類型

當檢測到意外行為時,分配器將輸出錯誤消息,並可能終止進程。輸出通常以 "Scudo 錯誤: 開頭,後面是發生的問題的簡要概述以及涉及的指針。再次強調,Scudo 的目的是作為一種緩解措施,可能不是幫助您找出問題根源的最有效的工具,請考慮 ASan 來達到此目的。

以下是當前錯誤消息及其可能原因的列表

  • 「損壞的區塊標頭」:區塊標頭的校驗和驗證失敗。這可能是由於以下兩件事之一:標頭被覆蓋(部分或全部),或者傳遞給函數的指針根本不是區塊;

  • 「區塊標頭上的競爭條件」:兩個不同的線程試圖同時操作同一個標頭。這通常是競爭條件或在對該區塊執行操作時缺乏鎖定的症狀;

  • 「無效的區塊狀態」:對於給定的操作,區塊的狀態不符合預期,例如:在嘗試釋放它時它沒有被分配,或者在嘗試回收它時它沒有被隔離,等等。重複釋放是導致此錯誤的典型原因;

  • 「指標未對齊」:我們嚴格執行基本對齊要求,在 32 位元平台上為 8 位元組,在 64 位元平台上為 16 位元組。如果傳遞給我們函數的指標不符合這些要求,則肯定有問題。

  • 「配置類型不匹配」:當啟用可選的釋放類型不匹配檢查時,在區塊上呼叫的釋放函數必須與用於配置它的函數類型相匹配。這種不匹配的安全隱患不一定很明顯,但在最佳情況下也是情境性的;

  • 「無效的大小刪除」:當使用 C++14 大小刪除運算子,並且啟用了可選檢查時,這表示釋放區塊時傳遞的大小與配置時請求的大小不一致。這很可能是編譯器問題,就像 Intel C++ 編譯器的情況一樣,或者是被釋放對象的某些類型混淆;

  • 「RSS 限制已滿」:已超過可選指定的 RSS 最大值;

其他幾個錯誤消息與 libc 配置 API 上的參數檢查有關,並且非常容易理解。