西元二零二四年七月十九日,就是全球 Windows 電腦集體罷工的那一天,我獨在微信徘徊,遇見陳 KY 君,彈窗問我道,“老萬可曾為藍色畫面慘案寫了一點什麼沒有?”我說“沒有”。他就正告我,“老萬還是寫一點罷;這樣新鮮的大瓜,怎能不配合先生的風涼話食用?”
可是我實在無話可說。我只覺得所住的並非人間。幾十個行業的偏癱,瀰漫在我的周圍。一片哀鴻之中,殘局尚未收拾。微軟與 CrowdStrike 老闆高高在上的宣告,更使我艱於呼吸視聽,哪裡還能有什麼言語?
我已經出離 WTF 了。我將深度剖析這事件濃黑的教訓,為預防慘劇的重現奉獻菲薄的力量。
~~~~
小時候,我獨愛看電影。銀幕上有人晃就看,管它是啥情節。
看久了,我開始有了糾結:這編劇是不是有病?這樣煞筆的情節也指望觀眾相信?世上哪有那麼巧的事?
過了很多年,我發現自己又雙叒叕被打臉了。生活比神劇還會瞎編,讓人不敢相信的事一再發生,不斷重新整理我的認知閾值。
比如這個星期的 Windows 藍色畫面事件。

介紹一下故事的主角。
微軟,老牌霸主,軟體巨頭,總部位於美國華盛頓州。其 Windows 作業系統佔據了全球桌面作業系統 73% 的份額。

CrowdStrike,後起之秀,軟體安全公司,總部在美國德州,微軟長期合作伙伴,為 Windows 提供殺病毒、防駭客等系統安全服務。

這是一個感人肺腑的故事:
微軟和 CrowdStrike 精誠合作,雙向奔赴,完美避開了所有避坑操作,成功製造了人類史上空前的史詩級 IT 災難。
事情的嚴重性已經被廣泛報道,不需我重複。這裡僅概述一下:
全球八百五十萬臺 Windows 電腦中招癱瘓,完全無法使用。
三萬八千餘次航班延遲,四千餘航班取消。
法國民眾開啟電視,以為電視機壞了,結果是電視臺停播了。
英國和澳大利亞,收銀機歇菜,超市關門,飢腸轆轆的人們拿著信用卡買不到吃喝。
美國,至少十二家大醫院因電腦故障取消了對病人的治療,911 電話服務中斷。
中國,世界上人口最多的國家,一切照常 – 除了少數幾家外企。因為,在政府多年推行系統軟體國產化之後,CrowdStrike 在中國幾乎沒有市場。
事情發生後,到目前為止,微軟和 CrowdStrike 只發表了幾篇簡單宣告,對事故的經過語焉不詳,完全沒有提及責任認定和公司內部的整改方案。
~~~~
綜合多方資訊,老萬整理出故障發生的原因和經過:
-
全球很多 Windows 使用者,特別是企業使用者,都使用 CrowdStrike 提供的安全服務。
-
眾所周知,駭客攻擊和防禦是道高一尺魔高一丈的關係。所以,CrowdStrike 會頻繁(一天數次)更新其配置檔案,以對付最新的病毒和攻擊。
-
這些更新會自動推送到使用者的電腦上,使用者無法拒絕,除非自拔網線。
-
美東時間7月18日下午,CrowdStrike 又一次例行推送了系統更新,向超過 850 萬臺裝置傳送了新配置檔案,檔名為 C-00000291*.sys。注意這批檔案並非早前傳聞的系統驅動程式。據 CrowdStrike 確認,它們只是使用了和系統驅動程式同樣的副檔名 .sys,但只是資料檔案。
-
雖然這批新檔案不是系統驅動,但它們改變了 CrowdStrike 系統驅動程式的行為,觸發了其中的一個臭蟲(邏輯錯誤),造成系統崩潰。
-
從已知情況看,這一錯誤很可能是 CrowdStrike 系統驅動程式的編寫者忘記了在訪問物件前先檢查指標是否為空。
-
在 Windows 中,系統驅動程式具有最高的許可權,一旦崩潰將導致整個作業系統崩潰。於是,更新後的機器紛紛藍色畫面,也就是掛了。
-
事發後,CrowdStrike 緊急釋出了新的配置檔案取代有問題的檔案。然鵝,中招的系統已經無法工作,也就無法獲取 CrowdStrike 的新配置。
-
這種情況下,CrowdStrike 無法自動為使用者排除故障,必須仰仗使用者的手動操作。它建議中招裝置的系統管理員以安全模式重啟系統,找到並刪除肇事檔案,然後再手動重啟。這是一個冗長且易錯的過程,估計這幾天好多公司的 IT 管理員都在加班加點重啟機器。
要造成如此完美的災難實非易事,相當於猜對了十位數字密碼鎖的密碼。然而他們做到了!接下來我們就來分析一下奇蹟是如何發生的。
~~~~
首先講講大家最關心的問題:這個鍋到底該微軟背還是該 CrowdStrike 背?
我認為兩者都有份兒。直接捅簍子的當然是 CrowdStrike,但不可否認 Windows 的系統架構和流程起到了遞刀子的作用,如不思悔改,即便沒有 CrowdStrike,也會有其它公司捅類似甚至更大的簍子。。
先看 CrowdStrike 的貢獻。

