Livepatch

本文件概述了有關核心 Livepatching 的基本資訊。

1. 動機

在許多情況下,使用者不願意重新啟動系統。 這可能是因為他們的系統正在執行復雜的科學計算,或者在高峰使用期間承受著巨大的負載。除了保持系統正常執行之外,使用者還希望擁有一個穩定且安全的系統。Livepatching 透過允許重定向函式呼叫來為使用者提供這兩者;因此,無需重新啟動系統即可修復關鍵函式。

2. Kprobes, Ftrace, Livepatching

Linux核心中有多種機制與程式碼執行的重定向直接相關;即:核心探針、函式跟蹤和 livepatching。

  • 核心探針是最通用的。可以透過放置一個斷點指令而不是任何指令來重定向程式碼。

  • 函式跟蹤器從預定義的位置呼叫程式碼,該位置靠近函式入口點。此位置由編譯器使用'-pg' gcc選項生成。

  • Livepatching 通常需要在函式引數或堆疊以任何方式修改之前,在函式入口的開頭重定向程式碼。

所有三種方法都需要在執行時修改現有程式碼。因此,它們需要相互瞭解,並且不能互相踩踏。 大部分問題都透過使用動態ftrace框架作為基礎來解決。當探測函式入口時,Kprobe被註冊為ftrace處理程式,請參見CONFIG_KPROBES_ON_FTRACE。Live Patch的替代函式也透過自定義ftrace處理程式來呼叫。但是存在一些限制,請參見下文。

3. 一致性模型

函式存在是有原因的。它們接受一些輸入引數,獲取或釋放鎖,以定義的方式讀取、處理甚至寫入一些資料,並具有返回值。換句話說,每個函式都有一個已定義的語義。

許多修復程式不會更改修改後的函式的語義。例如,它們新增空指標或邊界檢查,透過新增缺少的記憶體屏障來修復競爭,或者在關鍵部分周圍新增一些鎖定。這些更改大多是獨立的,並且該函式以相同的方式呈現給系統的其餘部分。在這種情況下,可以獨立地逐個更新這些函式。

但是還有更復雜的修復程式。例如,一個補丁可能同時更改多個函式中鎖定的順序。或者一個補丁可能交換某些臨時結構的含義並更新所有相關的函式。在這種情況下,受影響的單元(執行緒,整個核心)需要同時開始使用所有新版本的函式。此外,只有在安全的情況下才能進行切換,例如,當受影響的鎖被釋放或此時沒有資料儲存在修改後的結構中。

關於如何以安全的方式應用函式的理論相當複雜。目的是定義一個所謂的一致性模型。它試圖定義可以使用新實現以使系統保持一致的條件。

Livepatch具有一致性模型,該模型是kGraft和kpatch的混合:它使用kGraft的基於任務的一致性和系統呼叫屏障切換,以及kpatch的堆疊跟蹤切換。 還有許多備選選項,使其非常靈活。

補丁程式在每個任務的基礎上應用,當認為任務可以安全地切換時。啟用補丁程式後,livepatch進入過渡狀態,任務會收斂到已修補的狀態。通常,此過渡狀態可以在幾秒鐘內完成。停用補丁程式時也會發生相同的序列,只不過任務從已修補的狀態收斂到未修補的狀態。

中斷處理程式繼承其中斷的任務的已修補狀態。對於派生的任務也是如此:子任務繼承父任務的已修補狀態。

Livepatch使用幾種互補的方法來確定何時可以安全地修補任務

  1. 第一種也是最有效的方法是檢查睡眠任務的堆疊。 如果給定任務的堆疊上沒有受影響的函式,則修補該任務。 在大多數情況下,這將首次修補大多數或所有任務。 否則,它會定期嘗試。僅當該架構具有可靠的堆疊時,此選項才可用(HAVE_RELIABLE_STACKTRACE)。

  2. 如果需要,第二種方法是核心退出切換。 從系統呼叫、使用者空間IRQ或訊號返回到使用者空間時,會切換任務。 在以下情況下,它很有用

    1. 修補在受影響的函式上休眠的 I/O 繫結使用者任務。 在這種情況下,您必須傳送 SIGSTOP 和 SIGCONT 強制其退出核心並進行修補。

    2. 修補 CPU 繫結的使用者任務。 如果任務是高度 CPU 繫結的,那麼下次被 IRQ 中斷時,它將被修補。

  3. 對於空閒的“swapper”任務,由於它們永遠不會退出核心,因此它們在空閒迴圈中有一個 klp_update_patch_state() 呼叫,這允許它們在 CPU 進入空閒狀態之前被修補。

    (請注意,kthread 還沒有這樣的方法。)

