瞭解 MTE 報表

SIGSEGV 發生錯誤時,會顯示代碼 9 (SEGV_MTESERR) 或代碼 8 (SEGV_MTEAERR),這些都是記憶體標記錯誤。 Memory Tagging Extension (MTE) 是 Android 12 以上版本支援的 Armv9 功能。MTE 是標記記憶體的硬體實作方式。提供精細的記憶體保護功能,可偵測及減輕記憶體安全錯誤

在 C/C++ 中,從對 malloc() 或運算子 new() 或類似函式的呼叫傳回的指標,只能用於存取該配置的邊界內的記憶體,且只能在配置處於活動狀態 (未釋放或刪除) 時使用。Android 會使用 MTE 偵測違反此規則的情況,這在當機報告中稱為「緩衝區溢位」/「緩衝區欠流」和「釋放後使用」問題。

MTE 有兩種模式:同步 (或稱「sync」) 和非同步 (或稱「async」)。前者執行速度較慢,但提供更準確的診斷資訊。後者執行速度較快,但只能提供大致的詳細資料。由於兩者的診斷方式略有不同,我們會分別說明這兩種情況。

同步模式 MTE

在 MTE 的同步 (「sync」) 模式中,SIGSEGV 會因程式碼 9 (SEGV_MTESERR) 而當機。

pid: 13935, tid: 13935, name: sanitizer-statu  >>> sanitizer-status <<<
uid: 0
tagged_addr_ctrl: 000000000007fff3
signal 11 (SIGSEGV), code 9 (SEGV_MTESERR), fault addr 0x800007ae92853a0
Cause: [MTE]: Use After Free, 0 bytes into a 32-byte allocation at 0x7ae92853a0
x0  0000007cd94227cc  x1  0000007cd94227cc  x2  ffffffffffffffd0  x3  0000007fe81919c0
x4  0000007fe8191a10  x5  0000000000000004  x6  0000005400000051  x7  0000008700000021
x8  0800007ae92853a0  x9  0000000000000000  x10 0000007ae9285000  x11 0000000000000030
x12 000000000000000d  x13 0000007cd941c858  x14 0000000000000054  x15 0000000000000000
x16 0000007cd940c0c8  x17 0000007cd93a1030  x18 0000007cdcac6000  x19 0000007fe8191c78
x20 0000005800eee5c4  x21 0000007fe8191c90  x22 0000000000000002  x23 0000000000000000
x24 0000000000000000  x25 0000000000000000  x26 0000000000000000  x27 0000000000000000
x28 0000000000000000  x29 0000007fe8191b70
lr  0000005800eee0bc  sp  0000007fe8191b60  pc  0000005800eee0c0  pst 0000000060001000

backtrace:
      #00 pc 00000000000010c0  /system/bin/sanitizer-status (test_crash_malloc_uaf()+40) (BuildId: 953fc93301472d0b72709b2b9a9f6f30)
      #01 pc 00000000000014a4  /system/bin/sanitizer-status (test(void (*)())+132) (BuildId: 953fc93301472d0b72709b2b9a9f6f30)
      #02 pc 00000000000019cc  /system/bin/sanitizer-status (main+1032) (BuildId: 953fc93301472d0b72709b2b9a9f6f30)
      #03 pc 00000000000487d8  /apex/com.android.runtime/lib64/bionic/libc.so (__libc_init+96) (BuildId: 6ab39e35a2fae7efbe9a04e9bbb14331)

deallocated by thread 13935:
      #00 pc 000000000004643c  /apex/com.android.runtime/lib64/bionic/libc.so (scudo::Allocator<scudo::AndroidConfig, &(scudo_malloc_postinit)>::quarantineOrDeallocateChunk(scudo::Options, void*, scudo::Chunk::UnpackedHeader*, unsigned long)+688) (BuildId: 6ab39e35a2fae7efbe9a04e9bbb14331)
      #01 pc 00000000000421e4  /apex/com.android.runtime/lib64/bionic/libc.so (scudo::Allocator<scudo::AndroidConfig, &(scudo_malloc_postinit)>::deallocate(void*, scudo::Chunk::Origin, unsigned long, unsigned long)+212) (BuildId: 6ab39e35a2fae7efbe9a04e9bbb14331)
      #02 pc 00000000000010b8  /system/bin/sanitizer-status (test_crash_malloc_uaf()+32) (BuildId: 953fc93301472d0b72709b2b9a9f6f30)
      #03 pc 00000000000014a4  /system/bin/sanitizer-status (test(void (*)())+132) (BuildId: 953fc93301472d0b72709b2b9a9f6f30)

