代碼靜態掃描規則——類型轉換檢查

  • 簡介

      在項目中,存在許多不規範的代碼,其一就是將無符號變量賦值給有符號變量。在大多數情況下是不會出現問題的,因爲那些變量值往往小於 2147483648
      但是一些特定的接口,如時間獲取接口,可能返回一個較大的無符號值,如果使用 int 變量接收,便可能出現異常。當這些接口在項目中大量使用時,排查起來較爲困難,容易發生遺漏,因此引入代碼掃描工具進行特定接口的使用檢查。
      後續將針對 TimeGet 函數進行問題的詳細說明。
  • TimeGet 接口聲明

    // 獲取時間
    // IN: 時間格式(如 TIME_YYMMDDHHMM)
    // OUT: 時間值(如 2105301530 -> 21年5月30日15點30分)
    unsigned TimeGet(TIME_TYPE type);
    
  • 問題表現

      在進入22年後,TIME_YYMMDDHHMM 格式的時間值便超出了 int 的表示範圍,如果誤用 int 變量進行接收,便可能出現如開始時間未到等各種問題。
  • 錯誤用法

    1. 使用 int 變量接收 TimeGet 返回值。如下代碼,在進入22年後,活動仍然處於未開始狀態。
      int nTime = TimeGet(TIME_YYMMDDHHMM);
      if (nTime < 2112125959)
      {
          ShowMessage("活動未開始。");
          return ;
      }
      
    2. 格式串中使用 %d 接收 TimeGet 返回值。如下代碼,在進入22年後,會將整張表讀取到內存,實際上需要的可能只有三五條。
      char szSQL[1024];
      sprintf(szSQL
          , "select * from tbl where out_time > %d"
          , TimeGet(TIME_YYMMDDHHMM));
      auto result = database()->executeQuery(szSQL);
      
    3. 通用的數據庫記錄對象可能只提供了 GetIntSetInt 接口,在表字段類型爲 unsigned 時,調用 SetInt( TimeGet(TIME_YYMMDDHHMM) ) 是不會出現問題的,但是需要小心使用 GetInt 接口。如下代碼就將問題藏得很隱蔽,實際上,GetInt 返回值已經是個負數了,直接賦值給 long long 變量,i64Value 仍然是個負數。
      long long i64Value = data.GetInt(start_time);
      
      這種情況下,加個類型轉換就能解決問題。
      long long i64Value = (unsigned)data.GetInt(start_time);
      
  • 排查難點

    1. TimeGet 在代碼中使用得很頻繁,直接搜索 TimeGet(TIME_YYMMDDHHMM) 會出現大幾百行。
    2. 有可能存在 TimeGet 接收時無誤,但後續傳遞過程中出現錯誤的情況,如下代碼,Func 函數正確處理了 TimeGet 的返回值,但外部使用 Func 卻出現了錯誤。
      unsigned FuncX()
      {
          return TimeGet(TIME_YYMMDDHHMM);
      }
      
      int main()
      {
          int nTime = FuncX();
          //...
          return 0;
      }
      
  • 代碼靜態掃描工具

      考慮到人工排查的困難,這裏引入cppcheck 並自定義規則進行代碼的掃描,通過工具輔助,來度過22年時間溢出帶來的危機。
  • 掃描規則

    1. 確定合法的接收類型:
      unsigned
      unsigned &
      unsigned long
      unsigned long &
      unsigned long long
      unsigned long long &
      signed long long
      signed long long &  
      
    2. 定位到所有 TimeGet(TIME_YYMMDDHHMM) 調用位置。
    3. 查找 TimeGet 返回值的接收者,爲了方便理解,這裏直接描述爲向前尋找接收對象(實際實現上使用cppcheck 的語法分析樹查找)。接收者存在如下幾種情況:
      1. 變量賦值
        int nTime = TimeGet(TIME_YYMMDDHHMM);
        
        這裏向前查找會遇到賦值符號(可能是=、+=、-=、|=等等),這說明 TimeGet 將會賦值給某個變量,這時可以檢查變量的類型是不是合法的。
      2. 函數返回
        int func()
        {
            return TimeGet(TIME_YYMMDDHHMM);
        }
        
        這裏向前查找會遇到 return,這說明 TimeGet 返回值將通過函數進一步返回,這時可以檢查函數的返回值類型是不是合法的。
      3. 函數傳參
        func( TimeGet(TIME_YYMMDDHHMM) );
        
        這裏向前查找會遇到 func(,這說明 TimeGet 返回值將傳遞給func的參數,這時檢查對應函數的參數類型。
      4. 構造
        1. 匿名構造
          struct User
          {
              User(int nTime)
              {
                  //...
              }
          };
          
          int main()
          {
              func ( User( TimeGet(TIME_YYMMDDHHMM) ) );
              // ...
          }
          
          這裏向前查找會遇到 User(,此處的 User 是一個類型名,這說明 TimeGet 返回值將傳遞給 User 的構造函數,這時檢查對應函數的參數類型。cppcheck 這裏並未直接將User代碼鏈接到 User 類的構造函數,而僅僅認爲此處的User 是一個類型,因此這裏需要自行根據傳入參數索引構造函數。
        2. 普通構造
          struct User
          {
              User(int nTime)
              {
                  //...
              }
          };
          
          int main()
          {
              User user(TimeGet(TIME_YYMMDDHHMM));
          }
          
          這裏向前查找會遇到 user(,此處的 user 是一個變量,這說明 TimeGet 返回值將傳遞給 user 變量所屬類型的構造函數,同樣的,這時需要檢查對應函數的參數類型。
        3. 標準數據類型構造
          int(TimeGet(TIME_YYMMDDHHMM))
          
          這裏向前查找會遇到 int(,此處的 int 是一個類型,但其屬於基本類型,無法找到其構造函數,此時應直接判斷類型是否合法。
      5. 取餘、比較
        int nHHMM = TimeGet(TIME_YYMMDDHHMM) % 10000;
        bool bZero = TimeGet(TIME_YYMMDDHHMM) == 0;
        
        這裏直接向前查找會發生誤判,認爲將時間賦值給 intbool 變量,但是使用語法分析樹判斷時,會先找到 %== 符號,這裏認爲返回值的性質已經發生了變化,則不應算是錯誤。
      6. 控制流
        if (TimeGet(TIME_YYMMDDHHMM))
        {
            // ...
        }
        
        這裏最終會查找到 if(,事實上這裏隱含了時間值與 0的判斷,可以認爲返回值性質發生了變化,不應算是錯誤。掃描工具中允許了 ifswitch 的控制流關鍵字,其他關鍵字(如 while)則輸出錯誤信息。
      7. 不定參函數
        char szSQL[1024];
        sprintf(szSQL
            , "select * from tbl where out_time > %d"
            , TimeGet(TIME_YYMMDDHHMM));
        
        這裏會查找到 sprintf(,但與普通的函數傳參不同,這裏的 sprintf 是不定參的,即無法正常檢查傳參類型是否合法。不定參函數過於複雜,目前版本只處理系統庫中格式串函數。
      8. 其他複雜情況
        func( { TimeGet(TIME_YYMMDDHHMM) } );
        array[0] = TimeGet(TIME_YYMMDDHHMM); 
        *(p + 1) = TimeGet(TIME_YYMMDDHHMM);
        \\ ...
        
        過於複雜的代碼,這裏暫不考慮,目前版本只適用於一般情況。
    4. 如果接收者的類型不合法,則可以簡單地輸出錯誤log 並結束該處 TimeGet 的檢查。如果接收者的類型合法,則需要進行遞歸檢查。遞歸檢查存在如下情況:
      1. 接收者爲變量
        unsigned uTime = TimeGet(TIME_YYMMDDHHMM);
        int nTime = uTime;
        
        此時,需要重新掃描變量的生效區域,針對 uTime 進行類似 TimeGet 返回值的檢查。值得注意的是,如果接收者是局部變量,則只要搜索當前塊即可,如果接收者是全局變量,則需要搜索全部代碼。
        目前版本未針對處理引用變量,如下問題工具無法掃描出來。
        unsigned uTime = 0;
        unsigned& rTime = uTime;
        rTime = TimeGet(TIME_YYMMDDHHMM);
        int nTime = uTime;
        
      2. 接收者爲成員變量
        struct User
        {
            unsigned nLoginTime;
        };
        
        void func(User& user)
        {
            user.nLoginTime = TimeGet(TIME_YYMMDDHHMM);
        }
        
        此時,爲了簡化邏輯,將遞歸檢查對象設定爲 User::nLoginTime,即所有 User 對象的 nLoginTime 都視爲檢查目標,不關心是否真的傳遞過 TimeGet 返回值。
      3. 接收者爲函數返回值
        unsigned func()
        {
            return TimeGet(TIME_YYMMDDHHMM);
        }
        
        此時,需要檢查 func 所有調用位置。
      4. 接收者爲函數參數
        void func(unsigned uTime)
        {
            int nTime = uTime;
        }
        func(TimeGet(TIME_YYMMDDHHMM));
        
        此時,需要檢查 func 參數列表中的 uTime 變量。
  • 標籤功能

      基於cppcheck 的框架,掃描時並沒有一份全部代碼的符號庫,而是遍歷掃描每一個cpp 文件,同時間僅有當前掃描cpp 文件的完整內容,及其關聯頭文件的函數聲明等。這意味着,發現向某函參傳遞 TimeGet 時,如果該函數體未被cppcheck 載入分析,此時只能判斷參數類型是否合法,無法跟蹤函數參數後續的使用是否合法。
      因爲上述問題,引入標籤功能,對於當次運行無法掃描的功能,記錄標籤寫入配置文件,下次運行cppcheck 時讀取標籤文件進行掃描。
      標籤類型如下:
    1. 初始函數 [INIT_FUNCTION],即本例中的 TimeGet
      標籤類型;函數標識;追查堆棧;忽略參數
      [INIT-FUNCTION];TimeGet(signed int type) at /project8/base.h;;
      
      忽略參數可用於控制只檢查個別時間格式,如-1 |0 TIME_SECOND|0 TIME_DAY 表示不檢查 TimeGet()TimeGet(TIME_SECOND)TimeGet(TIME_DAY) 的使用。
    2. 普通函數 [FUNCTION],即出現 return TimeGet 並且返回值類型合法的函數。與 [INIT_FUNCTION] 的區別在於後續掃描未增加該標籤,則會被清除(如函數丟失或函數的 return 語句不再返回 TimeGet 等)。
      標籤類型;函數標識;追查堆棧
      [FUNCTION];Test() at /project8/main.cpp;$G_TimeGet at (/project8/main.cpp, 76)
      
    3. 函數參數 [FUNCTION-ARGUMENT],即出現 func(TimeGet) 並且 func 參數類型合法的情況。
      標籤類型;函數標識;參數編號;參數類型|參數名;追查堆棧
      [FUNCTION-ARGUMENT];func(unsigned int uTime) at /project8/base.h;0;unsigned int|uTime;$G_TimeGet at (/project8/main.cpp, 83)
      
    4. 變量 [VARIABLE],即出現 user.time = TimeGet 並且變量類型合法的情況。
      標籤類型;變量聲明的文件路徑|變量歸屬|變量類型|變量名;追查堆棧
      [VARIABLE];/project8/base.h|User|unsigned int|time;$G_TimeGet at (/project8/main.cpp, 83)
      
      結合上述的標籤,便可以通過cppcheck 實現跨cpp 文件的檢查。
  • 源碼地址

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