錯誤使用空指標
事故的直接原因極可能是程式設計師錯誤使用了空指標。
我們來用一段
C++
程式做例子:
Widget* GetWidget() {
if (!HasWidget()) return nullptr;
...
}
...
Widget* w = GetWidget();
auto area = w->width * w->height;
有經驗的 C++ 程式設計師一眼就能發現其中的錯誤:要是 HasWidget() 條件不成立,GetWidget() 函式就會返回一個空指標,也就是說 w 變數不指向任何一個 Widget 物件。這時,試圖訪問 w 指向的物件就是一個無定義的行為(undefined behavior)。
無定義行為是程式設計師最大的噩夢,一切錯誤中最嚴重的錯誤。
因為,C++ 規定,當程式出現無定義行為時,它什麼都可以幹:崩潰算是輕的,它甚至可以刪除全部使用者資料,或者在正確資料中混入一些似是而非的資料導致南轅北轍的結論。
C++ 為什麼會有這麼奇葩的規矩?因為這樣可以讓編譯器實現更多的最佳化,讓程式碼在正常工作的時候效率更高。
C++ 有數不清的坑會導致無定義行為。我可以負責任地說:每個 C++ 程式設計師在職業生涯中都寫出過無定義行為的程式。只有被這樣的錯誤咬過幾次之後,他才會成長為有經驗的程式設計師。
因為 w 是空指標,讀取 w->width 變數就是一個無定義行為,也就是說程式這時幹啥都可能。
話雖這麼說,大多數 C++ 編譯器會讓程式此時從一個錯誤的記憶體地址讀取資料。在 Windows 中,這樣的操作會導致程式立即中斷。
如果犯錯的是普通應用程式,結果就是程式退出,但其它程式還可以執行。這還不算太糟。
不幸的是,在這周的藍色畫面事件裡,犯錯的是系統驅動程式。這種程式是最核心的系統軟體,擁有至高無上的許可權。Windows 一看系統驅動程式崩潰,馬上就嚇傻了 – 它也不知道系統壞到了什麼程度,要是繼續執行,怕是會造成更大的傷害(說不定系統已經被駭客控制了)。這時最保險的做法就是擺爛:顯示一個藍色畫面,然後罷工。
有經驗的 C++ 程式設計師都會處處小心,確保指標不是空的之後才會訪問它。比如這麼寫就安全了:
Widget* w = GetWidget();
if (w != nullptr) {
auto area = w->width * w->height;
...
}
看來給 CrowdStrike 寫系統驅動的不是有經驗的程式設計師。

程式碼審查走過場
在任何一家正規的軟體公司,程式碼的改動都不能隨便提交。只有在同事審查並認可之後,一個改動才能被合併進入程式碼庫。
程式碼審查的目的,在於多一雙眼睛檢查程式的邏輯、效率和可讀性。有經驗又負責的審查者可以發現各種隱患,把它們消除在萌芽之中。
顯然,這段錯誤程式碼的審查者沒有盡到自己的責任。

測試不給力
測試是攔截臭蟲的有力防線。越是重要的軟體,測試要越嚴肅認真。
為什麼這麼大的問題沒有被測試抓住?是測試案例不夠還是測試流程本身出了問題(比如這次更新會不會跳過了測試這一步)?這些問題值得 CrowdStrike 深思。

