dotnet 6 在 Win7 系統證書鏈錯誤導致 HttpWebRequest 內存泄露

本文記錄我將應用遷移到 dotnet 6 之後,在 Win7 系統上,因爲使用 HttpWebRequest 訪問一個本地服務,此本地服務開啓 https 且證書鏈在此 Win7 系統上錯誤,導致應用內存泄露問題。本文記錄此問題的原因以及調查過程

核心原因

核心原因是在 CRYPT32.dll 上的 CertGetCertificateChain 方法存在內存泄露,更底層的原因未知

在 .NET 6 裏,更新了 https 訪問方法邏輯,詳細請看 Announcing .NET 6 - The Fastest .NET Yet - .NET BlogWhat's new in .NET 6 Microsoft Docs

核心問題是調用進入 ChainPal.BuildChain 時,將會調用 Crypt32.CertGetCertificateChain 方法的調用邏輯有所變更,此進入邏輯和 .NET Framework 4.5 有所不同。準確來說,此差異不是 .NET 6 與 .NET Framework 4.5 的差異,而是 .NET Framework 4.6 以及更高版本與 .NET Framework 4.5 的差異

在 .NET Framework 4.6 時引入 Switch.System.Net.DontEnableSchUseStrongCrypto 變更是導致此問題的關鍵,在 .NET Framework 4.5 下,默認是 true 的值,但是在 .NET Framework 4.6 和更高版本下都是 false 的值。這就導致了整體邏輯的行爲差異。此邏輯差異只和 SDK 相關,而和用戶端所安裝的運行時無關

但是此差異是否一定導致內存泄露,這是未知的。但內存泄露必定走了此調用邏輯

解決方法

如 SDK 提示,使用 WebRequest.Create 等方法創建 HttpWebRequest 用來進行網絡請求邏輯是一個過時的方法,應該換用 HttpClient 等代替。經過實際的測試,換用 HttpClient 即可完美解決內存泄露問題,順帶提升了不少的性能

也就是說此內存泄露從業務上說是使用了一個過時的 API 導致的問題

調查過程

在開始記錄調查過程之前,還請看一下背景

如上一篇博客 記將一個大型客戶端應用項目遷移到 dotnet 6 的經驗和決策 - lindexi - 博客園 我在完成了遷移了此大型應用到 dotnet 6 發佈到內測用戶端,有內測小白鼠反饋說第二天過來就看到應用掛掉了

一開始沒有認爲這是一個問題。等到第二個用戶反饋時纔開始認爲這是一個坑,開始進行調查

以下調試過程非新手友好,請新手一定不要閱讀下文,如果閱讀了也一定不要在調試內存泄露使用下面的方法

通過分析應用本身的日誌,瞭解到應用是被閃退的。詢問內測的用戶瞭解到,應用閃退的時候,都是在晚上掛機的時候,這時候沒有任何的用戶動作。爲了儘可能幹掉環境問題帶來的干擾,我搭建了虛擬機,使用 cn_windows_7_ultimate_with_sp1_x64_dvd_u_677408.iso 安裝了純淨的系統,再加上 KB2533623 補丁讓 dotnet 6 應用跑起來,最後部署上應用,進行掛機

十分符合預期的,第二天應用掛掉了,而且系統提示 Xx 應用停止工作。通過 系統日誌 可以看到存在應用錯誤異常,異常信息是 CLR Exception E0434352 也就是在 CLR 層面出現異常

我錯誤認爲這是升級到 dotnet 6 時,由於 dotnet 6 和 Win7 的兼容性導致的問題,開始着手根據 CLR Exception E0434352 Microsoft Docs 官方文檔的方法開始調查,然而卻沒有找到任何有用的信息

繼續掛機到第三天,我這次採用任務管理器在 Xx 應用停止工作時,對應用抓一個 DUMP 傳到我開發設備上,使用 VisualStudio 的混合調試進行調試,此時發現錯誤信息和第二天的不相同了,這次顯示的是 OutOfMemory 相關異常。但是我在 Win7 虛擬機上,使用任務管理器看到的 Xx 應用佔用的內存實際上才 250 MB 而已,這一定是在諷刺我

好在我反應過來,任務管理器上面看到的應用佔用 250MB 內存,完全不等於應用使用的內存是 250MB 的空間。爲什麼呢?這是一個複雜的問題,我不想在本文這裏聊 Windows 下的應用內存知識,也許後續會另外開一篇很長的博客來說明。需要了解的是,如果一個應用 OOM 了,那除了系統本身給不到應用足夠的內存之外,還有另一個問題就是應用本身用到了平臺限制的最大內存數量。別忘了 x86 和 x64 的差異

剛好,此 Xx 應用是一個 x86 應用。在通過系統日誌瞭解到此 Win7 虛擬機上沒有存在一刻是內存不足的情況,而且此純淨的虛擬機也就跑了 Xx 一個應用,要是內存不足,也是 Xx 應用的鍋。回憶一下,使用 x86 應用,默認的進程空間是 4G 大小,其中有 1 到 2G 需要給系統交稅,也就是應用在開啓大內存感知時,最大能用到 3G 的內存。如果應用在到達 3G 內存佔用附近時,依然向系統申請內存,那此時就 OOM 了

任務管理器說應用佔用了多少內存,實際上如果是以上的申請內存超過 x86 平臺限制的導致的問題,那完全必須無視任務管理器說的話。特別是在用戶端,別忘了還有 EmptyWorkingSet 這樣安慰人的方法

我通過拿到 DUMP 文件的大小,看到 DUMP 文件是接近 4G 的大小,猜測是 Xx 應用申請內存超過 x86 平臺限制。調查此問題需要用到微軟極品工具箱的 VMMap 工具

通過 vmmap 可以看到此時的應用的 Private Data 佔用達到接近 3G 的大小,因此可以定位到 Xx 應用閃退的原因是因爲申請內存超過 x86 平臺限制

也就是說有兩個分支導致 Private Data 佔用過多,第一個原因就是業務需要申請大量的內存空間,第一個原因不算是內存泄露問題,只能算是性能優化問題,某個業務邏輯空間複雜度過高。第二個原因就是應用內存泄露,應用不斷運行過程中,不斷泄露內存,運行的時間長了,自然多少內存都不夠用

換句話說,不是所有的 OOM 問題,都是內存泄露問題,可能還是業務需要申請大量的內存空間問題。但顯然,本次遇到的問題,應該就是內存泄露問題了。畢竟只是掛機就讓應用掛掉了,那大概確定是內存泄露了。但是這隻能說大概,萬一有一個定時任務是從後臺拉取某個數據,剛好這個數據導致了某個處理業務需要申請大量的內存,從而讓應用掛掉。爲了確定是哪個方式導致的 OOM 了,可以先使用排除的方式,如果是某個業務申請大量的內存導致內存泄露,這是非常好也非常方便調試出來的,只需要使用 dotMemory 工具分析一下即可

