代码静态扫描规则——类型转换检查

  • 简介

      在项目中,存在许多不规范的代码,其一就是将无符号变量赋值给有符号变量。在大多数情况下是不会出现问题的,因为那些变量值往往小于 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. 确定合法的接收类型:
      unsignedunsigned &unsigned longunsigned long &unsigned long longunsigned long long &signed long longsigned 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 文件的检查。

  • 源码地址

原文链接: https://www.cnblogs.com/hchlqlz-oj-mrj/p/15809238.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍

    代码静态扫描规则——类型转换检查

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/185963

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年2月12日 上午11:02
下一篇 2023年2月12日 上午11:02

相关推荐