無視現代工具
有人說測試都是有一定機率的,測試案例不一定正好就能夠引發臭蟲,所以不能完全依賴於測試。
說得對。其實,有很多工具可以彌補測試的不足,就看你會不會用。
比如,測試覆蓋(test coverage)工具可以告訴我們哪些程式碼沒有被測試覆蓋,從而幫助我們構建新的測試案例去覆蓋更多重要程式碼。
許多
靜態分析(static analysis)
和
動態分析(dynamic analysis)
工具可以系統性地抓出程式碼中隱藏的臭蟲。這兩者的區別是:前者不需要執行被分析的程式,後者透過改寫(instrument)程式的機器程式碼,讓程式執行時捎帶執行檢測臭蟲的功能。這兩種分析各有所長,宜配合使用。
Clang C++ 編譯器的 sanitizer 模式是動態分析的優秀代表。如果使用得當,有很大的機率會抓住 CrowdStrike 的這個臭蟲。谷歌絕大部分的 C++ 程式碼在正式上線前都會經過 sanitizer 的驗證,相當於加了一層安全網。我加入 Pinterest 後,也馬上在公司的 C++ 開發流程中引入了 sanitizer。透過這一過程,我們發現並消除了不少安全隱患。

選擇錯誤的程式語言
毋庸置疑,C++ 功能強大,能讓人寫出貼近底層硬體特別高效的程式。
但同時廣為人知的是 C++ 暗藏了許多陷阱,非浸淫多年不足以正確使用。即便是近年來 C++ 引入了許多新的特性,安全性有了極大提升,坑還是太多了。比如,它的記憶體安全問題至今沒有解決。
所以,對於安全性至關重要的系統軟體,C++ 可能不是一個好的選擇。為什麼不考慮改用記憶體安全而且高效的 Rust 語言呢?
如果改用 Rust,就絕對不會出這種臭蟲。因為,Rust 強迫程式設計師在處理一個 Option 值(相當於 C++ 中的指標)時必須考慮其為空的情況,比如:
match find_item(id) {
Some(value) => println!("Item found: {}", value),
None => println!("Item not found"),
}
這裡 id 是一個 Option 變數,它既可能有值(Some(value)),也可能為空(None)。只有在它有值的情況下,程式才有機會讀取它的值。也就是說編譯器確保了不會出現讀取空指標指向的值這樣的程式錯誤。
當然 Rust 不是萬靈藥。它最為人詬病的地方是學習曲線比較陡峭,編譯器對程式碼非常挑剔,而且編譯速度很慢,不適合於快速迭代。
這些缺點是因為 Rust 對安全性的強調造成的。Rust 編譯器會試圖證明一段程式是安全的。如果證明失敗,編譯就無法透過。這就迫使程式設計師用安全效能被編譯器驗證的方式寫程式碼。Rust 初學者可能會花費很多時間才能找到正確的方式。而且,編譯器也需要花更多時間分析程式才能知道它是否安全。
但是,對於 CrowdStrike 這種重要的安全軟體,Rust 的這些缺點相對於它的好處不值一提。

頭疼醫頭式治療
雖然 CrowdStrike 已經修正了其配置檔案中的錯誤,但這隻能算是臨時措施。它還沒有從根子上解決問題。
配置檔案是 CrowdStrike 防毒系統的輸入資料。軟體設計有一條基本原則:任何時候系統不能因輸入資料的原因跑飛。也就是說,我們必須保證,無論遇到什麼樣的資料,程式的安全性不能出問題。
因為 CrowdStrike 的系統驅動程式還沒有被修正,這個臭蟲依然存在,只是冬眠了,說不定哪天又會被配置檔案的變化觸發。
要真正解決問題,CrowdStrike 需要在近期改掉驅動程式中的邏輯錯誤,並在遠期用可證明正確性的程式語言和工具、流程重新編寫它的驅動程式。
~~~~
接下來我們再來表彰微軟對此次事故的貢獻。

對系統驅動程式監管不力
很多人好奇微軟為什麼會給 CrowdStrike 這樣的第三方軟體如此高的許可權,“你辦事,我放心”。這不是把自己的軟肋拱手獻給旁人嗎?
平心而論,微軟意識到了這樣的風險,所以
會對
系統驅動程式的改動做冗長的審查之後再放行。
然而這次爆雷的改動不是系統驅動程式本身,而是它的配置檔案。微軟大意了,大概是覺得改資料比改程式碼安全。話說回來,
CrowdStrike 一日三次改配置的頻度也讓微軟沒時間仔細審查。
我想問的是:微軟在審查 CrowdStrike 系統驅動程式時為什麼沒有發現它有可能被配置檔案觸發無定義行為?顯然,微軟對驅動程式的審查不夠嚴格,要麼沒有要求作者提供其安全性的形式化證明,要麼沒有用形式化的方法去驗證這個證明。
也就是說,微軟的審查不是數學意義上的檢查,只是一種“我盡力了”的流程。這樣的審查當然比沒有要好,但遠遠不夠。