在開始使用 dotMemory 之前,還遇到一個小問題,那就是 dotMemory 不能在我的 Win7 虛擬機上運行,而我又不想去污染此虛擬機環境。好在 dotMemory 可以分析 DUMP 文件,於是我就拿來剛纔使用 任務管理器 抓的 DUMP 文件進行分析。可惜,由於 Win7 虛擬機採用的是 X64 系統,而應用是 X86 應用,導致任務管理器抓的 DUMP 文件無法被 dotMemory 識別,只能再次換用專業 ProcDump 工具去抓進程的 DUMP 文件

換用 ProcDump 工具去抓應用的 DUMP 文件用起來比任務管理器更加方便,我也推薦使用 ProcDump 去抓 DUMP 文件,這個工具是十分強大的,本文用到的只是很少的功能。由於這個工具太強大了,要介紹的話,也是另一篇博客了,本文也不會包含此工具的更多使用方法

在虛擬機上面使用 procdump -ma <PID> 命令,這裏的 <PID> 就是要抓取的進程的 Id 號,將 Xx 應用抓取 DUMP 文件,然後再用 7z 壓縮一下,傳回到我的開發設備上,用 dotMemory 打開分析。使用 7z 是因爲可以很大的壓縮 DUMP 文件。通過 dotMemory 分析沒有看到有哪個業務使用了大量的內存,總的 .NET 內存佔用實際上纔不到 100MB 大小。因此大概可以確定不是因爲某個業務申請大量的內存導致內存泄露,至少不是申請託管內存

繼續回到確定 OOM 導致的原因上,我重新運行 Xx 應用,通過 VMMap 工具不斷按 F5 刷新,經過三個小時間斷追蹤,可以看到 Private Data 緩慢上漲。通過此,可以判斷是內存泄露問題

內存泄露通用處理方法就是先抓取泄露點,通過泄露點了解泄露模塊。抓取泄露點的通用方法就是對比幾段時間點,有哪些對象被創建且不被回收。依然是使用 ProcDump 工具抓取 DUMP 文件,然後通過 dotMemory 的導入 DUMP 功能,以及對比內存功能,進行分析

如果要是 dotMemory 可以符合預期的讓我看到業務模塊上有哪些對象沒有被釋放,那自然就不會有本文的記錄,畢竟如此簡單就能解決的問題,要是還水一篇博客就太水了。通過 dotMemory 抓取可以看到不同的時間點上,沒有任何業務代碼的對象泄露。唯一新建的幾個對象都是 System.Net 命名空間下的,而且佔用的託管內存也特別小,這幾個對象的根引用都是 Ssl 相關的底層模塊,看起來似乎沒有問題

也如一開始的調查,泄露的部分似乎不在 .NET 託管上,而是非託管的泄露。對一個純 .NET 應用來說,可以認定所有的非託管泄露都是由託管導致的。但是可惜 Xx 應用是一個複雜的應用裏面包含了其他團隊寫的一點庫邏輯。於是先嚐試定位一下是否遷移過程,修改了部分的 C++\CLI 邏輯導致的內存泄露。定位的方法是採用二分法,也就是幹掉這些引入的庫的邏輯。我重新寫了代碼,用 Fake 的方式重新實現了假邏輯,將所有的其他團隊寫的非 .NET 的庫的文件都刪掉

可惜刪除了其他團隊寫的非 .NET 的庫之後,依然存在內存泄露。也就是說可以確定是在託管層存在內存泄露的,此時我特別怕是遷移到 dotnet 6 導致的,和 Win7 的適配問題。而用 dotMemory 也無法給我帶來更多的幫助,用 dotMemory 最預期的能拿到的信息就是業務端有某些對象被泄露,可惜沒有找到任何業務端的對象泄露。那此時用 VisualStudio 是否有更多信息?不會有的,放心吧,在調試內存泄露方面,使用 VisualStudio 和 dotMemory 的能力是完全相同的,只是 VisualStudio 的交互做的太過垃圾,完全不如 dotMemory 的交互形式。因此用 dotMemory 沒有帶來更多幫助,同理使用 VisualStudio 也不會有更多幫助

爲了確定是否 dotnet 6 底層帶來的問題,我先在 dotnet 開源倉庫 https://github.com/dotnet/runtime/ 裏翻 dotnet 6 的內存相關的帖子,好在沒有找到任何有關聯的有幫助的,那就側面證明了,應該是沒有其他人遇到了此問題,這是一個好消息。但也許不是,那就是我是第一個遇到的人。其次,由於我採用的是 dotnet 6.0.1 版本,分發給用戶端的不敢那麼頭鐵用剛發佈的版本,官方最新的是 dotnet 6.0.4 版本,也許在某個安全更新修復了此問題,安全更新有一些是保密的,也就是說我沒有能找到,如果強行去找,可以用 MVP 權限去尋找,但這個響應速度就沒有那麼快

接下來可以調查的方向如下

  • 是否 dotnet 6 底層帶來的問題
  • 是否 dotnet 6.0.1 帶來的問題,但在 dotnet 6.0.4 修復了

確認是否 dotnet 6 底層帶來的問題剛好在我這個項目上,沒有那麼麻煩。我對比測試了在 Win10 的設備上,發現沒有內存泄露。剛好 Xx 應用是從 .NET Framework 遷移過來的,現在改改代碼還能跑 .NET Framework 的版本,於是也就同步在出現問題的 Win7 上跑 .NET Framework 的版本,結果發現在 Win7 上使用 .NET Framework 版本沒有任何問題。於是大概可以確定,這和 dotnet 6 底層是有所關聯,但不能說這是 dotnet 6 底層的鍋

接下來確定是否 dotnet 6.0.1 帶來的問題,但在 dotnet 6.0.4 修復了的問題。我在此出現問題的 Win7 上,使用 dotnet 6.0.4 版本代替原先的 6.0.1 版本,好在 dotnet 6 是不需要安裝的,替換文件即可。結果依然存在內存泄露,這是一個壞消息。也就是說也許我是第一個遇到此問題的人,或者說這是一個官方也不知道的問題。我就嘗試去面向羣編程,詢問了幾位大佬是否遇到過此問題,然而所有的回答都和本次遇到的不是相同的問題,且沒有一位大佬遇到 dotnet 6 底層的內存泄露問題,這也算是好消息

回到測試 dotnet 6 底層帶來的問題上,既然對比了 .NET Framework 和 dotnet 6 兩個框架,發現只有在 dotnet 6 框架纔出現問題。那可能的原因實際上可以分爲三個:

  • 遷移 dotnet 6 過程中,與 .NET Framework 的變更導致的問題
  • 由於 dotnet 6 的機制變更,與 .NET Framework 的不相同,導致的內存回收策略變更的內存泄露問題,例如之前遇到的委託問題
  • 這就是 dotnet 6 底層與 Win7 適配的問題

由於 Xx 應用是一個足夠複雜的大型應用,不好定位以上的三個原因。於是採用對比測試法,先創建一個空白的 dotnet 6 的 WPF 應用,在此 Win7 上運行。十分符合預期的,沒有內存泄露問題。這能證明,不是那麼簡單的 dotnet 6 的底層的問題。假如使用空的 dotnet 6 的 WPF 應用也能存在內存泄露,那就能快速定位是 dotnet 6 底層的問題,接下來的步驟就是看是否 WPF 的問題還是 dotnet 更底層的問題,畢竟這個 WPF 是我定製的版本,改了不少的內容