allocated by thread 13935:
      #00 pc 0000000000042020  /apex/com.android.runtime/lib64/bionic/libc.so (scudo::Allocator<scudo::AndroidConfig, &(scudo_malloc_postinit)>::allocate(unsigned long, scudo::Chunk::Origin, unsigned long, bool)+1300) (BuildId: 6ab39e35a2fae7efbe9a04e9bbb14331)
      #01 pc 0000000000042394  /apex/com.android.runtime/lib64/bionic/libc.so (scudo_malloc+36) (BuildId: 6ab39e35a2fae7efbe9a04e9bbb14331)
      #02 pc 000000000003cc9c  /apex/com.android.runtime/lib64/bionic/libc.so (malloc+36) (BuildId: 6ab39e35a2fae7efbe9a04e9bbb14331)
      #03 pc 00000000000010ac  /system/bin/sanitizer-status (test_crash_malloc_uaf()+20) (BuildId: 953fc93301472d0b72709b2b9a9f6f30)
      #04 pc 00000000000014a4  /system/bin/sanitizer-status (test(void (*)())+132) (BuildId: 953fc93301472d0b72709b2b9a9f6f30)

所有 MTE 當機報告都會包含一般註冊傾印和回溯追蹤,用於偵測問題的時間點。MTE 偵測到的錯誤「Cause:」行會包含「[MTE]」(如上例所示),並提供更多詳細資料。在本例中,系統偵測到的特定錯誤類型是「Use after free」,而「0 bytes into a 32-byte allocation at 0x7ae92853a0」則會告訴我們分配的大小和位址,以及我們嘗試存取的分配偏移量。

MTE 當機報告也包含額外的回溯追蹤,而非僅限於偵測點。

「Use After Free」錯誤會在當機傾印中加入「deallocated by」和「allocated by」部分,顯示記憶體釋放 (使用前) 和先前分配的堆疊追蹤記錄。這些資訊也會告訴您是哪個執行緒執行了分配/解除分配作業。在這個簡單的範例中,偵測執行緒、配置執行緒和釋放執行緒這三者都是相同的,但在更複雜的實際情況中,這不一定是正確的,瞭解這三者之間的差異,有助於找出與並行處理相關的錯誤。

「緩衝區溢位」和「緩衝區不足」錯誤只會提供額外的「allocated by」堆疊追蹤,因為依定義來說,這些錯誤尚未解除配置 (否則會顯示為「Use After Free」):

Cause: [MTE]: Buffer Overflow, 0 bytes right of a 32-byte allocation at 0x7ae92853a0
[...]
backtrace:
[...]
allocated by thread 13949:

請注意這裡使用的「right」一詞:這表示我們會告訴您,錯誤存取的位元組超出分配區尾端的數量;而溢位則會顯示「left」,並顯示分配區開始前的位元組數量。

多個可能原因

有時 SEGV_MTESERR 報告會包含以下行:

Note: multiple potential causes for this crash were detected, listing them in decreasing order of likelihood.

當錯誤來源有幾個可能的候選項,而我們無法判斷哪一個才是實際原因時,就會發生這種情況。我們會依據可能性高低的順序,列印最多 3 個候選項目,並將分析工作交由使用者負責。

signal 11 (SIGSEGV), code 9 (SEGV_MTESERR), fault addr 0x400007b43063db5
backtrace:
    [stack...]

Note: multiple potential causes for this crash were detected, listing them in decreasing order of probability.

Cause: [MTE]: Use After Free, 5 bytes into a 10-byte allocation at 0x7b43063db0
deallocated by thread 6663:
    [stack...]
allocated by thread 6663:
    [stack...]

Cause: [MTE]: Use After Free, 5 bytes into a 6-byte allocation at 0x7b43063db0
deallocated by thread 6663:
    [stack...]

allocated by thread 6663:
    [stack...]

在上例中,我們偵測到最近在相同記憶體位址中分配了兩個可能為無效記憶體存取的預期目標。當配置重新使用空閒記憶體時,就可能發生這種情況,例如如果您有 new、free、new、free、new、free、access 等序列。較新的分配會先列印。

