Scudo 強化的配置器

簡介

Scudo 強化的配置器是一個使用者模式配置器,最初基於 LLVM Sanitizers 的 CombinedAllocator。它的目標是在維持良好效能的同時,提供額外的緩解措施來對抗基於堆積的漏洞。Scudo 目前是 Fuchsia 和自 Android 11 以來的 Android 中的預設配置器。

名稱 “Scudo” 源自義大利語單詞 盾牌(以及西班牙語的 Escudo)。

設計

配置器

Scudo 的設計著重於安全性,但也旨在在安全性和效能之間取得良好的平衡。它的設計具有高度可調整性和可配置性,雖然我們提供了一些預設配置,但我們鼓勵使用者提出最適合其使用案例的參數。

此配置器結合了幾個組件,這些組件服務於不同的目的

  • 主要配置器:快速且有效率,它透過將保留的記憶體區域劃分為相同大小的區塊來服務較小的配置大小。目前實作了兩個主要配置器,分別針對 32 位元和 64 位元架構。它是可透過編譯時期選項配置的。

  • 次要配置器:速度較慢,它透過底層作業系統的記憶體對應原語來服務較大的配置大小。次要支援的配置周圍環繞著保護頁面。它也是可透過編譯時期選項配置的。

  • 執行緒特定資料登錄檔:定義每個執行緒的本機快取如何運作。目前實作了兩種模型:獨佔模型,其中每個執行緒都擁有自己的快取(使用 ELF TLS);或共享模型,其中執行緒共享固定大小的快取池。

  • 隔離區:提供一種延遲解除配置操作的方法,防止區塊立即可用於重複使用。一旦達到某些大小標準,持有的區塊將被回收。這本質上是一個延遲的可用列表,可以幫助緩解一些使用後釋放的情況。此功能在效能和記憶體佔用方面相當昂貴,主要由執行時期選項控制,並且預設為停用。

配置標頭

配置器傳回給應用程式的每個堆積記憶體區塊都將以標頭開頭。這有兩個目的

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

  • 能夠偵測潛在的損壞。為此目的,標頭經過校驗和,並且當存取該標頭時,將偵測到標頭的損壞(請注意,如果未存取損壞的標頭,則損壞將保持未偵測到)。

以下資訊儲存在標頭中

  • 該區塊的類別 ID,用於識別主要支援配置的區塊所在的區域,或次要支援配置的 0;

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

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

  • 該區塊的大小(主要)或未使用位元組數(次要),這對於重新配置或調整大小的解除配置操作是必要的;

  • 區塊的偏移量,即從傳回的區塊開始到後端配置(「區塊」)開始的位元組距離;

  • 16 位元校驗和;

此標頭在所有支援的平台上都適合 8 個位元組,並且為每個配置貢獻了少量額外開銷。

校驗和是使用全域密碼、區塊指標本身以及標頭的 8 個位元組(校驗和欄位歸零)的 CRC32(透過硬體支援加速)計算的。它並非旨在成為密碼學上強大的。

標頭是原子載入和儲存的,以防止競爭。這很重要,因為兩個連續的區塊可能屬於不同的執行緒。我們處理本機副本並使用比較交換原語來更新堆積記憶體中的標頭,並避免任何類型的雙重提取。

隨機性

隨機性是配置器提供的額外安全性的關鍵因素。配置器信任作業系統的記憶體對應原語在記憶體中提供(大部分)不可預測位置的頁面,以及使用 ASLR 編譯的二進制檔案。如果其中一個假設不正確,則安全性將大大降低。Scudo 進一步隨機化區塊在主要配置器中的配置方式,可以隨機化快取分配給執行緒的方式。

記憶體回收

主要和次要配置器在回收方面具有不同的行為。雖然次要對應的配置可以在解除配置時取消對應,但主要配置則不然,這可能會導致進程的 RSS 穩定成長。為了抵消這一點,如果底層作業系統允許,則主要配置中連續可用記憶體區塊覆蓋的頁面可以釋放:這通常表示它們不會計入進程的 RSS,並且在後續存取時將填零)。這是在解除配置路徑中完成的,並且存在多個選項來調整此行為。

使用方式

平台

如果使用 Fuchsia 或 Android 11 以上版本,您的記憶體配置已經由 Scudo 服務(請注意,Android Svelte 配置仍然使用 jemalloc)。

函式庫

可以從 LLVM 樹狀結構建置配置器靜態函式庫,這要歸功於 scudo_standalone CMake 規則。相關的測試可以透過 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

調整配置器對記憶體標籤的選擇,使其更有可能偵測到特定類型的記憶體錯誤。value 引數應為 scudo_memtag_tuning 的列舉器之一。

M_THREAD_DISABLE_MEM_INIT

調整每個執行緒的記憶體初始化,0 為正常行為,1 為停用自動堆積初始化。

M_CACHE_COUNT_MAX

設定次要快取中可以快取的最大條目數。

M_CACHE_SIZE_MAX

設定次要快取中可以快取的最大條目大小。

M_TSDS_COUNT_MAX

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

錯誤類型

當偵測到意外行為時,配置器將輸出錯誤訊息,並可能終止進程。輸出通常以 "Scudo ERROR:" 開頭,後跟發生的問題的簡短摘要以及涉及的指標。再次強調,Scudo 旨在成為一種緩解措施,可能不是幫助您找出問題根本原因的最有用的工具,請考慮使用 ASan 來達到此目的。

以下是目前錯誤訊息及其潛在原因的列表

  • "corrupted chunk header":區塊標頭的校驗和驗證失敗。這很可能是由於以下兩種情況之一:標頭被覆寫(部分或全部),或者傳遞給函式的指標根本不是區塊;

  • "race on chunk header":兩個不同的執行緒正在嘗試同時操作同一個標頭。這通常是競爭條件或在對該區塊執行操作時普遍缺乏鎖定的症狀;

  • "invalid chunk state":區塊未處於給定操作的預期狀態,例如:嘗試釋放時未配置,或者嘗試回收時未隔離等。重複釋放是發生此錯誤的典型原因;

  • "misaligned pointer":我們強烈執行基本對齊要求,32 位元平台為 8 個位元組,64 位元平台為 16 個位元組。如果傳遞給我們函式的指標不符合這些要求,則肯定有問題。

  • "allocation type mismatch":當啟用可選的解除配置類型不匹配檢查時,在區塊上呼叫的解除配置函式必須與呼叫以配置它的函式類型相符。這種不匹配的安全性影響不一定很明顯,但在最佳情況下是情境性的;

  • "invalid sized delete":當使用 C++14 大小調整的 delete 運算符,並且啟用可選檢查時,這表示在解除配置區塊時傳遞的大小與配置時請求的大小不一致。這很可能是 編譯器問題,就像 Intel C++ 編譯器的情況一樣,或者正在解除配置的物件上的某些類型混淆;

  • "RSS limit exhausted":已超過可選指定的最大 RSS;

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