再定位是否遷移 dotnet 6 過程中,與 .NET Framework 的變更導致的問題,我尋找了所有的變更邏輯,逐個還原,或者使用 Fake 邏輯,幹掉對應的功能。這個過程相當於一個二分,也就是說如果在幹掉了某些功能之後,沒有出現內存泄露,那就能定位內存泄露和被幹掉的功能相關。完成之後,同時構建出 dotnet 6 和 .NET Framework 兩個版本,在此 Win7 上運行。結果依然是 dotnet 6 版本存在內存泄露,而 .NET Framework 版本沒有內存泄露

這就證明了原因可能就是 由於 dotnet 6 的機制變更,與 .NET Framework 的不相同,導致的內存泄露。但經過以上的測試,不能說明一定是 內存回收策略變更的內存泄露問題

到這裏,其實基本沒有了通用套路可以定位的方法了。除了使用二分法,使用二分法逐個模塊幹掉,看幹掉到哪個模塊就不存在內存泄露問題。但在此 Xx 應用上使用二分法是一個大工程,再加上內存泄露的判斷是需要等待一段時間的。而不是快速就能定位出來,需要通過 VMMap 經過一段時間,按照小時爲單位,看 Private Data 的佔用,才能瞭解到是否內存泄露。以上的測試都是可以並行多個同時開始的,儘管每個測試都需要佔用半天的時間,好在多個測試並行,以上的測試都在一天內完成。但如果採用二分,那就意味着需要進行串行測試,在上次沒有測試完成之前,是無法進行下一個二分的。我就將二分作爲最後的方法,繼續找找其他的方法

回顧一下,使用 .NET Framework 沒有問題,只有 dotnet 6 版本存在內存泄露。通過 dotMemory 和 DUMP 沒有找到業務對象的內存泄露,只有某幾個 System.Net 命名空間下的對象存在,這些對象不確定是否泄露。更新了 dotnet 6.0.4 也沒有解決,也沒有搜到帖子,問了大佬們也沒有遇到相同的問題,也就是說不是 dotnet 的官方已知問題

既然看到了存在 System.Net 命名空間下的對象存在,那可以猜測是和網絡相關的問題,剛纔的 dotnet 6 的空 WPF 測試應用只能證明和基礎的 dotnet 6 無關,但沒有證明和網絡模塊無關。繼續寫一個訪問網絡的 demo 項目,運行發現沒有內存泄露問題,看起來此內存泄露問題也不是那麼簡單能復現,一半是好消息,一半是壞消息。剛好 waterlv 大佬有空回覆我了,他告訴我,內存不會無緣無故上漲的,一定是有某些業務邏輯在跑。於是另一個方向是放棄內存的方向,而是調查空閒的時候運行了哪些邏輯

調查某個應用在某段時間運行了哪些邏輯,這是一個 CPU 性能調試問題,相當於調查一段時間內,有哪些邏輯佔用了 CPU 資源。調查這個問題最好用的工具就是 dotTrace 工具了。我準備在此 Win7 使用 dotTrace 工具抓 Xx 應用的信息,可惜 dotTrace 工具無法在此 Win7 運行,原因有兩個,一個是需要 .NET Framework 4.7 的環境,另一個就是 ETW 準備失敗。其中 ETW 準備失敗也就無法抓取信息,於是我放棄了 dotTrace 工具

剛好 dotnet 系裏面有 dotnet trace 工具,此工具可以完美在 Win7 運行。於是我換用 dotnet trace 工具去抓取,雖然是抓取到了信息,但是 dotnet trace 工具比 dotTrace 工具還是差太遠了,差距大概是一個是記事本,一個是 SublimeText 的差距,我沒有成功分析出來什麼,反而又過去了一天

那換一個方式,通過 DUMP 抓取瞬時的線程調用堆棧,可以看到有很多線程存在,但是基本上都是不在運行的線程。唯一一個看起來稍微相關的堆棧如下

> ntdll.dll!_ZwWaitForMultipleObjects@20() Unknown
  KERNELBASE.dll!_WaitForMultipleObjectsEx@20()  Unknown
  kernel32.dll!_WaitForMultipleObjectsExImplementation@20()  Unknown
  kernel32.dll!_WaitForMultipleObjects@16()  Unknown
  winhttp.dll!HANDLE_OBJECT::IsInvalidated(void)  Unknown
  winhttp.dll!OutProcGetProxyForUrl(class INTERNET_SESSION_HANDLE_OBJECT *,unsigned short const *,struct WINHTTP_AUTOPROXY_OPTIONS const *,struct WINHTTP_PROXY_INFO *) Unknown
  winhttp.dll!_WinHttpGetProxyForUrl@16()  Unknown
  cryptnet.dll!InetGetProxy(void *,void *,unsigned short const *,unsigned long,struct WINHTTP_PROXY_INFO * *) Unknown
  cryptnet.dll!InetSendAuthenticatedRequestAndReceiveResponse(void *,void *,unsigned short const *,unsigned short const *,unsigned char const *,unsigned long,unsigned long,struct WINHTTP_PROXY_INFO *,struct _CRYPT_CREDENTIALS *,struct _CRYPT_RETRIEVE_AUX_INFO *)  Unknown
  cryptnet.dll!_InetSendReceiveUrlRequest@32() Unknown
  cryptnet.dll!CInetSynchronousRetriever::RetrieveObjectByUrl(unsigned short const *,char const *,unsigned long,unsigned long,struct _CRYPT_BLOB_ARRAY *,void (**)(char const *,struct _CRYPT_BLOB_ARRAY *,void *),void * *,void *,struct _CRYPT_CREDENTIALS *,struct _CRYPT_RETRIEVE_AUX_INFO *) Unknown
  cryptnet.dll!_InetRetrieveEncodedObject@40() Unknown
  cryptnet.dll!CObjectRetrievalManager::RetrieveObjectByUrl(unsigned short const *,char const *,unsigned long,unsigned long,void * *,void *,struct _CRYPT_CREDENTIALS *,void *,struct _CRYPT_RETRIEVE_AUX_INFO *) Unknown
  cryptnet.dll!CryptRetrieveObjectByUrlWithTimeoutThreadProc(void *)  Unknown
  kernel32.dll!@BaseThreadInitThunk@12() Unknown

看起來和系統的 cryptnet.dll 有幾毛錢關係,也許這是 Win7 一個已知的問題,也許更新了某個補丁能解決。到這裏想要繼續就只能通過 WinDbg 了,玩 WinDbg 工具需要花太多的時間,於是我先掛着 WinDbg 在 Win7 系統上,拉符號文件,將我本機的符號文件夾共享給他。拉取符號和共享符號文件夾需要半天的時間,我也不能摸魚。似乎走 CPU 分析這個路是不可行的。繼續回到分析內存的方法