詳細原因判斷法則

當發生當機時,系統應顯示「Cause」(原因),說明存取的指標原本來自哪裡的記憶體配置。很抱歉,MTE 硬體無法將標記不相符的指標轉譯為分配。為了說明 SEGV_MTESERR 當機事件,Android 會分析下列資料:

  • 錯誤位址 (包括指標標記)。
  • 近期堆積分配清單,其中包含堆疊追蹤和記憶體標記。
  • 附近的目前 (使用中) 配置和其記憶體代碼。

在錯誤位址中,任何最近已重新配置的記憶體 (記憶體標記與錯誤位址標記相符) 都可能是潛在的「Use After Free」原因。

任何記憶體標記與錯誤位址標記相符的鄰近記憶體,都可能是潛在的「緩衝區溢位」(或「緩衝區不足」) 原因。

與錯誤時間或空間上較接近的分配項目,比遠離的分配項目更有可能是錯誤來源。

由於已釋放的記憶體通常會重複使用,且不同的標記值數量很少 (少於 16 個),因此經常會發現幾個可能的候選項,而且無法自動找出真正的原因。因此,MTE 報告有時會列出多個可能的原因。

建議應用程式開發人員從最可能的原因開始,找出潛在原因。您通常可以輕鬆根據堆疊追蹤,篩除不相關的原因。

非同步模式 MTE

在 MTE 的非同步 (「async」) 模式中,SIGSEGV 會因程式碼 8 (SEGV_MTEAERR) 而當機。

當程式執行無效的記憶體存取作業時,不會立即發生 SEGV_MTEAERR 錯誤。系統會在事件發生後不久偵測到問題,並在該時點終止程式。這個點通常是下一個系統呼叫,但也可能是計時器中斷,簡而言之,任何使用者空間到核心的轉換。

SEGV_MTEAERR 錯誤不會保留記憶體位址 (一律顯示為「-------」)。回溯追蹤會對應到偵測到條件的時刻 (也就是下一個系統呼叫或其他情境切換),而非執行無效存取的時間。

也就是說,非同步 MTE 當機事件中的「main」回溯追蹤通常「不相關」。因此,與同步模式失敗相比,異步模式失敗的偵錯難度會高出許多。最容易理解的方式是,這些訊息表示在指定執行緒的附近程式碼中存在記憶體錯誤。墓碑檔案底部的記錄可能會提供實際發生的情況。否則,建議您在同步模式中重現錯誤,並使用同步模式提供的更完善診斷功能!

進階主題

記憶體標記的運作方式是將隨機 4 位元 (0..15) 標記值指派給每個堆積配置。這個值會儲存在與已分配堆積記憶體相對應的特殊中繼資料區域中。同樣的值會指派給從 malloc() 或 operator new() 等函式傳回的堆積指標的最高位元組。

在程序中啟用標記檢查功能後,CPU 會自動比較指標的頂層位元組與每個記憶體存取的記憶體標記。如果標記不相符,CPU 會發出錯誤信號,導致應用程式當機。

由於可能的標記值有限,因此這種方法是概率性的。任何不應使用特定指標存取的記憶體位置 (例如超出邊界或在釋放後) (「懸掛指標」) 都可能具有不同的標記值,並導致當機。約有 7% 的機率無法偵測到任何錯誤。由於代碼值是隨機指派的,因此下次發生錯誤時,偵測到錯誤的機率約為 93%。

標記值會顯示在錯誤位址欄位和註冊傾印中,如下所示。您可以使用這一節來檢查標記是否以正確的方式設定,並查看其他具有相同標記值的鄰近記憶體配置,因為這些可能會導致錯誤,而這類錯誤並未列在報告中。我們認為這項功能主要對負責實作 MTE 本身或其他低階系統元件的人員有用,而非開發人員。