沒有 HAVE_RELIABLE_STACKTRACE 的架構完全依賴於第二種方法。 很可能有些任務仍在使用舊版本的函式執行,直到該函式返回。 在這種情況下,您必須向任務傳送訊號。 這尤其適用於 kthread。 它們可能沒有被喚醒,需要強制喚醒。 有關更多資訊,請參見下文。

除非我們可以提出另一種修補 kthread 的方法,否則核心 livepatching 不認為沒有 HAVE_RELIABLE_STACKTRACE 的架構得到完全支援。

/sys/kernel/livepatch/<patch>/transition 檔案顯示補丁程式是否處於過渡狀態。 一次只能有一個補丁程式處於過渡狀態。 如果任何任務停留在初始補丁程式狀態,則補丁程式可能會無限期地保持在過渡狀態。

可以透過在過渡過程中將相反的值寫入 /sys/kernel/livepatch/<patch>/enabled 檔案來反轉過渡並有效地取消過渡。 然後,所有任務將嘗試收斂回原始補丁程式狀態。

還有一個 /proc/<pid>/patch_state 檔案,可用於確定哪些任務正在阻止修補操作完成。 如果補丁程式處於過渡狀態,則此檔案顯示 0 表示任務未修補,顯示 1 表示任務已修補。 否則,如果沒有補丁程式處於過渡狀態,則顯示 -1。 可以使用 SIGSTOP 和 SIGCONT 向任何阻止過渡的任務傳送訊號,以強制它們更改其已修補狀態。 但是,這可能對系統有害。 向所有剩餘的阻止任務傳送偽訊號是一種更好的選擇。 實際上沒有傳遞任何合適的訊號(訊號掛起結構中沒有資料)。 任務被中斷或喚醒,並強制更改其已修補狀態。 偽訊號每 15 秒自動傳送一次。

管理員還可以透過 /sys/kernel/livepatch/<patch>/force 屬性影響過渡。 在那裡寫入 1 會清除所有任務的 TIF_PATCH_PENDING 標誌,從而強制任務進入已修補狀態。 重要說明! force 屬性旨在用於過渡因阻塞任務而長時間卡住的情況。 希望管理員收集所有必要的資料(即此類阻塞任務的堆疊跟蹤),並向補丁程式分發者請求清除以強制過渡。 未經授權的使用可能會對系統造成損害。 這取決於補丁程式的性質、哪些函式被(取消)修補以及阻塞任務在哪些函式中休眠(/proc/<pid>/stack 可能會有所幫助)。 移除(rmmod)補丁程式模組在 force 功能使用時被永久停用。 不能保證沒有任務在此類模組中休眠。 如果在迴圈中停用和啟用補丁程式模組,則意味著無限制的引用計數。

此外,使用 force 還可能影響未來 live patch 的應用,並對系統造成更大的損害。 管理員應首先考慮簡單地取消過渡(參見上文)。 如果使用了 force,則應計劃重新啟動,並且不再應用 live patch。

3.1 向新架構新增一致性模型支援