繼續猜測是網絡相關問題,好在使用的是虛擬機,我聽了 waterlv 大佬的方法,禁用了網卡,跑了一個晚上,沒有內存泄露。那基本可以定位和網絡問題是強相關了。於是開啓 Fiddler 準備抓數據,默認的 Fiddler 是沒有抓 Https 的請求的,我分爲兩個階段,先抓 http 的請求,結果發現 Xx 應用沒有任何 http 請求。開啓 Fiddler 的抓取 https 請求,結果發現有某些請求發出,但是此時詭異的是 Xx 應用不再有內存泄露了

我根據 Fiddler 抓 Https 請求的原理猜測是因爲 Fiddler 爲了抓取 Https 安裝的證書導致 Xx 應用的行爲和之前不同,從而沒有內存泄露問題。於是做對比測試,關掉 Fiddler 的抓 https 功能,重啓 Xx 應用,跑了半天,內存泄露

大概可以定位到和證書相關,繼續定位是和請求哪個鏈接相關,從代碼裏面進行二分邏輯,從 Fiddler 裏面抓到的各個請求的代碼,逐個幹掉,終於被我定位到核心的問題所在。我的另一個本機的服務應用,這是一個在本機開啓的進程服務,通過 Https 進行 IPC 本機跨進程通訊。業務模塊和這個本地服務應用有心跳通訊,每次通訊都是內存泄露。那爲什麼這個本地服務應用的通訊會讓 Xx 應用內存泄露,根據 Fidder 的證書問題我猜測和證書相關。重新閱讀這個服務應用的代碼,以及請教了 lsj 證書相關知識點之後,瞭解到這個服務應用,採用的證書有點問題,這個服務應用的證書鏈是不完整的,剛好在此 Win7 系統上,證書也都沒有更新

解決的方法有幾個:

  • 換用 http 通訊,都是本機了,還用什麼 https 通訊
  • 換用 HttpClient 通訊,默認明確拋出 System.Security.Authentication.AuthenticationException: The remote certificate is invalid because of errors in the certificate chain: PartialChain 異常

換用 HttpClient 通訊時,可以使用如下代碼忽略證書錯誤問題,但是此方式是不受推薦的

var handler = new HttpClientHandler()
{
   ServerCertificateCustomValidationCallback = delegate { return true; }
};
var httpClient = new HttpClient(handler);

於是我將 Https 換成 Http 的方式,再次測試,跑了一段時間,沒有內存泄露。看起來就是證書導致的問題

邏輯上也是對的,一次對本機的服務應用訪問,不需要創建任何業務端的對象,全部使用的都是 System.Net 的對象,這就是使用 dotMemory 工具失敗的原因,而且請求的速度也足夠快,無法讓 DUMP 抓到信息,再加上異步是沒有 DUMP 的線程堆棧,這就讓上面使用 DUMP 調試的方法掛掉。其實要是 dotTrace 能跑起來,是可以快速定位到此模塊的,可惜 dotnet trace 還是比較渣。在瞭解到是這個模塊的時候,我換用 PerfView 去調試 dotnet trace 抓的文件,其實依然能看到這個模塊的邏輯,可惜如果沒有了解到是這個模塊的問題時,應該是無法通過 PerfView 定位的。也就是說,實際上 dotnet trace 是具備此定位的能力的,能收集到足夠的信息,但上層的分析工具卻是渣的很,無論是 VisualStudio 還是 PerfView 工具,在界面和交互上都渣

不過說 VisualStudio 還是 PerfView 工具渣,我還是需要和 dotTrace 對比一下。和這個本地服務應用的通訊模塊,在我的開發設備上也是相同運行的,和在 Win7 系統上一樣,差別只是我的開發設備上沒有內存泄露。但是如上文,其實只是調查某段時間的 CPU 佔用,和內存泄露沒有關係。我在開發設備上開啓 dotTrace 工具,抓了 Xx 應用,果然迅速就看到了和這個本地服務應用的通訊模塊的執行邏輯。也就是說如果有 dotTrace 工具一開始就能跑起來,應該可以半天內搞定

噴完了 VisualStudio 工具渣,剛好此時 WinDbg 的符號也下載完成了,可以繼續調查更底層的邏輯,依然從內存的角度調查。在 VMMap 工具上,通過 Private Data 的數據可以看到堆上有很多大小相同的數據,根據 Win32 內存調試的套路,基本上可以確定這就是某個相同的模塊申請的,而且也沒有釋放

爲了確定是哪個模塊申請了某個非託管內存,我使用了 gflags 工具的輔助,這個工具就放在 WinDbg 所在的文件夾裏面,在命令行執行下面命令,執行的時候將會提示管理員權限,執行完成之後是不會有任何界面的

gflags.exe /i Xx.exe +ust

使用以上命令,即可讓 gflags 輔助抓取 Xx 應用的內存申請的調用堆棧。以上命令的 Xx.exe 是不需要也不能使用絕對路徑的,只是一個進程的文件名即可,因爲實際上的抓取邏輯還是在 WinDbg 下執行。詳細請看 官方文檔

接下來是將 Xx 應用跑起來,由於 Xx 應用是在空閒的時候,沒有用戶交互,就出現內存泄露,爲了減少 WinDbg 的複雜調試,我在應用跑起來,啓動完成,才使用 WinDbg 附加調試

儘管知道是某個大小的數據佔用了 Private Data 內存,但我對 VMMap 工具不夠熟悉,不敢作爲結果使用,但是可以作爲方向。我重新通過 WinDbg 定位是否某個模塊申請了內存沒有釋放,步驟就是先找到哪個內存在變更,對應的堆裏面的內容,是否某個大小的數據是在不斷泄露的,這些大小的數據的申請的調用堆棧是什麼

先通過 !heap -s 命令多次執行,瞭解是那個內存在變更

按照慣例是執行至少兩次進行對比,對於大型應用,基本上都推薦是三次以上。不過我通過 VMMap 工具大概瞭解到方向了,於是就只使用三次。首次執行的命令和輸出如下

0:024> !heap -s
LFH Key                   : 0x5327c840
Termination on corruption : ENABLED
  Heap     Flags   Reserv  Commit  Virt   Free  List   UCR  Virt  Lock  Fast 
                    (k)     (k)    (k)     (k) length      blocks cont. heap 
-----------------------------------------------------------------------------
00420000 00000002   48768  43096  48768   1929   715    16    0      3   LFH
006b0000 00001002    1088    680   1088      8    21     2    0      0   LFH
00e30000 00001002     256    204    256      2    21     1    0      0   LFH
00df0000 00041002     256      4    256      2     1     1    0      0      
01170000 00001002    1088    196   1088     16     8     2    0      0   LFH
05970000 00041002     256      4    256      2     1     1    0      0      
05920000 00001002     256    160    256      3     7     1    0      0   LFH
083a0000 00001002     256    172    256    118     3     1    0      0      
0b240000 00001002     256    168    256      5    10     1    0      0   LFH
0a3f0000 00041002     256     16    256      5     1     1    0      0      
0e510000 00011002     256     12    256      9     6     1    0      0      
0ec10000 00001002     256    148    256      6     5     1    0      0   LFH
0ee20000 00001002     256    256    256    111    11     1    0      0   LFH
0ed10000 00001002      64     52     64      7     3     1    0      0      
0f990000 00001002     256      4    256      1     2     1    0      0      
0fdb0000 00001002   12096   4048  12096   2601    32     8    0      0   LFH
    External fragmentation  64 % (32 free blocks)
