【openssl】從openssl的常用接口調用淺談【內存泄漏】的風險和規避

    openssl是一個很有名的開源軟件,它在解決SSL/TLS通訊上提供了一套行之有效的解決方案,同時在軟件算法領域,它也集成絕大部分常見的算法,真可謂是程序員開發網絡通訊和信息安全加解密的一個利器。

       熟悉github的朋友,一定在github上目睹過openssl的真容【https://github.com/openssl/openssl】,它的官網地址是【https://www.openssl.org】。就拿github來說,高達8.8K顆星,被fork4千多次,總共有2萬多次的提交記錄,足以可見該開源項目的熱度有多高。

 

        然而,就是這樣的一個開源利器,能給我們工作帶來便利的同時,倘若你使用不當,那麼給你帶來的不是喜悅,而是煩惱。通過觀察openssl提供的API,你會發現,它的很多API返回的都是指針類型,在應用層調用時,僅需用一個對應類型的指針去接收返回的指針,即可取得對應的數據或操作方法,使用非常靈活。類似這樣的接口有很多,例如:

//新生成一個BIGNUM結構
BIGNUM *BN_new(void);

//將s中的len位的正整數轉化爲大數
BIGNUM *BN_bin2bn(const unsigned char *s, int len, BIGNUM *ret); 
 
//初始化一個RSA結構
RSA * RSA_new(void);
 
//RSA私鑰產生函數
//產生一個模爲num位的密鑰對,e爲公開的加密指數,一般爲65537(0x10001)
RSA *RSA_generate_key(int num, unsigned long e,void (*callback)(int,int,void *), void *cb_arg);
 
//從文件中加載RSAPublicKey格式公鑰證書
RSA *PEM_read_RSAPublicKey(FILE *fp, RSA **x, pem_password_cb *cb, void *u);
 
//從BIO重加載RSAPublicKey格式公鑰證書
RSA *PEM_read_bio_RSAPublicKey(BIO *bp, RSA **x, pem_password_cb *cb, void *u);

 

       聰明的你,留意到這些“生成”功能的API接口的同時,一定也留意到它們都有對應的“銷燬”API接口。上面列表一一對應的是:

//釋放一個BIGNUM結構,釋放完後a=NULL;
void BN_free(BIGNUM *a);

//釋放一個RSA結構
void RSA_free(RSA *rsa);

       看到這裏,也許你就會明白我今天要講的主題了,既然這些“生成”API提供了返回指針類型的功能,那麼很明顯指針所指向內容的存儲空間,必定是在openssl內部通過malloc等動態內存申請的方式獲取的;所以在使用了這段內存後,自然而然就是要執行內存釋放的動作,這與C語言動態內存申請講的“malloc--free”必須配套使用,是如出一轍的;只不過,現在這些openssl的API是把malloc和free的動作封裝在了接口的內部,而暴露給調用者的只有類型XXX_init和XXX_free,或者XXX_new和XXX_delete,諸如此類的接口,僅此而已。

       回到openssl的API接口的使用上來,博主有一次在使用openssl的某個接口有些疑惑,想在網上找找調用的demo時,結果一搜,一眼就進到 【OpenSSL編程-RSA編程詳解 http://www.qmailer.net/archives/216.html】這篇博文。它確實給初學者提供了幾組常用API的簡單demo,正常情況下這些代碼都是能跑通的,但近來我在日常工作中,有在做一些【內存泄露】相關的【代碼優化】工作,所以對這個切入點比較敏感,果不其然,細讀其中的部分示例代碼,就發現了其中不嚴謹的地方,很有可能就會發生【內存泄露】的風險。如果本身系統的內存比較吃緊,比如像在嵌入式系統上運行,這樣的【內存泄露】可以說是致命的。

       還是拿代碼來說事,以下代碼片段是上文提及的參考博文中截取到的,如下:

1. 數據加、密解密示例

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<openssl/rsa.h>
#include<openssl/pem.h>
#include<openssl/err.h>
 
#define PRIKEY "prikey.pem"
#define PUBKEY "pubkey.pem"
#define BUFFSIZE 4096
 
/************************************************************************
 * RSA加密解密函數
 *
 * file: test_rsa_encdec.c
 * gcc -Wall -O2 -o test_rsa_encdec test_rsa_encdec.c -lcrypto -lssl
 *
 * author: [email protected] by www.qmailer.net
 ************************************************************************/
 
char *my_encrypt(char *str, char *pubkey_path)
{
    RSA *rsa = NULL;
    FILE *fp = NULL;
    char *en = NULL;
    int len = 0;
    int rsa_len = 0;
 
    if ((fp = fopen(pubkey_path, "r")) == NULL) {
        return NULL;  //函數出口1
    }
 
    /* 讀取公鑰PEM,PUBKEY格式PEM使用PEM_read_RSA_PUBKEY函數 */
    if ((rsa = PEM_read_RSAPublicKey(fp, NULL, NULL, NULL)) == NULL) {
        return NULL; //函數出口2
    }
 
    RSA_print_fp(stdout, rsa, 0);
 
    len = strlen(str);
    rsa_len = RSA_size(rsa);
 
    en = (char *)malloc(rsa_len + 1);
    memset(en, 0, rsa_len + 1);
 
    if (RSA_public_encrypt(rsa_len, (unsigned char *)str, (unsigned char*)en, rsa, RSA_NO_PADDING) < 0) {
        return NULL; //函數出口3
    }
 
    RSA_free(rsa);
    fclose(fp);
 
    return en; //函數出口4
}
 

    通過簡單分析,我們可以知道my_encrypt這個函數,有一個入口,但是有4個出口(見代碼註釋):

