安全和性能:24核卻動不了滑鼠之後續

上個月,著名的性能分析專家Bruce Dawson在自己的blog寫了一篇文章,說他的24核機器一開編譯就動不了滑鼠了:24-core CPU and I can』t move my mouse。後來也在知乎看到了相關的問題:如何看待「24核PC不能動滑鼠」這篇文章?。原文里的分析非常有學習價值,結論也很有趣。是程序退出的時候,清除GDI對象的時候有個加鎖解鎖的動作。在24核(超線程到48)的機器上,編譯器在退出的時候,所有進程都卡在那個鎖上了,47個idle,1個在工作。結果連滑鼠都動不了了。

這篇文章留下了幾個疑問,

  1. 為什麼那個時候需要加鎖?
  2. 編譯器是個無UI的進程,為什麼會卡在GDI對象清除上?
  3. 為什麼最近才出現這個問題,而以前的Windows上沒有?

後來,Bruce又出了一篇文章,What is Windows *doing* while hogging that lock,進一步分析了當時可能出的差錯。

隨著這些分析,問題逐漸明朗起來了。

在去年就有一個關於Windows 10 Anniversary Edition(v1607)里GDI安全性改進的分析。cvr-data.blogspot.com/2。基本意思是,在v1607里,GDI句柄管理為了安全性,做出的一些修改。

v1607以前

win32k.sys里的GDI句柄管理器有一個表,在建立每個GDI對象的時候,把一個結構體放入這個表:

struct GDICELL64n{n PVOID64 pKernelAddress; n USHORT wProcessId; n USHORT wCount; n USHORT wUpper; n USHORT wType; n PVOID64 pUserAddress; n};n

其中最有意思的就是pKernelAddress,它是那個GDI對象的內核地址。(別忘了,Windows的GDI也在內核里。)

這張表會映射到每個GDI進程去,在user mode里可以通過PEB.GdiSharedHandleTable來訪問。

安全性問題來了,你可以從進程里訪問到GDI句柄表,從而得到那些GDI對象的內核地址。從而發動攻擊。

在進程退出的時候,系統會遍歷那個進程的GDI對象,從GdiSharedHandleTable里把它們刪除

v1607之後

在v1607里,這個安全問題被修好了。解決的方法也比較簡單粗暴。不再把那張表映射到進程,也不提供pKernelAddress,而是個入口的ID。在kernel mode裡面維護一張全局的從user mode無法訪問的表。通過進程給的ID得到真實的內核地址。這樣就避免了泄露內核地址的問題。

然而,現在進程推出的工作就比以前大了。需要遍歷整張表,找到表裡面對應那個進程的GDI對象的ID,再進一步得到內核地址,進行刪除。而這個過程需要加鎖和解鎖,造成了進程間的序列化。

是這個修改惹的禍嗎?

這個修改,可以解釋問題3。也就是說這是v1607里新引進的問題,之前的Windows上沒有。也能解釋問題1,因為那張表訪問的時候需要加鎖,所有進程序列化訪問。

最疑惑的就是問題2。編譯器可以完全沒有UI,也完全沒有GDI對象,為什麼也會卡得那麼嚴重?

諷刺啊諷刺

其實,這個bug最諷刺的地方,莫過於即便一個進程完全沒有建立過GDI對象,也會卡在同一個地方。而原因卻簡單到不可思議。

進程里其實保存了自己擁有的GDI對象的數量。在退出的時候,其實只要看一下那個數量,0的話就別去加鎖/遍歷表/解鎖了。這麼簡單的early exit,就能在很大程度上緩解這個問題

推薦閱讀:

TAG:MicrosoftWindows | GDI | 信息安全 |