08700000 00001002      64      4     64      2     1     1    0      0      
-----------------------------------------------------------------------------

在 WinDbg 按下 g 命令讓應用繼續運行一段時間

0:024> g
(7c0.1874): CLR exception - code e0434352 (first chance)
(7c0.1874): CLR exception - code e0434352 (first chance)
(7c0.e64): Break instruction exception - code 80000003 (first chance)
eax=fff9c000 ebx=00000000 ecx=00000000 edx=7743f7ea esi=00000000 edi=00000000
eip=773b000c esp=0a5efe4c ebp=0a5efe78 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
ntdll!DbgBreakPoint:
773b000c cc              int     3

可以看到存在一些 CLR 異常,這就是本文開頭所抓到的 CLR 異常的部分,但不是相同的異常信息。這些是可以忽略的,而且我也大概定位到方向,加上前幾天也嘗試定位了 CLR 異常沒有收穫,就沒有繼續定位

讓 Xx 應用跑了一段時間,在 WinDbg 工具按下暫停,繼續執行 !heap -s 命令

0:007> !heap -s
LFH Key                   : 0x5327c840
Termination on corruption : ENABLED
  Heap     Flags   Reserv  Commit  Virt   Free  List   UCR  Virt  Lock  Fast 
                    (k)     (k)    (k)     (k) length      blocks cont. heap 
-----------------------------------------------------------------------------
00420000 00000002   81152  67244  81152   1992   723    18    0      3   LFH
006b0000 00001002    1088    680   1088      8    22     2    0      0   LFH
00e30000 00001002     256    204    256      2    21     1    0      0   LFH
00df0000 00041002     256      4    256      2     1     1    0      0      
01170000 00001002    1088    196   1088     16     9     2    0      0   LFH
05970000 00041002     256      4    256      2     1     1    0      0      
05920000 00001002     256    160    256      3     7     1    0      0   LFH
083a0000 00001002     256    172    256    118     3     1    0      0      
0b240000 00001002     256    168    256      5    10     1    0      0   LFH
0a3f0000 00041002     256     16    256      5     1     1    0      0      
0e510000 00011002     256     12    256      9     6     1    0      0      
0ec10000 00001002     256    148    256      6     5     1    0      0   LFH
0ee20000 00001002     256    256    256    111    11     1    0      0   LFH
0ed10000 00001002      64     52     64      7     3     1    0      0      
0f990000 00001002     256      4    256      1     2     1    0      0      
0fdb0000 00001002   12096   4048  12096   2601    32     8    0      0   LFH
    External fragmentation  64 % (32 free blocks)
08700000 00001002      64      4     64      2     1     1    0      0      
-----------------------------------------------------------------------------

大概可以看到 00420000 的大小從 4876881152 的大小

使用 !heap -stat -h 00420000 瞭解這個內存裏面的數據分佈情況

0:007> !heap -stat -h 00420000
 heap @ 00420000
group-by: TOTSIZE max-display: 20
    size     #blocks     total     ( %) (percent of total busy bytes)
    27994 71 - 117aa54  (37.88)
    269f8 6f - 10bf288  (36.29)
    fdcc 67 - 661d14  (13.83)
    10 7560 - 75600  (0.99)
    1c 2fec - 53dd0  (0.71)
    49a9c 1 - 49a9c  (0.62)
    390 e3 - 328b0  (0.43)
    711 68 - 2dee8  (0.39)
    284 108 - 29820  (0.35)
    618 64 - 26160  (0.32)
    40 934 - 24d00  (0.31)
    20 11f8 - 23f00  (0.30)
    70 49e - 20520  (0.27)
    50 639 - 1f1d0  (0.26)
    60 4b2 - 1c2c0  (0.24)
    dce0 2 - 1b9c0  (0.23)
    84 2d7 - 176dc  (0.20)
    15f13 1 - 15f13  (0.19)
    15eee 1 - 15eee  (0.19)
    30 6c5 - 144f0  (0.17)

可以看到大小爲 27994 的數據有 0x71 個,而大小爲 269f8 的數據有 0x6f 個。其實這兩個不能說明問題,繼續讓 Xx 應用執行一段時間,再輸入 !heap -s 命令

0:019> !heap -s
LFH Key                   : 0x5327c840
Termination on corruption : ENABLED
  Heap     Flags   Reserv  Commit  Virt   Free  List   UCR  Virt  Lock  Fast 
                    (k)     (k)    (k)     (k) length      blocks cont. heap 
-----------------------------------------------------------------------------
00420000 00000002   97344  91356  97344   2082   730    19    0      3   LFH
006b0000 00001002    1088    680   1088      9    22     2    0      0   LFH
00e30000 00001002     256    204    256      2    21     1    0      0   LFH
00df0000 00041002     256      4    256      2     1     1    0      0      
01170000 00001002    1088    196   1088     17     9     2    0      0   LFH
05970000 00041002     256      4    256      2     1     1    0      0      
05920000 00001002     256    160    256      3     7     1    0      0   LFH
083a0000 00001002     256    172    256    118     3     1    0      0      
0b240000 00001002     256    172    256      5    11     1    0      0   LFH
0a3f0000 00041002     256     16    256      5     1     1    0      0      
0e510000 00011002     256     12    256      9     6     1    0      0      
0ec10000 00001002     256    148    256      6     5     1    0      0   LFH
0ee20000 00001002     256    256    256    111    11     1    0      0   LFH
0ed10000 00001002      64     52     64      7     3     1    0      0      
0f990000 00001002     256      4    256      1     2     1    0      0      
0fdb0000 00001002   12096   4048  12096   2601    32     8    0      0   LFH
    External fragmentation  64 % (32 free blocks)
08700000 00001002      64      4     64      2     1     1    0      0      
-----------------------------------------------------------------------------

可以看到 00420000 佔用的內存更加多了,使用 !heap -stat -h 00420000 查看

0:019> !heap -stat -h 00420000
 heap @ 00420000
group-by: TOTSIZE max-display: 20
    size     #blocks     total     ( %) (percent of total busy bytes)
    27994 b1 - 1b60f54  (39.25)
    269f8 af - 1a67088  (37.85)
    fdcc a6 - a49248  (14.75)
    10 757a - 757a0  (0.66)
    1c 2ff4 - 53eb0  (0.47)
    49a9c 1 - 49a9c  (0.41)
    711 97 - 42b07  (0.37)
    618 86 - 33090  (0.29)
    390 e3 - 328b0  (0.28)
    284 108 - 29820  (0.23)
    40 935 - 24d40  (0.21)
    20 1236 - 246c0  (0.20)
    70 4a2 - 206e0  (0.18)
    50 63a - 1f220  (0.17)
    60 4b2 - 1c2c0  (0.16)
    dce0 2 - 1b9c0  (0.15)
    84 2d7 - 176dc  (0.13)
    15f13 1 - 15f13  (0.12)
    15eee 1 - 15eee  (0.12)
    30 6c5 - 144f0  (0.11)

