2020年4月28日 星期二

OpenSSL 1.0.2 upgrade到 OpenSSL 1.1.1 遇到的memory leak



原本這blog幾乎就只是拿來當我side project的release note, 沒什麼拿來寫文章(文筆不好又沒什麼內容是想持續寫的, 寫文章又超花時間...), 不過最近想開始紀錄一些生命中遇到的Bug, 特別是那種網路上超難找到解方的bug, 希望寫的這些文章能剛好幫上遇到一樣問題的人~。

這次的內容是OpenSSL的memory leak, 在某專案上的service剛好發現有memory leak出現, 然後出問題的那段code是Dll plugin, 所以沒辦法直接用VLD (Visual Leak Detector)找問題點, 只能退而求其次選擇直接印heap用量在log上, 之後就用註解大法 + binary search + fiddler大量打request reproduce問題, 最後找出問題點是curl每打一次request就都會有memory leak發生, 而且這問題是發生在OpenSSL從1.0.2升級到1.1.1後, 也只有https的request才會有leak, http request不會有memory leak問題。

試到這邊也只能知道, 一樣的code在OpenSSL 1.1.1會有leak的問題, 而且OpenSSL 1.0.2只maintain到去年底, 所以還是得找出為什麼會Memory Leak, 就寫了個小測試程式, 掛上VLD測試, 以下是測試結果:

  1. 直接在main裡面用libcurl測試打http & https request, 都沒有memory leak發生
  2. 建thread並在裡面用libcurl測試打http & https request, 只有https會發生memory leak (就算只建一個thread測試一樣會發生, 所以不是thread safe問題, 看起來問題點是只要不是在main thread的話curl就會沒辦法正常釋放openssl的記憶體)
  3. 之後有嘗試不要用MT (static lib) build openssl改用MD (dll) build openssl, 就不會有memory leak問題

這樣測下來反而更玄了, 到底什麼bug才會符合上述所有情況, 後來我就猜該不會是我OpenSSL build壞了, 還是libCurl build壞導致它用完OpenSSL的resource沒有釋放掉。

後來乾脆直接去Curl的Github開issue, 直接抱Curl工程師的大腿了。
https://github.com/curl/curl/issues/5282

沒想到Curl工程師沒多久就立刻回覆了, 主要是說他們CI都有做memory leak的檢測, 像這種換linker導致leak的問題很奇怪, 因為這表示執行的是一模一樣的curl行為, 應該9成9不是curl的鍋。

好啦, 既然這樣只能繼續挖, 不過這樣感覺問題應該還是出在OpenSSL身上, 所以就繼續朝自己build壞OpenSSL的方向追, 不過試了不少方法還是沒結果, 最後終於找到有類似的問題在stackoverflow上(這issue明明就價值千金, 怎麼這麼冷門...)
https://stackoverflow.com/questions/60088679/c-openssl-1-1-1-running-rsa-algorithm-in-thread-causing-memory-leaks/60090384#60090384

解決方法就是, thread結束以前call一個API: OPENSSL_thread_stop()

測試了一下還真的解決, 超級傻眼, 這個API OpenSSL 1.0.2根本沒有, 是1.1.0之後才加的, 所以用之前的code跑才會有問題, 在去看官方的文件:
https://www.openssl.org/docs/man1.1.0/man3/OPENSSL_thread_stop.html

--------------------------

The OPENSSL_thread_stop() function deallocates resources associated with the current thread. Typically this function will be called automatically by the library when the thread exits. This should only be called directly if resources should be freed at an earlier time, or under the circumstances described in the NOTES section below.
The OPENSSL_INIT_LOAD_CONFIG flag will load a default configuration file. For optional configuration file settings, an OPENSSL_INIT_SETTINGS must be created and used. The routines OPENSSL_init_new() and OPENSSL_INIT_set_config_appname() can be used to allocate the object and set the application name, and then the object can be released with OPENSSL_INIT_free() when done.
--------------------------

--------------------------

NOTES

Resources local to a thread are deallocated automatically when the thread exits (e.g. in a pthreads environment, when pthread_exit() is called). On Windows platforms this is done in response to a DLL_THREAD_DETACH message being sent to the libcrypto32.dll entry point. Some windows functions may cause threads to exit without sending this message (for example ExitProcess()). If the application uses such functions, then the application must free up OpenSSL resources directly via a call to OPENSSL_thread_stop() on each thread. Similarly this message will also not be sent if OpenSSL is linked statically, and therefore applications using static linking should also call OPENSSL_thread_stop() on each thread. Additionally if OpenSSL is loaded dynamically via LoadLibrary() and the threads are not destroyed until after FreeLibrary() is called then each thread should call OPENSSL_thread_stop() prior to the FreeLibrary() call.
On Linux/Unix where OpenSSL has been loaded via dlopen() and the application is multi-threaded and if dlclose() is subsequently called prior to the threads being destroyed then OpenSSL will not be able to deallocate resources associated with those threads. The application should either call OPENSSL_thread_stop() on each thread prior to the dlclose() call, or alternatively the original dlopen() call should use the RTLD_NODELETE flag (where available on the platform).
--------------------------
簡單的說就是, 從OpenSSL 1.1.0開始, OpenSSL會開始自動分配thread資源, 可是根據某些情況你必須自己釋放, 以windows來說, 如果你的OpenSSL是build成MD DLL, 那除了某些特定情況需要自己手動釋放之外, OpenSSL會在thread結束時自動釋放記憶體; 而如果你的OpenSSL是build成MT static lib, 那你就必須自己在thread結束時自己call OPENSSL_thread_stop()主動釋放記憶體。 而Unix / Linux的情況則是相反, 大多情況OpenSSL都會在thread結束時自動釋放記憶體, 除非你動態載入openssl, 然後又提早unload它, 那你就必須自己主動釋放記憶體。

看完整個傻眼, 怎麼會有不同linker方式得寫不同的code處理, 這也太雷了吧..., , 找這bug花的時間搞不好比我直接去翻OpenSSL 1.1.0整本手冊還要長.....。 而且跟我實驗的結果還完全符合, 只是我結論的方向完全錯誤囧

這次分享的case就到這邊了, 希望我以後不要再遇到這麼噁心的bug / case, 或是遇到了剛好老天保佑能讓我快點找到解決方法...。

最後來個小推坑, 最近看完街角魔族了, 超級讚!!!  看完直接立刻重刷第二輪, 歡迎大家入坑XDD





沒有留言:

張貼留言