要向新架構新增一致性模型支援,有幾個選項

  1. 新增 CONFIG_HAVE_RELIABLE_STACKTRACE。 這意味著移植 objtool,並且對於非 DWARF 解卷器,還要確保堆疊跟蹤程式碼有一種方法來檢測堆疊上的中斷。

  2. 或者,確保每個 kthread 在安全位置呼叫 klp_update_patch_state()。 Kthread 通常處於無限迴圈中,會重複執行某些操作。 切換 kthread 補丁程式狀態的安全位置是在迴圈中的指定點,在該點沒有獲取任何鎖,並且所有資料結構都處於定義明確的狀態。

    當使用 workqueue 或 kthread worker API 時,該位置很明確。 這些 kthread 在通用迴圈中處理獨立的操作。

    對於具有自定義迴圈的 kthread 來說,這要複雜得多。 在這種情況下,必須根據具體情況仔細選擇安全位置。

    在這種情況下,沒有 HAVE_RELIABLE_STACKTRACE 的架構仍然能夠使用一致性模型的非堆疊檢查部分

    1. 當用戶任務跨越核心/使用者空間邊界時修補使用者任務;並且

    2. 在其指定的補丁程式點修補 kthread 和空閒任務。

    此選項不如選項 1,因為它需要向用戶任務傳送訊號並喚醒 kthread 才能修補它們。 但是,對於那些還沒有可靠堆疊跟蹤的架構來說,它仍然可能是一個不錯的備用選項。

4. Livepatch模組

Livepatch使用核心模組分發,請參見samples/livepatch/livepatch-sample.c。

該模組包括我們要替換的函式的新實現。此外,它定義了一些結構,描述了原始實現和新實現之間的關係。然後,當載入livepatch模組時,有一些程式碼會使核心開始使用新程式碼。此外,還有一些程式碼可以在刪除livepatch模組之前進行清理。所有這些將在接下來的部分中進行更詳細的說明。

4.1. 新函式

函式的新版本通常只是從原始原始碼複製。一種好的做法是在名稱中新增字首,以便可以將它們與原始名稱區分開,例如在回溯中。 它們也可以宣告為靜態,因為它們不會被直接呼叫,並且不需要全域性可見性。

該補丁程式僅包含真正修改的函式。但是,它們可能希望訪問原始原始檔中的函式或資料,這些函式或資料可能只能在本地訪問。這可以透過生成的livepatch模組中的特殊重定位段來解決,有關更多詳細資訊,請參見 Livepatch模組ELF格式

4.2. 元資料

該補丁由幾個結構描述,這些結構將資訊分為三個級別

  • struct klp_func 是為每個已修補的函式定義的。 它描述了特定函式的原始實現和新實現之間的關係。

    該結構包含原始函式的名稱(作為字串)。 函式地址在執行時透過 kallsyms 查詢。

    然後它包括新函式的地址。它是透過分配函式指標直接定義的。 請注意,新函式通常在同一原始檔中定義。

    作為可選引數,kallsyms資料庫中的符號位置可用於消除相同名稱的函式的歧義。 這不是資料庫中的絕對位置,而只是僅針對特定物件(vmlinux 或核心模組)找到它的順序。 請注意,kallsyms 允許根據物件名稱搜尋符號。

  • struct klp_object 定義了同一物件中已修補函式 (struct klp_func) 的陣列。 其中,物件是 vmlinux (NULL) 或模組名稱。

    該結構有助於將每個物件的功能分組並一起處理。 請注意,修補的模組可能會比補丁本身晚載入,並且只有在相關函式可用時才能對其進行修補。

  • struct klp_patch 定義了已修補物件 (struct klp_object) 的陣列。

    此結構始終如一且最終同步地處理所有已修補的函式。 僅當找到所有已修補的符號時,才應用整個補丁。 唯一的例外是尚未載入的物件的符號(核心模組)。

    有關如何在每個任務的基礎上應用補丁的更多詳細資訊,請參見“一致性模型”部分。

5. Livepatch生命週期

Livepatching 可以透過五個基本操作來描述:載入、啟用、替換、停用、移除。

其中,替換和停用操作是互斥的。 它們對給定的補丁程式具有相同的結果,但對系統則沒有。

5.1. 載入

唯一合理的方法是在載入 livepatch 核心模組時啟用補丁程式。為此,必須在 module_init() 回撥中呼叫 klp_enable_patch()。有兩個主要原因

首先,只有該模組才能輕鬆訪問相關的 struct klp_patch

其次,當無法啟用補丁程式時,可以使用錯誤程式碼來拒絕載入該模組。

5.2. 啟用

透過從 module_init() 回撥呼叫 klp_enable_patch() 來啟用 livepatch。 在此階段,系統將開始使用已修補函式的新實現。