可以看到前面兩個變更了,也就是大小爲 27994 的數據和大小爲 269f8 的數據的數量變更了

原先:
    27994 71 - 117aa54  (37.88)
    269f8 6f - 10bf288  (36.29)
當前:
    27994 b1 - 1b60f54  (39.25)
    269f8 af - 1a67088  (37.85)

也就是說大小 Size 爲 27994 的存在很多重複項

接下來就是獲取到這些被分配內存的地址,使用命令 !heap -flt s 27994 過濾其它的內存塊,只顯示大小爲 27994 的內存塊信息

0:019> !heap -flt s 27994
    _HEAP @ 420000
      HEAP_ENTRY Size Prev Flags    UserPtr UserSize - state
        05fd2880 4f34 0000  [00]   05fd2888    27994 - (busy)
        06020c20 4f34 4f34  [00]   06020c28    27994 - (busy)
        0614cc18 4f34 4f34  [00]   0614cc20    27994 - (busy)
        08a719d0 4f34 4f34  [00]   08a719d8    27994 - (busy)
        08b05028 4f34 4f34  [00]   08b05030    27994 - (busy)
        08b9e4f0 4f34 4f34  [00]   08b9e4f8    27994 - (busy)
      .....  
        0b493108 4f34 4f34  [00]   0b493110    27994 - (busy)
      .....  
        0b366408 4f34 4f34  [00]   106b9378    27994 - (busy)
      .....  
        1e2abff8 4f34 4f34  [00]   1e2ac000    27994 - (busy)
        1e31a178 4f34 4f34  [00]   1fa93750    27994 - (busy)
        1e3782f0 4f34 4f34  [00]   1e3782f8    27994 - (busy)
        1e3d6468 4f34 4f34  [00]   2004dc80    27994 - (busy)
    _HEAP @ 6b0000
    _HEAP @ e30000
    _HEAP @ df0000
    _HEAP @ 1170000
    _HEAP @ 5970000
    _HEAP @ 5920000
    _HEAP @ 83a0000
    _HEAP @ b240000
    _HEAP @ a3f0000
    _HEAP @ e510000
    _HEAP @ ec10000
    _HEAP @ ee20000
    _HEAP @ ed10000
    _HEAP @ f990000
    _HEAP @ fdb0000
    _HEAP @ 8700000

輸出的內容太多了,我忽略了一些信息

剛纔開啓了 GFlags 工具,可以通過 !heap -p -a <UserPtr> 瞭解內存塊的申請調用堆棧,也就是哪個模塊申請的內存。此命令的 <UserPtr> 請替換爲 UserPtr 這一列的內存地址。需要抓幾個內存塊地址來進行統計才能瞭解是哪個模塊申請而且泄露的

我先抓取了 2004dc80 地址的信息

!heap -p -a 2004dc80
    address 2004dc80 found in
    _HEAP @ 490000
      HEAP_ENTRY Size Prev Flags    UserPtr UserSize - state
        2004dc68 4f36 0000  [00]   2004dc80    27994 - (busy)
        7741df42 ntdll!RtlAllocateHeap+0x00000274
        76874ec3 KERNELBASE!LocalAlloc+0x0000005f
        76424b84 CRYPT32!PkiAlloc+0x00000032
        764516b3 CRYPT32!ChainCreateCyclicPathObject+0x000000b8
        764515c7 CRYPT32!ExtractEncodedCtlFromCab+0x000001b0
        7645142c CRYPT32!ExtractAuthRootAutoUpdateCtlFromCab+0x00000041
        764504d3 CRYPT32!CCertChainEngine::GetAuthRootAutoUpdateCtl+0x000001f8
        764c047c CRYPT32!CChainPathObject::GetAuthRootAutoUpdateUrlStore+0x00000082
        76469850 CRYPT32!CChainPathObject::CChainPathObject+0x000003d0
        76437934 CRYPT32!ChainCreatePathObject+0x0000005e
        76437da9 CRYPT32!CCertIssuerList::AddIssuer+0x0000006c
        764387ac CRYPT32!CChainPathObject::FindAndAddIssuersFromStoreByMatchType+0x0000018b
        764386bd CRYPT32!CChainPathObject::FindAndAddIssuersByMatchType+0x00000096
        7643bbc6 CRYPT32!CChainPathObject::FindAndAddIssuers+0x00000063
        764697e0 CRYPT32!CChainPathObject::CChainPathObject+0x0000035b
        76437934 CRYPT32!ChainCreatePathObject+0x0000005e
        76438c8d CRYPT32!CCertChainEngine::CreateChainContextFromPathGraph+0x000001ae
        76438a6e CRYPT32!CCertChainEngine::GetChainContext+0x00000046
        76436d42 CRYPT32!CertGetCertificateChain+0x00000072

然後再選中間的 1fa93750 地址

0:042> !heap -p -a 1fa93750
    address 1fa93750 found in
    _HEAP @ 490000
      HEAP_ENTRY Size Prev Flags    UserPtr UserSize - state
        1fa93738 4f36 0000  [00]   1fa93750    27994 - (busy)
        7741df42 ntdll!RtlAllocateHeap+0x00000274
        76874ec3 KERNELBASE!LocalAlloc+0x0000005f
        76424b84 CRYPT32!PkiAlloc+0x00000032
        764516b3 CRYPT32!ChainCreateCyclicPathObject+0x000000b8
        764515c7 CRYPT32!ExtractEncodedCtlFromCab+0x000001b0
        7645142c CRYPT32!ExtractAuthRootAutoUpdateCtlFromCab+0x00000041
        764504d3 CRYPT32!CCertChainEngine::GetAuthRootAutoUpdateCtl+0x000001f8
        764c047c CRYPT32!CChainPathObject::GetAuthRootAutoUpdateUrlStore+0x00000082
        76469850 CRYPT32!CChainPathObject::CChainPathObject+0x000003d0
        76437934 CRYPT32!ChainCreatePathObject+0x0000005e
        76437da9 CRYPT32!CCertIssuerList::AddIssuer+0x0000006c
        764387ac CRYPT32!CChainPathObject::FindAndAddIssuersFromStoreByMatchType+0x0000018b
        764386bd CRYPT32!CChainPathObject::FindAndAddIssuersByMatchType+0x00000096
        7643bbc6 CRYPT32!CChainPathObject::FindAndAddIssuers+0x00000063
        764697e0 CRYPT32!CChainPathObject::CChainPathObject+0x0000035b
        76437934 CRYPT32!ChainCreatePathObject+0x0000005e
        76438c8d CRYPT32!CCertChainEngine::CreateChainContextFromPathGraph+0x000001ae
        76438a6e CRYPT32!CCertChainEngine::GetChainContext+0x00000046
        76436d42 CRYPT32!CertGetCertificateChain+0x00000072

