tgwadm接入coverity告警案例分享

本文適合不瞭解代碼靜態檢查的初學者或者猶豫是否將項目接入靜態檢查的人閱讀

tgwadm是雲網關組轉發模塊的agent組件,其主要功能有配置下發、與報文轉發進程交互等功能。最新版本由於將其重構,代碼變化很大,單純靠有限時間內的人肉測試以及codereview,可以發現的問題有限,故嘗試接入公司內部代碼檢查平臺來檢查代碼中的比如死鎖、內存問題等常見但難以發現的問題。

靜態代碼分析

靜態代碼分析是指無需運行被測代碼,通過詞法分析、語法分析、控制流、數據流分析等技術對程序代碼進行掃描,找出代碼隱藏的錯誤和缺陷,如參數不匹配,有歧義的嵌套語句,錯誤的遞歸,非法計算,可能出現的空指針引用等等。統計證明,在整個軟件開發生命週期中,30% 至 70% 的代碼邏輯設計和編碼缺陷是可以通過靜態代碼分析來發現和修復的。
在C++項目開發過程中,因爲其爲編譯執行語言,語言規則要求較高,開發團隊往往要花費大量的時間和精力發現並修改代碼缺陷。所以C++ 靜態代碼分析工具能夠幫助開發人員快速、有效的定位代碼缺陷並及時糾正這些問題,從而極大地提高軟件可靠性並節省開發成本。
靜態代碼分析工具的優勢 :

  • 自動執行靜態代碼分析,快速定位代碼隱藏錯誤和缺陷。
  • 幫助代碼設計人員更專注於分析和解決代碼設計缺陷。
  • 減少在代碼人工檢查上花費的時間,提高軟件可靠性並節省開發成本。

在這裏插入圖片描述

帶着問題來看:靜態分析找到的Bug中,有多少是能夠在Code Review(代碼審查)、自驗證或系統測試過程被發現的?

本組項目使用的是公司內部的代碼檢查平臺,包括Coverity、Klocwork、PyLint等檢查工具,支持的工具列表如下:

在這裏插入圖片描述

由於本組主要開發語言爲C/C++,經過一段時間試用,綜合對比後,個人認爲最適合本組而且又最好用的就是Coverity。

Coverity

Coverity公司是由一流的斯坦福大學的科學家於2002年成立的,產品核心技術是1998年至2002年在斯坦福大學計算機系統實驗室開發的,用於解決一個計算機科學領域最困難的問題,在2003年發佈了第一個能夠幫助Linux、FreeBSD等開源項目檢測大量關鍵缺陷的系統,Coverity是唯一位列IDC前10名軟件質量工具供應商的靜態分析工具廠商,被VDC評爲靜態源代碼分析領域的領導者。

Coverity做了近15年,才基本做到精確(基本的意思是還有大概15%的誤報率,當然這個數字在業內可以說是頂尖的)。

Coverity主要從以下四個方面來對代碼進行分析

  • 編譯器告警
  • 編碼規範
  • 靜態分析
  • 數據流分析

具體數據可以參考WeTest團隊的一篇《C++代碼質量掃描主流工具深度比較》文章,地址看最後的參考

下面看下使用Coverity掃描本項目的告警情況,用數據說話

靜態檢查告警案例

靜態檢查問題總覽

優先級分類:

在這裏插入圖片描述

嚴重缺陷分類:

在這裏插入圖片描述

一般缺陷分類:

在這裏插入圖片描述