過於依賴第三方
計算機安全界的一個常識是要儘量減小被信賴的基礎部件(trusted computing base,TCB)。所謂 TCB,就是系統中最核心的、許可權最高的部件,比如作業系統核心和系統驅動程式。TCB 裡的程式碼越多,系統安全性就越難保證。
CrowdStrike 做為一個防病毒軟體,有多大必要有龐大、複雜的系統驅動程式?微軟對它的合作伙伴究竟能有多信任?
也許,微軟更妥當的做法是把防病毒軟體中需要特權的大部分功能拿過來自己開發,並嚴格控制其品質。同時,儘量壓縮 CrowdStrike 的系統驅動程式的體量,把絕大部分不需要特權的邏輯放到 TCB 之外,按使用者級別許可權執行。這樣,即便這部分程式碼跑飛了也不會炸掉整個系統。

不能自動回滾
作業系統在更新過程中,如果發現了問題,應該可以自動回滾(或者是在使用者確認後回滾)到上一個安全狀態。這是現代作業系統的基本修養。
然而,我們看到 Windows 藍色畫面後如無人工干預將一直保持不響應狀態。其實,既然 Windows 能在系統驅動程式崩潰後執行顯示藍色畫面的邏輯,它也可以檢視近期系統更新記錄,得出上次更新搞砸了的結論,然後啟動回滾。

一次推送給所有使用者
微軟的 Windows 系統自動更新的過程可以說是在裸奔。事關係統安全性的軟體更新,居然是大撒把式部署,一次性推送到全部使用者。
做過大規模軟體的都知道,為保險起見,部署一個更新的時候可以滾動式更新(rolling deployment):先更新一小部分機器,然後檢測它們的健康狀況,如果系統的重要指標沒有出現異常,再逐步完成其餘機器的更新。要是發現故障率超過預期,系統應該自動回滾,恢復到上一個已知安全的版本。這一切操作都應該自動進行,不需要人為處理。
微軟可能信奉的是人有多大膽地有多大產。薩總估計忙著跟 Sam 勾兌,趕 AI 的大潮,結果忽略了自己安身立命的作業系統安全,爆雷只是時間問題。

強行更新使用者系統
在這次事故中,850 多萬臺 Windows 電腦的使用者沒有選擇 – 在微軟的許可下,CrowdStrike 強行自動升級了他們的系統配置檔案,結果悲劇了。
儘管升級到最新的防病毒軟體可以增強系統安全性,但這也可能影響系統的穩定性,所以這是一個需要權衡輕重的選擇。對於不同的使用者,安全性和穩定性的相對重要程度是不一樣的。比如,愛冒險喜歡嚐鮮不怕 Z turn 的同學可以選擇第一時間更新,911 電話中心可能應該等新版本被一定數量使用者驗證後再更新。 微軟和 CrowdStrike 憑什麼替所有人做出一刀切的選擇?
是時候把選擇權交給使用者了。
~~~~
鑑於目前微軟和 CrowdStrike 尚未公開完整的事故報告,老萬的分析只是基於部分資訊,難免有錯漏之處。歡迎各位朋友在留言區指正。謝謝!
~~~~~~~~~~
猜你會喜歡:
-
谷歌對微軟:程式碼管理工具哪家強?– 要集中還是要分佈
-
Exchange Server 使用者新年發不出郵件怪誰 – 一隻微軟大臭蟲的屍檢報告
-
谷歌羅曼蒂克消亡史– 萬字長文剖析谷歌文化的演化
-
後 C++ 演義(第三回) – C++ 的最新發展
-
程式設計師的核心技能 – 以脫口秀的方式講解程式設計師最重要的技能
-
如何做出保鮮十年的軟體 – 老碼農冒死披露行業內幕系列
~~~~~~~~~~
關注老萬故事會公眾號:
本公眾號不開讚賞不放廣告。如果喜歡這篇文章,歡迎點贊、在看、轉發。謝謝大家
關鍵詞
程式
程式碼
問題
編譯器
程式設計師