最後選了比較前面的地址

0:042> !heap -p -a 106b9378
    address 106b9378 found in
    _HEAP @ 490000
      HEAP_ENTRY Size Prev Flags    UserPtr UserSize - state
        106b9360 4f36 0000  [00]   106b9378    27994 - (busy)
        7741df42 ntdll!RtlAllocateHeap+0x00000274
        76874ec3 KERNELBASE!LocalAlloc+0x0000005f
        76424b84 CRYPT32!PkiAlloc+0x00000032
        764516b3 CRYPT32!ChainCreateCyclicPathObject+0x000000b8
        764515c7 CRYPT32!ExtractEncodedCtlFromCab+0x000001b0
        7645142c CRYPT32!ExtractAuthRootAutoUpdateCtlFromCab+0x00000041
        764504d3 CRYPT32!CCertChainEngine::GetAuthRootAutoUpdateCtl+0x000001f8
        764c047c CRYPT32!CChainPathObject::GetAuthRootAutoUpdateUrlStore+0x00000082
        76469850 CRYPT32!CChainPathObject::CChainPathObject+0x000003d0
        76437934 CRYPT32!ChainCreatePathObject+0x0000005e
        76438c8d CRYPT32!CCertChainEngine::CreateChainContextFromPathGraph+0x000001ae
        76438a6e CRYPT32!CCertChainEngine::GetChainContext+0x00000046
        76436d42 CRYPT32!CertGetCertificateChain+0x00000072

可以看到都是 CRYPT32.dll 的 CertGetCertificateChain 函數申請的,對比剛纔的 DUMP 抓到的線程調用堆棧,似乎 CRYPT32.dll 這個系統組件就是有鍋的。而且 CRYPT32.dll 就是處理證書相關的邏輯。 通過官方文檔瞭解到 CertGetCertificateChain 就是證書鏈相關邏輯

根據上文使用二分調試到的,和本地服務應用的通訊模塊的證書鏈在 Win7 系統上損壞導致的內存泄露。現在根據 WinDbg 可以看到是 CertGetCertificateChain 處理證書鏈申請的內存沒有釋放,那就證明一定是證書鏈的問題

剛纔通過 WinDbg 抓到的內存變更的內存塊大小有兩個,接下來再看 269f8 大小的內存塊的地址

0:042> !heap -flt s 269f8
    _HEAP @ 490000
      HEAP_ENTRY Size Prev Flags    UserPtr UserSize - state
        084e4400 4d42 0000  [00]   084e4418    269f8 - (busy)
        0b810470 4d42 4d42  [00]   0b810488    269f8 - (busy)
        0b8cb7e8 4d42 4d42  [00]   0b8cb800    269f8 - (busy)
        0b90b900 4d42 4d42  [00]   0b90b918    269f8 - (busy)
        0b96b990 4d42 4d42  [00]   0b96b9a8    269f8 - (busy)
        0b9cba20 4d42 4d42  [00]   0b9cba38    269f8 - (busy)
        0ba3f108 4d42 4d42  [00]   0ba3f120    269f8 - (busy)
        105650b8 4d42 4d42  [00]   105650d0    269f8 - (busy)
        10692950 4d42 4d42  [00]   10692968    269f8 - (busy)
        10754ec0 4d42 4d42  [00]   10754ed8    269f8 - (busy)
        107f2630 4d42 4d42  [00]   107f2648    269f8 - (busy)
        10c28f90 4d42 4d42  [00]   10c28fa8    269f8 - (busy)
        10c8d038 4d42 4d42  [00]   10c8d050    269f8 - (busy)
        10cc4670 4d42 4d42  [00]   10cc4688    269f8 - (busy)
        10e0dbd0 4d42 4d42  [00]   10e0dbe8    269f8 - (busy)
        10e5bf90 4d42 4d42  [00]   10e5bfa8    269f8 - (busy)
      .....  
        201783a8 4d42 4d42  [00]   201783c0    269f8 - (busy)
        201ff188 4d42 4d42  [00]   201ff1a0    269f8 - (busy)
        2025d330 4d42 4d42  [00]   2025d348    269f8 - (busy)
        20329698 4d42 4d42  [00]   203296b0    269f8 - (busy)
    _HEAP @ 760000
    _HEAP @ a20000
    _HEAP @ ec0000
    _HEAP @ 1060000
    _HEAP @ 4e50000
    _HEAP @ 1010000
    _HEAP @ bd10000
    _HEAP @ e5c0000
    _HEAP @ e7f0000
    _HEAP @ 11900000
    _HEAP @ 11c10000
    _HEAP @ 12030000
    _HEAP @ 12750000
    _HEAP @ 12880000
    _HEAP @ 13410000
    _HEAP @ 1a2b0000

先隨意選擇 201ff1a0 內存地址,通過 !heap -p -a 201ff1a0 瞭解是哪個模塊申請

0:042> !heap -p -a 201ff1a0
    address 201ff1a0 found in
    _HEAP @ 490000
      HEAP_ENTRY Size Prev Flags    UserPtr UserSize - state
        201ff188 4d42 0000  [00]   201ff1a0    269f8 - (busy)
        7741df42 ntdll!RtlAllocateHeap+0x00000274
        76874ec3 KERNELBASE!LocalAlloc+0x0000005f
        76424b84 CRYPT32!PkiAlloc+0x00000032
        76447489 CRYPT32!ICM_GetListSignedData+0x000000fa
        76447299 CRYPT32!ICM_UpdateDecodingSignedData+0x0000006d
        764475cc CRYPT32!CryptMsgUpdate+0x000001e0
        764464c4 CRYPT32!FastCreateCtlElement+0x00000221
        76446252 CRYPT32!CertCreateContext+0x000000f1
        76451464 CRYPT32!ExtractAuthRootAutoUpdateCtlFromCab+0x000000b0
        764504d3 CRYPT32!CCertChainEngine::GetAuthRootAutoUpdateCtl+0x000001f8
        764c047c CRYPT32!CChainPathObject::GetAuthRootAutoUpdateUrlStore+0x00000082
        76469850 CRYPT32!CChainPathObject::CChainPathObject+0x000003d0
        76437934 CRYPT32!ChainCreatePathObject+0x0000005e
        76437da9 CRYPT32!CCertIssuerList::AddIssuer+0x0000006c
        764387ac CRYPT32!CChainPathObject::FindAndAddIssuersFromStoreByMatchType+0x0000018b
        764386bd CRYPT32!CChainPathObject::FindAndAddIssuersByMatchType+0x00000096
        7643bbc6 CRYPT32!CChainPathObject::FindAndAddIssuers+0x00000063
        764697e0 CRYPT32!CChainPathObject::CChainPathObject+0x0000035b
        76437934 CRYPT32!ChainCreatePathObject+0x0000005e
        76438c8d CRYPT32!CCertChainEngine::CreateChainContextFromPathGraph+0x000001ae
        76438a6e CRYPT32!CCertChainEngine::GetChainContext+0x00000046
        76436d42 CRYPT32!CertGetCertificateChain+0x00000072