解決的典型問題及分析:
(1) RESOURCE_LEAK 查找程序可能存在的內存泄漏、指針泄露等問題,避免系統崩潰;
(2) OVERRUN 檢測內存越界訪問等問題。它可以檢測堆緩衝區和棧緩衝區的越界情況。內存越界問題很嚴重。
(3) UNINIT 查找使用變量/對象時存在的未初始化的情況,使用未初始化的變量可能會導致無法預測的行爲或程序崩潰;
(4) UNINIT_CTOR 未初始化的成員變量,風險與UNINIT類似;
(5) CHECKED_RETURN 檢測代碼忽略處理系統調用返回的錯誤代碼等情況,忽略返回的函數錯誤代碼並假設運算成功可能導致異常。
(6) MISSING_BREAK 查找 switch 語句中缺少 break 語句的情況,缺少break可能會導致不可預測的行爲,增加代碼理解難度,而且在後續代碼維護過程中容易出錯。
(7) NEGATIVE_RETURNS 查找濫用負整數的情況,負整數和可能爲負的函數返回值在使用(例如作爲數組索引、循環邊界、代數表達式變量或系統調用的大小/長度參數)之前必須進行檢查。濫用負整數可能導致內存損壞、進程崩潰、無限循環、整數溢出和安全缺陷。比如使用負值當做數組下標。
(8) UNREACHABLE 檢測代碼中的DEAD CODE,不會跑到的代碼增加後續維護成本,且由於一直無法運行,其可用性未知。
(9) SLEEP 檢測在持有鎖/互斥鎖時調用了sleep函數的情況。這會導致其他線程無法獲得該鎖,可能導致死鎖或者性能下降。
(10) NULL_RETURNS 檢測C/C++代碼中指針或引用是否可能會爲NULL值,檢測是否會存在操作空指針操作。
(11) CONSTANT_EXPRESSION_RESULT 檢測常量中的運算符混淆、優先級混淆、類型大小或者複製/粘貼錯誤等問題。
(12) SIZEOF_MISMATCH 和 BAD_SIZEOF 檢測不匹配的sizeof操作或者memset時不匹配的對象大小,可能會導致memset會不足或者memset越界。

下面挑幾個比較經典的錯誤拿出來講一講

案例1(內存泄露)

int CBinlogMgr::BinlogSetMirIPRecover(map<IPAddr, MirrorIP *> *ip_list,
    char *curr)
{
    int i, num;
    be32 ip;
    MirrorIP *mip = new MirrorIP;

    ip_list->clear();
    GETINT(num);
    for(i = 0; i < num; ++ i){
        mip = new MirrorIP;
        if(! mip){
            return -1;
        }
        GETBE32(ip);
        mip->addr = ip;
        ip_list->insert(make_pair(ip, mip));
    }

    return 0;
}

Coverity案例
RESOURCE_LEAK 查找程序沒有儘快釋放系統資源的情況。沒有釋放所需資源的應用程序可能面臨性能降級、崩潰、拒絕服務或無法成功獲取指定資源
這段代碼聲明mip變量時,申請了一段內存,在後續循環中,還未釋放相應內存便讓mip變量指向了新申請的內存。
影響分析
內存泄露
修復方法
刪除變量定義代碼中,MirrorIP *mip = new MirrorIP;中的申請內存部分。

案例2(內存泄露)

int Config::SetSgList(ReqTVSAdminSetSgList *req,
    RspTVSAdminSetSgList *rsp)
{
    SgRuleList *sg_list;
    SgRule *sg, *rsp_sg;
    int success, total, ret;
    ...

    asn_list_for_each(sg_list, sg) 
    {
        ++ total;
        ...
        
        TvsSecureGroup *tvssg = new TvsSecureGroup(sg);
        if (!tvssg)
        {
            LOG("alloc Sg rule %s failed.\n", (char *)sg->sgid);
            continue;
        }
        ret = g_stAclMgr.SetAclToUmod(tvssg);
        if (ret) 
        {
            LOG("Set Sg rule %s failed.\n", tvssg->id.c_str());
        } 
        else 
        {
            ++ success;
            g_stAclMgr.SaveAclToAdmin(tvssg->id, tvssg);
            g_stBinlogMgr.BinlogSetSg(tvssg);
        }
        ...
    }
    ...
    return 0;
}

Coverity案例
屬於RESOURCE_LEAK問題
本代碼在循環中,在ret = g_stAclMgr.SetAclToUmod(tvssg);if (ret)分支中,若是false分支,會將tvssg指向的內存放入一個隊列,會在其他位置釋放;但若進入另一個分支,則不會將tvssg指向的內存釋放,在下一個循環中,tvssg指向新的內存,導致內存泄露
影響分析
內存泄露
修復方法
在異常分支增加釋放內存邏輯

