可靠的堆疊跟蹤¶
本文件概述了有關可靠堆疊跟蹤的基本資訊。
1. 簡介¶
核心 livepatch 一致性模型依賴於準確識別哪些函式可能具有活動狀態,因此可能不適合修補。 識別哪些函式是活動的其中一種方法是使用堆疊跟蹤。
現有的堆疊跟蹤程式碼可能並不總是能準確地描述所有具有活動狀態的函式,並且對於除錯有幫助的最佳實踐方法對於 livepatching 而言是不健全的。 Livepatching 依賴於架構來提供可靠的堆疊跟蹤,以確保它永遠不會從跟蹤中遺漏任何活動函式。
2. 要求¶
架構必須實現其中一個可靠的堆疊跟蹤函式。 使用 CONFIG_ARCH_STACKWALK 的架構必須實現 'arch_stack_walk_reliable',其他架構必須實現 'save_stack_trace_tsk_reliable'。
原則上,可靠的堆疊跟蹤函式必須確保以下任一項
跟蹤包括任務可能返回的所有函式,並且返回程式碼為零以指示跟蹤是可靠的。
返回程式碼為非零,以指示跟蹤不可靠。
注意
在某些情況下,從跟蹤中省略特定函式是合法的,但必須報告所有其他函式。 這些情況將在下面詳細描述。
其次,可靠的堆疊跟蹤函式必須能夠處理堆疊或其他展開狀態損壞或不可靠的情況。 該函式應嘗試檢測此類情況並返回非零錯誤程式碼,並且不應陷入無限迴圈或以不安全的方式訪問記憶體。 具體案例將在下面詳細描述。
3. 編譯時分析¶
為了確保可以在所有情況下正確展開核心程式碼,架構可能需要驗證程式碼是否以 unwinder 期望的方式進行編譯。 例如,unwinder 可能期望函式以有限的方式操作堆疊指標,或者所有函式都使用特定的 prologue 和 epilogue 序列。 具有此類要求的架構應使用 objtool 驗證核心編譯。
在某些情況下,unwinder 可能需要元資料才能正確展開。 如有必要,應使用 objtool 在構建時生成此元資料。
4. 注意事項¶
展開過程因架構、其各自的程式呼叫標準和核心配置而異。 本節介紹架構應考慮的常見詳細資訊。
4.1 識別成功終止¶
展開可能會因多種原因而提前終止,包括
堆疊或幀指標損壞。
缺少對不常見場景的展開支援,或者 unwinder 中存在錯誤。
動態生成的程式碼(例如 eBPF)或外部程式碼(例如 EFI 執行時服務)不遵循 unwinder 期望的約定。
為了確保這不會導致函式從跟蹤中省略,即使未被其他檢查捕獲,強烈建議架構驗證堆疊跟蹤是否在預期位置結束,例如
在作為核心入口點的特定函式內。
在核心入口點預期的堆疊上的特定位置。
在核心入口點預期的特定堆疊上(例如,如果該架構具有單獨的任務和 IRQ 堆疊)。
4.2 識別可展開程式碼¶
展開通常依賴於程式碼遵循特定的約定(例如,操作幀指標),但是可能存在不遵循這些約定並且可能需要在 unwinder 中進行特殊處理的程式碼,例如
異常向量和入口彙編。
程式連結表 (PLT) 條目和 veneer 函式。
Trampoline 彙編(例如 ftrace、kprobes)。
動態生成的程式碼(例如 eBPF、optprobe trampolines)。
外部程式碼(例如 EFI 執行時服務)。
為了確保此類情況不會導致函式從跟蹤中省略,強烈建議架構積極識別已知可以可靠展開的程式碼,並拒絕從所有其他程式碼展開。
可以使用 '__kernel_text_address()' 將包括模組和 eBPF 在內的核心程式碼與外部程式碼區分開來。 檢查此項也有助於檢測堆疊損壞。
架構可以透過多種方式識別被認為不可靠展開的核心程式碼,例如
將此類程式碼放入特殊的連結器節中,並拒絕從這些節中的任何程式碼展開。
使用邊界資訊識別程式碼的特定部分。
4.3 跨中斷和異常展開¶
在函式呼叫邊界處,堆疊和其他展開狀態預計處於適合可靠展開的一致狀態,但這可能不是函式執行過程中的情況。 例如,在函式 prologue 或 epilogue 期間,幀指標可能暫時無效,或者在函式體期間,返回地址可能儲存在任意通用暫存器中。 對於某些架構,這可能會由於動態工具而發生更改。
如果在堆疊或其他展開狀態處於不一致狀態時發生中斷或其他異常,則可能無法可靠地展開,並且可能無法識別此類展開是否可靠。 請參見下面的示例。
無法識別何時可以可靠地展開此類情況(或永遠不可靠)的架構必須拒絕跨越異常邊界的展開。 請注意,跨越某些異常(例如 IRQ)進行展開可能是可靠的,但跨越其他異常(例如 NMI)進行展開可能是不可靠的。
可以識別何時可以可靠地展開此類情況(或沒有此類情況)的架構應嘗試跨越異常邊界進行展開,因為這樣做可以防止不必要地停止 livepatch 一致性檢查,並允許 livepatch 轉換更快地完成。
4.4 重寫返回地址¶
一些 trampoline 會暫時修改函式的返回地址,以便在函式使用返回 trampoline 返回時進行攔截,例如
ftrace trampoline 可以修改返回地址,以便函式圖跟蹤可以攔截返回。
kprobes(或 optprobes)trampoline 可以修改返回地址,以便 kretprobes 可以攔截返回。
發生這種情況時,原始返回地址將不會位於其通常的位置。 對於不受即時修補影響的 trampoline,如果 unwinder 可以可靠地確定原始返回地址並且 trampoline 不會更改任何展開狀態,則 unwinder 可以報告原始返回地址來代替 trampoline,並將其報告為可靠的。 否則,unwinder 必須將這些情況報告為不可靠的。
在識別原始返回地址時需要特別小心,因為此資訊在入口 trampoline 或返回 trampoline 的持續時間內不在一致的位置。 例如,考慮 x86_64 'return_to_handler' 返回 trampoline
SYM_CODE_START(return_to_handler)
UNWIND_HINT_UNDEFINED
subq $24, %rsp
/* Save the return values */
movq %rax, (%rsp)
movq %rdx, 8(%rsp)
movq %rbp, %rdi
call ftrace_return_to_handler
movq %rax, %rdi
movq 8(%rsp), %rdx
movq (%rsp), %rax
addq $24, %rsp
JMP_NOSPEC rdi
SYM_CODE_END(return_to_handler)
當跟蹤的函式執行時,堆疊上的返回地址指向 return_to_handler 的開頭,原始返回地址儲存在任務的 cur_ret_stack 中。 在此期間,unwinder 可以使用 ftrace_graph_ret_addr() 找到返回地址。
當跟蹤的函式返回到 return_to_handler 時,堆疊上不再有返回地址,但原始返回地址仍然儲存在任務的 cur_ret_stack 中。 在 ftrace_return_to_handler() 中,原始返回地址將從 cur_ret_stack 中刪除,並由編譯器暫時任意移動,然後以 rax 返回。 return_to_handler trampoline 在跳轉到該地址之前將其移動到 rdi 中。
架構可能並不總是能夠展開此類序列,例如當 ftrace_return_to_handler() 已從 cur_ret_stack 中刪除地址時,並且無法可靠地確定返回地址的位置。
建議架構展開尚未返回到 return_to_handler 的情況,但架構不需要從 return_to_handler 的中間展開,並且可以將其報告為不可靠的。 架構不需要從其他修改返回地址的 trampoline 展開。
4.5 模糊返回地址¶
一些 trampoline 不會重寫返回地址來攔截返回,但會暫時覆蓋返回地址或其他展開狀態。
例如,x86_64 optprobes 的實現使用 JMP 指令修補被探測的函式,該指令以關聯的 optprobe trampoline 為目標。 當命中探針時,CPU 將分支到 optprobe trampoline,並且被探測函式的地址不會儲存在任何暫存器或堆疊上。
同樣,arm64 DYNAMIC_FTRACE_WITH_REGS 的實現使用以下內容修補跟蹤的函式
MOV X9, X30
BL <trampoline>
MOV 將連結暫存器 (X30) 儲存到 X9 中,以在 BL 覆蓋連結暫存器並分支到 trampoline 之前保留返回地址。 在 trampoline 的開頭,跟蹤的函式的地址位於 X9 中,而不是通常情況下的連結暫存器中。
架構必須確保 unwinder 要麼可靠地展開此類情況,要麼將展開報告為不可靠的。
4.6 連結暫存器的不可靠性¶
在其他一些架構上,“call”指令將返回地址放入連結暫存器中,“return”指令從連結暫存器中獲取返回地址,而不修改暫存器。 在這些架構上,軟體必須在進行函式呼叫之前將返回地址儲存到堆疊中。 在函式呼叫的持續時間內,返回地址可能單獨儲存在連結暫存器中、單獨儲存在堆疊中,或者同時儲存在這兩個位置中。
Unwinder 通常假設連結暫存器始終處於活動狀態,但此假設可能導致不可靠的堆疊跟蹤。 例如,考慮以下用於簡單函式的 arm64 彙編
function:
STP X29, X30, [SP, -16]!
MOV X29, SP
BL <other_function>
LDP X29, X30, [SP], #16
RET
進入該函式時,連結暫存器 (x30) 指向呼叫方,幀指標 (X29) 指向呼叫方的幀,包括呼叫方的返回地址。 前兩個指令建立一個新的堆疊幀並更新幀指標,此時連結暫存器和幀指標都描述了該函式的返回地址。 此時的跟蹤可能會描述該函式兩次,如果正在跟蹤函式返回,則 unwinder 可能會從 fgraph 返回堆疊中消耗兩個條目,而不是一個條目。
BL 呼叫 'other_function',連結暫存器指向該函式的 LDR,幀指標指向該函式的堆疊幀。 當 'other_function' 返回時,連結暫存器指向 BL,因此此時的跟蹤可能導致 'function' 在回溯中出現兩次。
同樣,函式可能會故意覆蓋 LR,例如
caller:
STP X29, X30, [SP, -16]!
MOV X29, SP
ADR LR, <callee>
BLR LR
LDP X29, X30, [SP], #16
RET
ADR 將 'callee' 的地址放入 LR 中,然後 BLR 分支到此地址。 如果在 ADR 之後立即進行跟蹤,則 'callee' 將顯示為 'caller' 的父級,而不是子級。
由於上述情況,可能只能在函式呼叫邊界處可靠地消耗連結暫存器值。 如果出現這種情況,除非架構可以可靠地識別何時應使用 LR 或堆疊值(例如,使用 objtool 生成的元資料),否則必須拒絕跨越異常邊界的展開。