函數出口1: 很明顯出錯的可能性是,打開pubkey_path文件失敗,這個很好理解,可能是文件不存在,或者路徑文件不正確等等,此處出錯,對外返回NULL,是完全沒有問題的

函數出口2: 這裏出錯的可能性是fp指向的pubkey_path文件,壓根不是一個pem格式的公鑰文件,自然會出錯;但是此處直接對外返回NULL,而不管fp的死活,這是不可取的!

函數出口3: 這裏出錯的可能是公鑰加密輸入的數據長度不對或者數據填充不對等等,然而這裏也是出錯後,立即對外返回NULL,完全不理fp和rsa,還有en這條友【往往更容易忽略】,這3者的死活,更是不可取的!

函數出口4: 這個沒的說,正常的函數出口;大家注意,在這個正常的函數出口中,它在return前是執行了 RSA_free(rsa); fclose(fp); 的動作;沒錯,這個就是我們要講的使用完的內存要及時釋放,它的使用需要注意的關鍵點就在這裏。那麼如上可能出現內存泄露的代碼片段應該如何優化呢?直接貼上,優化後的示例代碼:

char *my_encrypt(char *str, char *pubkey_path)
{
    RSA *rsa = NULL;
    FILE *fp = NULL;
    char *en = NULL;
    int len = 0;
    int rsa_len = 0;
 
    if ((fp = fopen(pubkey_path, "r")) == NULL) {
        en = NULL;
        goto exit_entry; //使用goto語句,保證函數單一入口,單一出口
    }
 
    /* 讀取公鑰PEM,PUBKEY格式PEM使用PEM_read_RSA_PUBKEY函數 */
    if ((rsa = PEM_read_RSAPublicKey(fp, NULL, NULL, NULL)) == NULL) {
        return NULL;
        goto exit_entry; //使用goto語句,保證函數單一入口,單一出口
    }
 
    RSA_print_fp(stdout, rsa, 0);
 
    len = strlen(str);
    rsa_len = RSA_size(rsa);
 
    en = (char *)malloc(rsa_len + 1);
    if (!en) {
        goto exit_entry; //當en申請不到內存的時候,也不能往下執行了,需要退出
    }
    memset(en, 0, rsa_len + 1);
 
    if (RSA_public_encrypt(rsa_len, (unsigned char *)str, (unsigned char*)en, rsa, RSA_NO_PADDING) < 0) {
        if (en) { 
            free(en);   //走到這裏的時候en理論上已經不爲空了,當在這一步出錯時,對外en的內存已經變得無意義了,所以必須要釋放掉,同時將en置爲NULL,防止外部調用者邏輯出錯
            en = NULL;
        }
        goto exit_entry;
    }
 
exit_entry: //函數統一出口,退出前執行相應的內存釋放動作

    //先判斷是否需要執行內存釋放
    if (rsa) {
        RSA_free(rsa);
    }
    //文件打開的fp句柄要及時關閉
    if (fp) {
        fclose(fp);
    } 

    return en;
}
 

       通過如上的示例代碼,基本上很好地修復了因異常情況處理不當導致的【內存泄露】隱患,同時利用goto語句,使得函數的結構的緊湊性有所提高,代碼的可讀性也提升了不少。

       有的朋友可能會有疑問,“我們在學C語言教程的時候,老師不是常常跟我們說,儘量不要使用goto語句,這樣會帶來代碼災難,爲何博主卻推薦使用goto語句來優化代碼呢?”

       原因很簡單,C語言的goto語句並不會造成代碼災難,而是使用goto語句的程序員造成的災難!怎麼說呢,goto語句是有點偏彙編層面的關鍵字,它有點像彙編指令裏面的jump指令,也就是說使用好它,指不定還可以提升代碼的運行效率。但是值得注意的是,goto語句不能濫用,尤其是使用goto語句往前跳轉,或者使用goto語句執行遞歸、循環等操作時,代碼的邏輯將有可能變得不可控制,或者難以控制,基本上除了寫代碼本身的人能讀懂外【估計過個一兩個月,他自己也讀不懂了】,其他人估計就摸不着頭腦了。但是,如果像用在如上所優化的代碼那樣,僅僅在函數的出口做一個symbol標籤,當函數中間執行異常的時候,立即跳轉到定義的出口標籤,同時執行一些函數退出的收尾工作,比如清理內存、釋放不再使用的內存、接口返回值轉換等操作;這樣的代碼,將會大大提升了代碼的可讀性,這也儘可能地將錯誤規避掉,讓bug無處藏身,代碼更加整潔,反而能編寫出可讀性強的高質量代碼。 

       如上僅是提出了對【內存泄露】的小小看法和感悟,藉助openssl的demo例子,也僅僅是拋磚引玉,也許讀者有更高的見解。期待有讀者與我一同討論相關話題。

注:文中引用了【博主:大佟,文章地址:http://www.qmailer.net/archives/216.html】的示例代碼,如有版權問題,請及時與我聯繫。不勝感激!

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