首先,根據已修補函式的名稱找到其地址。“新函式”部分中提到的特殊重定位將被應用。 將在 /sys/kernel/livepatch/<name> 下建立相關的條目。 當以上任何操作失敗時,將拒絕該補丁程式。

其次,livepatch 進入過渡狀態,任務會收斂到已修補的狀態。 如果首次修補原始函式,則會建立一個特定於函式的 struct klp_ops,並註冊一個通用 ftrace 處理程式[1]。 此階段由 /sys/kernel/livepatch/<name>/transition 中的值“1”指示。 有關此過程的更多資訊,請參見“一致性模型”部分。

最後,一旦所有任務都已修補,“transition”值將更改為“0”。

5.3. 替換

所有已啟用的補丁程式都可以被設定了 .replace 標誌的累積補丁程式替換。

一旦啟用了新補丁程式並且“transition”完成,則所有與已替換補丁程式關聯的函式 (struct klp_func) 都將從相應的 struct klp_ops 中刪除。 此外,當相關函式未被新補丁程式修改且 func_stack 列表為空時,ftrace 處理程式將被登出,並且 struct klp_ops 將被釋放。

有關更多詳細資訊,請參見 原子替換 & 累積補丁

5.4. 停用

可以透過將“0”寫入 /sys/kernel/livepatch/<name>/enabled 來停用已啟用的補丁程式。

首先,livepatch 進入過渡狀態,任務會收斂到未修補的狀態。 系統開始使用先前啟用的補丁程式中的程式碼,甚至使用原始程式碼。 此階段由 /sys/kernel/livepatch/<name>/transition 中的值“1”指示。 有關此過程的更多資訊,請參見“一致性模型”部分。

其次,一旦所有任務都已取消修補,“transition”值將更改為“0”。 與要停用的補丁程式關聯的所有函式 (struct klp_func) 都將從相應的 struct klp_ops 中刪除。 當 func_stack 列表為空時,ftrace 處理程式將被登出,並且 struct klp_ops 將被釋放。

第三,sysfs 介面被銷燬。

5.5. 移除

僅當沒有模組提供的函式的使用者時,模組移除才是安全的。 這就是 force 功能永久停用移除的原因。 只有當系統成功過渡到新的補丁程式狀態(已修補/未修補)而沒有被強制時,才能保證沒有任務在舊程式碼中休眠或執行。

6. Sysfs

有關已註冊補丁程式的資訊可以在 /sys/kernel/livepatch 下找到。 可以透過在那裡寫入來啟用和停用補丁程式。

/sys/kernel/livepatch/<patch>/force 屬性允許管理員影響修補操作。

有關更多詳細資訊,請參見 ABI 檔案測試/sysfs-kernel-livepatch

7. 侷限性

當前的 Livepatch 實現有幾個限制

  • 只能修補可以跟蹤的函式。

    Livepatch 基於動態 ftrace。 特別是,實現 ftrace 或 livepatch ftrace 處理程式的函式無法修補。 否則,程式碼最終會進入無限迴圈。 透過用“notrace”標記有問題的功能,可以防止潛在的錯誤。

  • 僅當動態 ftrace 位於函式的開頭時,Livepatch 才能可靠地工作。

    需要在堆疊或函式引數以任何方式修改之前重定向該函式。 例如,livepatch 需要在 x86_64 上使用 -fentry gcc 編譯器選項。

    一個例外是 PPC 埠。 它使用相對定址和 TOC。 每個函式都必須處理 TOC 並在呼叫 ftrace 處理程式之前儲存 LR。 此操作必須在返回時恢復。 幸運的是,通用 ftrace 程式碼也存在同樣的問題,所有這些都在 ftrace 級別處理。

  • 使用 ftrace 框架的 Kretprobe 與已修補的函式衝突。

    kretprobe 和 livepatch 都使用修改返回地址的 ftrace 處理程式。 第一個使用者獲勝。 當處理程式已被另一個使用者使用時,將拒絕探針或補丁程式。

  • 當代碼重定向到新實現時,原始函式中的 Kprobe 將被忽略。

    目前正在努力新增有關此情況的警告。