signal 11 (SIGSEGV), code 9 (SEGV_MTESERR), fault addr 0x0800007ae92853a0
Cause: [MTE]: Use After Free, 0 bytes into a 32-byte allocation at 0x7ae92853a0
    x0  0000007cd94227cc  x1  0000007cd94227cc  x2  ffffffffffffffd0  x3  0000007fe81919c0
    x4  0000007fe8191a10  x5  0000000000000004  x6  0000005400000051  x7  0000008700000021
    x8  0800007ae92853a0  x9  0000000000000000  x10 0000007ae9285000  x11 0000000000000030
    x12 000000000000000d  x13 0000007cd941c858  x14 0000000000000054  x15 0000000000000000
    x16 0000007cd940c0c8  x17 0000007cd93a1030  x18 0000007cdcac6000  x19 0000007fe8191c78
    x20 0000005800eee5c4  x21 0000007fe8191c90  x22 0000000000000002  x23 0000000000000000
    x24 0000000000000000  x25 0000000000000000  x26 0000000000000000  x27 0000000000000000
    x28 0000000000000000  x29 0000007fe8191b70
    lr  0000005800eee0bc  sp  0000007fe8191b60  pc  0000005800eee0c0  pst 0000000060001000

錯誤報告中也會顯示特殊的「Memory tags」部分,顯示錯誤位址附近的記憶體標記。在下方範例中,指標標記「4」與記憶體標記「a」不符。

Memory tags around the fault address (0x0400007b43063db5), one tag per 16 bytes:
  0x7b43063500: 0  f  0  2  0  f  0  a  0  7  0  8  0  7  0  e
  0x7b43063600: 0  9  0  8  0  5  0  e  0  f  0  c  0  f  0  4
  0x7b43063700: 0  b  0  c  0  b  0  2  0  1  0  4  0  7  0  8
  0x7b43063800: 0  b  0  c  0  3  0  a  0  3  0  6  0  b  0  a
  0x7b43063900: 0  3  0  4  0  f  0  c  0  3  0  e  0  0  0  c
  0x7b43063a00: 0  3  0  2  0  1  0  8  0  9  0  4  0  3  0  4
  0x7b43063b00: 0  5  0  2  0  5  0  a  0  d  0  6  0  d  0  2
  0x7b43063c00: 0  3  0  e  0  f  0  a  0  0  0  0  0  0  0  4
=>0x7b43063d00: 0  0  0  a  0  0  0  e  0  d  0 [a] 0  f  0  e
  0x7b43063e00: 0  7  0  c  0  9  0  a  0  d  0  2  0  0  0  c
  0x7b43063f00: 0  0  0  6  0  b  0  8  0  3  0  0  0  5  0  e
  0x7b43064000: 0  d  0  2  0  7  0  a  0  7  0  a  0  d  0  8
  0x7b43064100: 0  b  0  2  0  b  0  4  0  1  0  6  0  d  0  4
  0x7b43064200: 0  1  0  6  0  f  0  2  0  f  0  6  0  5  0  c
  0x7b43064300: 0  1  0  4  0  d  0  6  0  f  0  e  0  1  0  8
  0x7b43064400: 0  f  0  4  0  3  0  2  0  1  0  2  0  5  0  6

顯示所有註冊值周圍記憶體內容的墓碑區段,也會顯示其標記值。

memory near x10 ([anon:scudo:primary]):
0000007b4304a000 7e82000000008101 000003e9ce8b53a0  .......~.S......
0700007b4304a010 0000200000006001 0000000000000000  .`... ..........
0000007b4304a020 7c03000000010101 000003e97c61071e  .......|..a|....
0200007b4304a030 0c00007b4304a270 0000007ddc4fedf8  p..C{.....O.}...
0000007b4304a040 84e6000000008101 000003e906f7a9da  ................
0300007b4304a050 ffffffff00000042 0000000000000000  B...............
0000007b4304a060 8667000000010101 000003e9ea858f9e  ......g.........
0400007b4304a070 0000000100000001 0000000200000002  ................
0000007b4304a080 f5f8000000010101 000003e98a13108b  ................
0300007b4304a090 0000007dd327c420 0600007b4304a2b0   .'.}......C{...
0000007b4304a0a0 88ca000000010101 000003e93e5e5ac5  .........Z^>....
0a00007b4304a0b0 0000007dcc4bc500 0300007b7304cb10  ..K.}......s{...
0000007b4304a0c0 0f9c000000010101 000003e9e1602280  ........."`.....
0900007b4304a0d0 0000007dd327c780 0700007b7304e2d0  ..'.}......s{...
0000007b4304a0e0 0d1d000000008101 000003e906083603  .........6......
0a00007b4304a0f0 0000007dd327c3b8 0000000000000000  ..'.}...........