C++ 解構器拋出例外時發生了什麼:堆疊展開與 std::terminate 的機制
isocpp.org / Sandor Dargo · 2026-05-15
C++ RAII 慣例讓解構器(destructor)承擔資源釋放的責任,但解構器拋出例外(throw)在語意上會引發一系列難以控制的連鎖效應,Sandor Dargo 在 2026 年 5 月詳細解析了這個問題的根源與正確處理方式。
背景
解構器的設計限制之一是沒有回傳值,因此在資源釋放失敗時,開發者能做的選擇有限:記錄錯誤、儲存錯誤狀態,或(不建議)拋出例外。C++ 標準自 C++11 起將所有解構器預設標記為 noexcept,這個決定背後有明確的技術理由。
核心改動:堆疊展開期間的雙重例外
問題的核心在於堆疊展開(stack unwinding)期間。當某個例外被拋出、程式開始向上尋找 catch handler 的過程中,所有經過的作用域的物件都會被解構。若此時某個解構器再次拋出例外,C++ 標準規定立即呼叫 std::terminate(),程式直接中止,原本的例外與新例外都無從處理。
std::terminate() 預設呼叫 std::abort(),不執行任何剩餘的清理動作,也不繼續展開堆疊。程式會以非正常方式結束,作業系統回收所有資源,但應用層面的清理(flush、commit、unlock 等)不會發生。
noexcept 標記的作用正是在此:若一個 noexcept 函式拋出例外,同樣觸發 std::terminate()。這讓編譯器能在不需要 unwind table 的情況下最佳化程式碼,也讓語意明確——函式宣告不拋出就是不拋出的承諾。
規則與實務建議
C++ Core Guidelines 的規則 C.36 明確指出:解構器不得拋出例外。若資源釋放的確可能失敗,有幾個替代方式:
- 提供顯式的
close()/flush()方法,讓呼叫端在例外傳播前手動處理失敗情況 - 在解構器內部 catch 所有例外(
catch (...) {}),搭配記錄或設定錯誤旗標 - 使用
std::uncaught_exceptions()(C++17)判斷目前是否處於展開狀態,決定是否要吞掉例外
std::uncaught_exceptions()(注意是複數形)回傳目前尚未被 catch 的例外數量,比 C++03 的 std::uncaught_exception()(單數)更精確,可以正確處理嵌套例外情境,如 std::scope_exit 等 utility 內部的判斷。
影響範圍
這個議題在使用 RAII 封裝資料庫連線、檔案 IO、網路 socket 的程式碼中最為常見。若解構器在析構 DB transaction 物件時嘗試 rollback 並失敗,在展開路徑上觸發 std::terminate() 會讓原始例外的診斷資訊完全消失。C++23 的 std::expected 提供了另一個方向:用回傳值傳遞錯誤,完全避開例外機制。
原始來源:isocpp.org
BPF 能否接管 Linux 記憶體管理?Roman Gushchin 的提案與障礙
LWN.net · 2026-05-15
Meta 核心工程師 Roman Gushchin 在 2026 年 5 月的 Linux Plumbers 相關討論中,探索了以 BPF 程式動態調整記憶體管理行為的可行性,並坦承目前的技術與政治障礙仍相當高。
原本的問題
Linux 核心的記憶體管理(MM)子系統設計上是「一體適用」的通用策略,但不同工作負載的最佳 MM 行為差異極大:資料庫希望控制 page cache 的積極程度,即時系統需要最小化分配延遲,容器環境則要在 cgroup 邊界之間精確分配記憶體壓力。目前的調整途徑——sysctl、cgroup 參數——缺乏足夠的細粒度,且無法根據執行時狀態動態切換策略。
採用的方法
Gushchin 提議在 MM 的關鍵路徑上插入 BPF hook,讓使用者空間的策略程式能夠影響:
- Page reclaim 的候選選擇邏輯(哪些 page 優先被回收)
- 記憶體壓力通知的觸發閾值
- OOM killer 的目標選擇
- Huge page 的分配決策
BPF 的核心優勢在於程式碼可在不重新編譯核心的情況下動態載入,且 verifier 確保程式不會破壞核心不變量(invariants)。Meta 內部已在 scheduler(sched_ext)與 TCP congestion control 使用類似模式,並取得正面成效。
障礙
然而 MM 子系統的 BPF 化面臨幾個特殊困難。安全性方面,BPF verifier 目前難以確保記憶體管理 hook 不會引發 deadlock——MM 程式碼路徑本身就可能觸發分配,形成遞迴呼叫風險。API 穩定性方面,核心開發者對把 MM 內部資料結構暴露給 BPF 程式存有疑慮,因為這等於承諾維護這些結構的穩定性。社群接受度方面,MM 維護者歷來對高侵入性的修改持保守態度,sched_ext 花了多年才合併進主線。Gushchin 的提案目前尚無對應的核心修補集,仍處於概念探索階段。
原始來源:LWN.net
七個穩定版核心修補 CVE-2026-46333:ptrace 競態讓未授權者竊取 SSH 私鑰
LWN.net · 2026-05-15
2026 年 5 月 15 日,Greg Kroah-Hartman 同時發佈七個 Linux 穩定版核心,統一修補 CVE-2026-46333——一個由 Qualys 發現、已有公開 PoC 的 ptrace 競態條件漏洞,能讓同 UID 的非特權用戶在一秒內竊取 SSH 主機私鑰。
漏洞機制
漏洞利用串連三個核心行為。第一,__ptrace_may_access() 在目標進程的 task->mm == NULL(無記憶體管理結構)時跳過 dumpable 檢查。第二,進程終止時 do_exit() 的呼叫順序是先 exit_mm()、後 exit_files(),造成一個短暫視窗:進程已無 mm 但仍持有開啟的檔案描述符。第三,在此視窗內,pidfd_getfd(2) 只要呼叫者的 UID 與目標相符即可成功,達成檔案描述符竊取(FD theft)。
ssh-keysign 二進位在 2002 年的設計缺陷使其成為理想目標:它在呼叫 permanently_set_uid() 降權之前就已開啟 /etc/ssh/ssh_host_*_key(mode 0600),並在 EnableSSHKeysign=no 時帶著這些 FD 結束。chage 同理可讀取 /etc/shadow。典型的利用成功率約 100–2000 次嘗試內必中。
受影響版本
- 7.0.x < 7.0.8
- 6.18.x < 6.18.31
- 6.12.x < 6.12.89
- 6.6.x < 6.6.139
- 6.1.x < 6.1.173
- 5.15.x < 5.15.207
- 5.10.x < 5.10.256
確認受影響的發行版包括 Raspberry Pi OS Bookworm、Debian 13、Ubuntu 22.04/24.04/26.04、Arch、CentOS 9。
修補與緩解
修補提交(commit 31e62c2ebbfd)修正了 __ptrace_may_access() 的條件判斷,確保 task->mm == NULL 情況下不跳過 dumpable 檢查。若暫時無法升級,可透過調整 ptrace scope(/proc/sys/kernel/yama/ptrace_scope = 1)緩解,但這會影響正常的除錯工作流。Jann Horn 早在 2020 年便提出過類似修補方向,此次 Qualys 的完整 PoC 促使修補正式合併。
原始來源:LWN.net