案例3(變量未初始化)

int Config::SetFldList(
    ReqTVSAdminSetFldList *req, RspTVSAdminSetFldList *rsp)
{
    ...
    int op_type, end_ret, del_count, count, set_count;
    ...
    if (FLD_OP_UNBAN == op_type) {
        ...
        for (list_iter = m_tmpDelFldList.begin();
            list_iter != m_tmpDelFldList.end(); ++ list_iter) {
            ret = DelFldListFromUmod(*list_iter);
            if (ret) {
                ...
                goto FAILED;
            } 
            ++ del_count;
        }
        SetFldFlagToUmod(m_tmpDelFldList, DEL);
    }
    ...
    set_count = 0;
    ...

FAILED:
    ...
    count = 0;
    for (iter = m_tmpModifyFldMap.begin();
        iter != m_tmpModifyFldMap.end() && count < set_count; ++ iter) {
        
        ...
        ++ count;
    }
    ...
}

Coverity案例
該錯誤也是變量未初始化錯誤,但爲什麼把該錯誤當做典型舉出來?
雖然,變量沒有初始化cpplint也可以檢查出來,但是cpplint很容易誤報,而且要求很嚴,cpplint是直接掃描代碼,只要它發現變量未初始化就會給你報錯。
但是coverity不一樣,它會分析所有可能的值,然後根據代碼邏輯遍歷所有分支,分析是否存在使用未初始化值的情況,所以相較於cpplint更爲精確。
在這段代碼裏面,可能最初編寫的代碼並沒有這個問題,隨着後續對該代碼的修改,在原有的邏輯上增加了更多的邏輯以及跳出清理資源邏輯後,如果編碼不夠嚴謹規範(即聲明時即初始化),就會很容易產生這樣的邏輯問題,即可能一個值還沒有進行初始化,便跳出了,但在異常處理邏輯中還用到了該未初始化的值,導致更多的異常。
set_count在初始化爲0之前,存在可能跳到異常邏輯,在異常邏輯中用到了該值作爲循環,由於它未初始化,是一個不確定的值,導致循環異常,程序崩潰。
影響分析
set_count在初始化爲0之前,存在可能跳到異常邏輯,在異常邏輯中用到了該值作爲循環,由於它未初始化,是一個不確定的值,導致循環異常,程序崩潰。
修復方法
聲明set_count時進行初始化

案例4(內存越界)

int CIfManager::SendARP(int if_idx, unsigned char* hwaddr, string &src, unsigned int dst)
{
    struct ethhdr* eth;
    struct arphdr* arph;
    char buffer[42];
    char* arp_ptr;
    int af;
    unsigned int vip;
    in6_addr addr6;

    af = get_ip_af(src.c_str());
    /*build ethernet header*/
    eth = (struct ethhdr*)buffer;
    ...
    
    /*build arp header*/
    arph = (struct arphdr*)(buffer + sizeof(struct ethhdr));
    ...
    
    arp_ptr = (char*)(arph + 1);
    memcpy(arp_ptr, hwaddr, 6);
    arp_ptr += 6;
    if (AF_INET == af)
    {
        vip = inet_addr(src.c_str());
        memcpy(arp_ptr, &vip, sizeof(vip));
        arp_ptr += sizeof(vip);
    }
    else
    {
        int ret = get_ipv6_addr(src.c_str(), (struct gw_in6_addr *)&addr6);
        if (ret)
        {
            ...
            return -1;
        }
        memcpy(arp_ptr, &addr6, sizeof(addr6));
        arp_ptr += sizeof(addr6);
    }
    memset(arp_ptr, 0x0, 6);
    arp_ptr += 6;
    memcpy(arp_ptr, &dst, sizeof(dst));
    ...
    return 0;
}

