咱先不談多線程競爭、空指標、野指標、數位溢出這些常見的坑,我們就拿很多人盯著的“邊界檢查”來說。
char *gets(char *s);為什麼 gets 函數從根本上就是不安全的?因為它讀取輸入時不知道提供的緩衝區有多大,只能一直寫入緩衝區,直到碰到換行符或 EOF。
如果駭客特意構造超長的輸入,則完全可以寫壞緩衝區,寫壞某些數據結構,覆蓋函數指標,從而控制你的程序行為。最終結果是什麼?許可權被駭客獲取,系統被入侵,你的隱私數據被公開。這叫緩衝區溢出漏洞。
C 語言是“可移植彙編”,你的記憶體操作不會帶有檢查。你 996 哈欠連天時一不小心寫錯邏輯,忘記證明瞭一處操作不會越界,測試也恰巧沒覆蓋到這種邊緣情況。漏洞穿越為數不多的幾層品質管理體系,留到了產品上線,後果可想而知。
2014 年的心臟滴血,OpenSSL 實現 TLS 的心跳擴展時沒有對輸入進行適當驗證,沒有邊界檢查,駭客可以越界讀取伺服器記憶體中的敏感資訊。福布斯網路安全專欄作家約瑟夫·斯坦伯格寫道:“有些人認為,至少就其潛在影響而言,‘心臟滴血’是自互聯網商用以來所發現的最嚴重的漏洞。”
2017 年的永恆之藍,美國 NSA 開發的網路攻擊武器,利用了 Windows SMB 協定中的緩衝區溢出漏洞,恰好就是算錯了數位大小導致越界寫入。該漏洞被駭客組織披露后,WannaCry 勒索病毒全球大爆發,至少 150 個國家、30 萬名使用者中招,造成損失達 80 億美元。
有一次我給 Redis 提交性能改進 PR 時,有一處漏寫了指標賦值,造成錯誤寫入。review 的大佬們沒看出來,原有的測試也沒覆蓋到。後來還是我親手發現和修復,補上了對應的測試。相信這種情況並不少見。
自 1970 年代發明以來,C 語言已有五十年的歷史,而現代程式設計語言幾乎都已配置預設的邊界檢查,為的就是減少此類漏洞損失,又能讓程式師更快更輕鬆地寫出業務邏輯,資本家也得到了更多的剩餘價值。
甚至連 C++ 都接受了標準庫加固,從 C++26 開始,啟用標準庫加固時將帶有預設的邊界檢查。谷歌的生產部署中報告,加固模式僅有 0.3% 的性能影響,卻幫助發現了上千個 bug。
如果不能接受邊界檢查,你可以選擇繞過,自行證明這裡不可能越界。如果預設邊界檢查,你會得到對緩衝區溢出漏洞的防禦,但代價是損失一點性能。這就是 Rust 採取的策略,編譯器甚至還能自動抹除不必要的邊界檢查。
除了作業系統、音視頻處理、密碼學、嵌入式等需要極高性能和資源受限的領域,你會發現 C 語言在其他領域已被淘汰,沒人寫個商城或者腳本還要用 C 語言吧。
這就是現代程式設計語言改進的成果,讓坑人的地方再少一點,讓自動化工具取代“程式設計仙人”,讓廣大程式師少掉頭髮。