依然是 CertGetCertificateChain 申請的,這是一個利好消息。繼續再隨意找了 10e0dbe8 地址,通過 !heap -p -a 10e0dbe8 瞭解是哪個模塊申請

0:042> !heap -p -a 10e0dbe8
    address 10e0dbe8 found in
    _HEAP @ 490000
      HEAP_ENTRY Size Prev Flags    UserPtr UserSize - state
        10e0dbd0 4d42 0000  [00]   10e0dbe8    269f8 - (busy)
        7741df42 ntdll!RtlAllocateHeap+0x00000274
        76874ec3 KERNELBASE!LocalAlloc+0x0000005f
        76424b84 CRYPT32!PkiAlloc+0x00000032
        76447489 CRYPT32!ICM_GetListSignedData+0x000000fa
        76447299 CRYPT32!ICM_UpdateDecodingSignedData+0x0000006d
        764475cc CRYPT32!CryptMsgUpdate+0x000001e0
        764464c4 CRYPT32!FastCreateCtlElement+0x00000221
        76446252 CRYPT32!CertCreateContext+0x000000f1
        76451464 CRYPT32!ExtractAuthRootAutoUpdateCtlFromCab+0x000000b0
        764504d3 CRYPT32!CCertChainEngine::GetAuthRootAutoUpdateCtl+0x000001f8
        764c047c CRYPT32!CChainPathObject::GetAuthRootAutoUpdateUrlStore+0x00000082
        76469850 CRYPT32!CChainPathObject::CChainPathObject+0x000003d0
        76437934 CRYPT32!ChainCreatePathObject+0x0000005e
        76438c8d CRYPT32!CCertChainEngine::CreateChainContextFromPathGraph+0x000001ae
        76438a6e CRYPT32!CCertChainEngine::GetChainContext+0x00000046
        76436d42 CRYPT32!CertGetCertificateChain+0x00000072

可以看到依然是 CertGetCertificateChain 申請的

現在可以完全證明內存泄露問題是證書鏈損壞導致 CertGetCertificateChain 內存泄露

但是無法確定 CertGetCertificateChain 內存泄露的更底層原因,也無法確定這是否是 Win7 這個版本存在的問題,是否安裝了補丁可以修復,還是因爲 dotnet 6 調用的問題。我嘗試去搜以上的堆棧,找到了 2013 的帖子 IE crashes due to SSL certificate check - Problem with MSVCR80.dll, - Microsoft Community

看起來和上面說的是相同的一個問題,我預計是有補丁可以解決。而且讓 Win7 修復證書預計也能解決此問題

繼續調查是否因爲 dotnet 6 調用的問題,從 WinDbg 上看到的堆棧只是到 CertGetCertificateChain 函數,這是因爲我沒有加載 dotnet 6 的 sos 因此無法拿到 .NET 層的調用信息。如何加載 dotnet 6 的 sos 請看 WinDbg 加載 dotnet core 的 sos.dll 輔助調試方法

在調試到 CertGetCertificateChain 申請的內存沒有泄露,後續的調試我也不用 WinDbg 了,也不需要去加載 dotnet 6 的 sos 了。我通過靜態代碼分析,閱讀 dotnet 6 的底層代碼,看到了下面代碼

internal sealed partial class ChainPal
{
   internal static partial IChainPal? BuildChain()
   {
       // 忽略代碼
                            if (!Interop.Crypt32.CertGetCertificateChain(storeHandle.DangerousGetHandle(), certificatePal.CertContext, &ft, extraStoreHandle, ref chainPara, flags, IntPtr.Zero, out chain))
                            {
                                return null;
                            }
   }
}

根據官方文檔,需要使用 CertFreeCertificateChain 釋放上面代碼的 chain 變量。然而如上面代碼,在 CertGetCertificateChain 方法返回 false 值,就返回了,沒有對 chain 調用釋放

我不瞭解是否在 CertGetCertificateChain 方法返回 false 值,就不需要調用 CertFreeCertificateChain 的問題,我反饋給了 dotnet 官方,詳細請看 CertGetCertificateChain memory leak in pure Windows 7 system · Issue #68892 · dotnet/runtime

通過閱讀 mozilla 的代碼,看到了 mozilla 在 CertGetCertificateChain 方法返回 false 值,也是立刻返回,沒有調用 CertFreeCertificateChain 方法,詳細請看 https://hg.mozilla.org/releases/mozilla-release/rev/d9659c22b3c5#l3.347

但是 Xx 應用的內存泄露問題已解決,後續就交給 dotnet 官方

那爲什麼 .NET Framework 就不存在問題?我繼續閱讀 dotent 代碼和考古 .NET Framework 的代碼,看到了這個邏輯是在 .NET Framework 4.6 變更的,也就是本文開始說的內容。剛好 Xx 應用是從 .NET Framework 4.5 升級到 dotnet 6 的,剛好就踩到這個坑

我回顧了本次的調試,用了五天,實際上方向錯了。如果開始聽 waterlv 大佬,內存不會無緣無故上漲的,一定是有某些業務邏輯在跑,通過調試 CPU 佔用的方法,是能在一天內完成。而如上文的調試過程,我調試的方向都是去調試內存,這是不對的。通過 Fiddler 定位是證書問題和定位是 IPC 使用 Https 通訊且證書鏈損壞,也是定位有哪些業務模塊在執行,也就是調試 CPU 佔用。通過任務管理器可以看到,大概每間隔 3 秒就有 CPU 佔用,也就是說可以認爲在 Xx 應用,所有定時任務小於 10 秒的,都是可能導致本次內存泄露的邏輯,我再次閱讀 Xx 應用的代碼,看到了定時任務小於 10 秒的任務,才只有 5 個。通過二分的方法,逐個定時任務幹掉,讓這些定時任務一個個都不跑,看哪個定時任務不跑就沒有內存泄露,就可以定位到具體的模塊。瞭解到是哪個模塊就可以快速瞭解到具體原因。如果開始使用這個方法,可以在一天內完成,而不是花了兩週時間

這就是本次我用 dotnet 6 在 Win7 系統上運行,由於用到了詭異的方式實現的邏輯,導致了觸發了一個系統組件或者是 dotnet 底層的坑,讓應用內存泄露了,我記錄了調試的過程,以及調試使用的工具,讓大家看的更加無聊

更多請看

ServicePointManager Class (System.Net) Microsoft Docs

無法連接到一臺服務器升級到.NET Framework 4.6 後使用 ServicePointManager 或 SslStream Api

CLR Exception E0434352 Microsoft Docs

EmptyWorkingSet function (psapi.h) - Win32 apps Microsoft Docs

使用 ProcDump 解決 VMM 服務問題 - Virtual Machine Manager Microsoft Docs

ProcDump - Windows Sysinternals Microsoft Docs

GFlags - Windows drivers Microsoft Docs

CertGetCertificateChain function (wincrypt.h) - Win32 apps Microsoft Docs

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章