OpenAI公布了它如何運用「流行病學」的思路,查明ChatGPT背後資料基礎設施中一連串神秘當機的原因[1]。調查發現了兩個毫不相干、卻恰好同時浮現的漏洞,其中一個已經在一個被廣泛使用的開源函式庫中潛藏了18年[1]。

看似「不可能」的當機

OpenAI的模型與代理人在推論時需要檢索相關資料,因此仰賴可擴充的資料基礎設施[1]。其中部分服務以C++撰寫,以求將效能與記憶體效率最大化。但由於C++缺乏記憶體安全性,寫入錯誤的記憶體位址就可能引發當機[1]。

問題出在名為Rockset的服務上,它是ChatGPT資料基礎設施的核心之一[1]。Rockset是一套面向搜尋與即時分析的雲端原生資料系統,OpenAI已於2024年將其收購[2]。當ChatGPT檢索對話內容或工作區知識庫時,就會用到它[1]。

數個月前,團隊在Rockset內部觀察到一些奇怪的當機[1]。一個正常的C++函式看似執行完畢,卻試圖返回到一個無效位址,於是核心(作業系統的核心)便終止了程式[1]。有時函式保存的返回位址是NULL(空值),有時堆疊指標(%rsp,指向堆疊目前位置的CPU暫存器)偏移了8個位元組,這些都是一般應用程式不可能出現的損毀方式[1]。團隊能想到的每一個假設都有強而有力的反證,因此這個漏洞看起來「根本不可能存在」[1]。

從「醫師」到「流行病學家」的思路轉變

起初,團隊把這些當機當作常規的除錯問題:仔細檢視少量核心傾印(core dump,即當機瞬間記憶體狀態的快照檔案),提出假設並逐一排除[1]。但返回目標的搜尋範圍過於龐大,即便想從日誌中為當機分類也不可靠,因為記錄下來的呼叫堆疊本身就是損毀的[1]。

陷入僵局後,團隊大幅調整了思路:從像醫師那樣深入診斷單一病例,轉向像流行病學家那樣綜觀整個群體、尋找規律[1]。漏洞是從某個特定版本開始出現的嗎?它與某種硬體、某個地區或某個核心版本相關嗎?會不會是多個不同的群體被誤看成了同一種症狀?帶著這些問題,他們決定先蒐集高品質的資料[1]。

轉機出現在他們讓ChatGPT撰寫指令碼、自動分析過去一年全部正式環境核心傾印的時候[1]。指令碼會下載每個核心檔案的開頭部分,取出暫存器,濾除已知的誤報,再把當機自動標記為「返回NULL」「堆疊錯位」或「其他」[1]。

真相是兩個各自獨立的漏洞

有了乾淨的資料集之後,相關性立刻浮現[1]。原本以為是一個奇怪漏洞的現象,其實是兩個各自獨立的當機群體[1]。

「堆疊錯位」型集中在一個地區,有明確的開始時間,而且從不發生在長時間運行的節點上[1]。追查結果顯示,根源是一台硬體故障的實體機器,將該主機停用後當機便消失了[1]。換言之,這是一種「靜默的硬體毀損」,也就是CPU根本沒有正確完成運算[1]。

而「返回NULL」型則分散在多個叢集與地區[1]。在把硬體因素剝離出來之後,團隊發現剩下的當機全部發生在C++例外展開(exception unwinding,即拋出例外時將控制權轉交給對應清理處理與捕捉區塊的機制)過程中[1]。

在libunwind中沉睡18年的競爭條件

OpenAI的二進位檔連結了兩個實作例外展開的函式庫──GNU libunwind與libgcc,而實際被使用的是GNU libunwind[1]。

根源就在GNU libunwind的內部處理中[1]。它會在堆疊上建立一個保存目標暫存器狀態的結構,交給內部的 _Ux86_64_setcontext 處理;但這段處理在改寫堆疊指標%rsp之後,才從該結構中讀取指令指標(指向下一條要執行指令位置的值)[1]。一旦%rsp被改寫,那個結構就已脫離了「正在使用的堆疊」[1]。

此時若有訊號(打斷執行中程式的通知)到來,核心會在%rsp稍下方開闢處理區域,從而可能覆蓋尚未讀取的那個結構[1]。結果就是目標指令指標被毀損成了NULL[1]。這個競爭條件(race condition,即結果取決於時序的瑕疵)的危險窗口只有一條指令那麼寬──在現代CPU上大約只有100皮秒(1皮秒為1兆分之一秒)[1]。

這個漏洞自x86_64首個支援C++例外展開的版本起就已存在,18年多來一直無人察覺[1]。

為何直到現在才浮現

潛藏18年的漏洞如今才浮上檯面,是因為Rockset同時滿足了讓該問題容易出現的三個條件[1]:作為過載控制手段而高頻拋出例外;為精細量測CPU時間而頻繁發送名為SIGUSR2的訊號;以及今年以來該訊號處理所佔用的堆疊空間有所增加[1]。這三者的乘積終於越過了問題顯現的臨界值[1]。

作為因應,OpenAI把例外展開從GNU libunwind切換到了libgcc的實作[1],這在大規模環境下的鎖競爭方面也是更佳的選擇[1]。同時,他們把可重現的程式碼與修正方案回報(upstream)給了GNU libunwind本身[1]。此外,他們還改進了訊號處理,使得無需核心傾印、僅憑日誌即可偵測復發,並在維運層面做了讓故障節點更易被發現的調整[1]。

總結

OpenAI從這次調查中得到的最大教訓是:相較於精巧的組合語言分析或深厚的底層知識,「建立高品質的資料集」才是最重要的一步[1]。當兩種現象被混進同一個故事裡推論時始終無法解決的問題,一旦掌握了涵蓋整個群體的準確資料,其結構便清晰可見[1]。可靠性不僅在於修復漏洞,更在於建立能把無解問題轉化為可診斷問題的資料與流程──這對任何大規模系統的維運者而言,都是一個樸實卻富有啟發的案例。

來源:https://openai.com/index/core-dump-epidemiology-data-infrastructure-bug

來源:https://openai.com/index/openai-acquires-rockset/