Coverity案例
OVERRUN 可查找越界訪問緩衝區的很多情況。不當的緩衝區訪問可能損壞內存,導致進程崩潰、安全漏洞和其他嚴重的系統問題。OVERRUN 可查找到堆緩衝區和棧緩衝區的越界索引。
這段代碼對buffer進行操作,填充ARPheader字段。
buffer本身長度爲42字節,char buffer[42];
第一處偏移arph = (struct arphdr*)(buffer + sizeof(struct ethhdr));,arph現在指向buffer的字節14
第二處偏移arp_ptr = (char*)(arph + 1);;arp_ptr現在指向buffer的字節22
第三處偏移arp_ptr += 6;,arp_ptr現在指向buffer的字節28
需要進入ipv6分支,即if (AF_INET == af)的else分支,第四處偏移memcpy(arp_ptr, &addr6, sizeof(addr6));arp_ptr += sizeof(addr6);,此處已經產生了內存越界,此時arp_ptr偏移在44
第五處偏移

    memset(arp_ptr, 0x0, 6);
    arp_ptr += 6;
    memcpy(arp_ptr, &dst, sizeof(dst));

這裏偏移了6個字節和4個字節,arp_ptr已經偏移到54字節
影響分析
若從src獲取出來的IP協議類型不是ipv4的話,便會發生內存越界錯誤,可能導致進程崩潰或者其他嚴重問題。
修復方法
將buffer大小修改爲大於54字節,此處修改爲60字節大小

案例5(無效的sizeof)

int Config::ReadBanFldList()
{
    int ret;
    char *fld_buffer;
    ...
    fld_buffer = (char *)malloc((MAX_FLD_NUM_PER_SVC + 1) * HTTP_HOST_LEN);
    ...
    ret = 0;
    for (vector<string>::const_iterator it = fld_list.begin();
        it != fld_list.end(); ++ it) {
        ...
        memset(fld_buffer, 0, sizeof((MAX_FLD_NUM_PER_SVC + 1) * HTTP_HOST_LEN));
        ...
        }
    ...
}

Coverity案例
BAD_SIZEOF 可報告在參數是可疑類別(例如對象的地址,通常應該是實際對象的大小)之一時使用 sizeof 運算符的情況。非正常大小值可能導致各種問題,例如分配不足或過量、緩衝區越界訪問、部分初始化或複製以及邏輯不一致。
fld_buffer大小爲(MAX_FLD_NUM_PER_SVC + 1) * HTTP_HOST_LEN,在memset時,用的是sizeof((MAX_FLD_NUM_PER_SVC + 1) * HTTP_HOST_LEN),錯誤使用了sizeof
影響分析
memset實際上算是未生效,只初始化了fld_buffer的4~8個字符
若後續邏輯代碼依賴於fld_buffer初始化,可能會有問題
修復方法
將代碼改爲memset(fld_buffer, 0, (MAX_FLD_NUM_PER_SVC + 1) * HTTP_HOST_LEN);

Coverity使用最佳實踐

這裏的最佳實踐是我從其他人的經驗分享整理得來,也與大家分享一下。

  • 1、每日凌晨自動掃描代碼,若有問題自動提單
  • 2、告警處理人查看新增告警,明顯問題直接修復,涉及到業務邏輯負責問題則交由相關模塊負責人負責修復。
  • 3、先進行codereview再進行提交

總結

通過前面的一些例子,發現了很多常見或疑難的錯誤,足以說明Coverity靜態檢查工具的功能強大。

確實,Coverity靜態檢查檢查出來的問題有些由於邏輯分支條件非常難以滿足,導致不會出現相應問題,不會觸發bug。但一旦進入,便要花費上十倍百倍的時間來複現定位。

開發流程中使用靜態檢查,確實可能會增加部分開發成本,但是在後續維護以及穩定性上,有絕對的好處;另一方面,有了靜態檢查來專注於檢查常見編碼錯誤,codereview便可以更加專注於業務邏輯,也算是變相提高了開發效率。

使用好的靜態檢查檢查工具來檢查編碼常見錯誤,解放codereview,讓codereview專注於業務邏輯檢查,提升效率
降低維護成本,提升程序穩定性

參考文章:

1、【代碼質量】C++代碼質量掃描主流工具深度比較
https://blog.csdn.net/wetest_tencent/article/details/51516347

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