代码静态扫描规则——类型转换检查
-
简介
在项目中,存在许多不规范的代码,其一就是将无符号变量赋值给有符号变量。在大多数情况下是不会出现问题的,因为那些变量值往往小于2147483648
。
但是一些特定的接口,如时间获取接口,可能返回一个较大的无符号值,如果使用int
变量接收,便可能出现异常。当这些接口在项目中大量使用时,排查起来较为困难,容易发生遗漏,因此引入代码扫描工具进行特定接口的使用检查。
后续将针对TimeGet
函数进行问题的详细说明。 -
TimeGet 接口声明
// 获取时间 // IN: 时间格式(如 TIME_YYMMDDHHMM) // OUT: 时间值(如 2105301530 -> 21年5月30日15点30分) unsigned TimeGet(TIME_TYPE type);
-
问题表现
在进入22年后,TIME_YYMMDDHHMM
格式的时间值便超出了int
的表示范围,如果误用int
变量进行接收,便可能出现如开始时间未到等各种问题。 -
错误用法
- 使用
int
变量接收TimeGet
返回值。如下代码,在进入22年后,活动仍然处于未开始状态。int nTime = TimeGet(TIME_YYMMDDHHMM); if (nTime < 2112125959) { ShowMessage("活动未开始。"); return ; }
- 格式串中使用
%d
接收TimeGet
返回值。如下代码,在进入22年后,会将整张表读取到内存,实际上需要的可能只有三五条。char szSQL[1024]; sprintf(szSQL , "select * from tbl where out_time > %d" , TimeGet(TIME_YYMMDDHHMM)); auto result = database()->executeQuery(szSQL);
- 通用的数据库记录对象可能只提供了
GetInt
和SetInt
接口,在表字段类型为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);
- 使用
-
排查难点
TimeGet
在代码中使用得很频繁,直接搜索TimeGet(TIME_YYMMDDHHMM)
会出现大几百行。- 有可能存在
TimeGet
接收时无误,但后续传递过程中出现错误的情况,如下代码,Func
函数正确处理了TimeGet
的返回值,但外部使用Func
却出现了错误。unsigned FuncX() { return TimeGet(TIME_YYMMDDHHMM); } int main() { int nTime = FuncX(); //... return 0; }
-
代码静态扫描工具
考虑到人工排查的困难,这里引入cppcheck 并自定义规则进行代码的扫描,通过工具辅助,来度过22年时间溢出带来的危机。 -
扫描规则
- 确定合法的接收类型:
unsigned unsigned & unsigned long unsigned long & unsigned long long unsigned long long & signed long long signed long long &
- 定位到所有
TimeGet(TIME_YYMMDDHHMM)
调用位置。 - 查找
TimeGet
返回值的接收者,为了方便理解,这里直接描述为向前寻找接收对象(实际实现上使用cppcheck 的语法分析树查找)。接收者存在如下几种情况:- 变量赋值
这里向前查找会遇到赋值符号(可能是=、+=、-=、|=等等),这说明int nTime = TimeGet(TIME_YYMMDDHHMM);
TimeGet
将会赋值给某个变量,这时可以检查变量的类型是不是合法的。 - 函数返回
这里向前查找会遇到int func() { return TimeGet(TIME_YYMMDDHHMM); }
return
,这说明TimeGet
返回值将通过函数进一步返回,这时可以检查函数的返回值类型是不是合法的。 - 函数传参
这里向前查找会遇到func( TimeGet(TIME_YYMMDDHHMM) );
func(
,这说明TimeGet
返回值将传递给func
的参数,这时检查对应函数的参数类型。 - 构造
- 匿名构造
这里向前查找会遇到struct User { User(int nTime) { //... } }; int main() { func ( User( TimeGet(TIME_YYMMDDHHMM) ) ); // ... }
User(
,此处的User
是一个类型名,这说明TimeGet
返回值将传递给User
的构造函数,这时检查对应函数的参数类型。cppcheck 这里并未直接将User
代码链接到User
类的构造函数,而仅仅认为此处的User
是一个类型,因此这里需要自行根据传入参数索引构造函数。 - 普通构造
这里向前查找会遇到struct User { User(int nTime) { //... } }; int main() { User user(TimeGet(TIME_YYMMDDHHMM)); }
user(
,此处的user
是一个变量,这说明TimeGet
返回值将传递给user
变量所属类型的构造函数,同样的,这时需要检查对应函数的参数类型。 - 标准数据类型构造
这里向前查找会遇到int(TimeGet(TIME_YYMMDDHHMM))
int(
,此处的int
是一个类型,但其属于基本类型,无法找到其构造函数,此时应直接判断类型是否合法。
- 匿名构造
- 取余、比较
这里直接向前查找会发生误判,认为将时间赋值给int nHHMM = TimeGet(TIME_YYMMDDHHMM) % 10000; bool bZero = TimeGet(TIME_YYMMDDHHMM) == 0;
int
或bool
变量,但是使用语法分析树判断时,会先找到%
或==
符号,这里认为返回值的性质已经发生了变化,则不应算是错误。 - 控制流
这里最终会查找到if (TimeGet(TIME_YYMMDDHHMM)) { // ... }
if(
,事实上这里隐含了时间值与 0的判断,可以认为返回值性质发生了变化,不应算是错误。扫描工具中允许了if
和switch
的控制流关键字,其他关键字(如while
)则输出错误信息。 - 不定参函数
这里会查找到char szSQL[1024]; sprintf(szSQL , "select * from tbl where out_time > %d" , TimeGet(TIME_YYMMDDHHMM));
sprintf(
,但与普通的函数传参不同,这里的sprintf
是不定参的,即无法正常检查传参类型是否合法。不定参函数过于复杂,目前版本只处理系统库中格式串函数。 - 其他复杂情况
过于复杂的代码,这里暂不考虑,目前版本只适用于一般情况。func( { TimeGet(TIME_YYMMDDHHMM) } ); array[0] = TimeGet(TIME_YYMMDDHHMM); *(p + 1) = TimeGet(TIME_YYMMDDHHMM); \\ ...
- 变量赋值
- 如果接收者的类型不合法,则可以简单地输出错误log 并结束该处
TimeGet
的检查。如果接收者的类型合法,则需要进行递归检查。递归检查存在如下情况:- 接收者为变量
此时,需要重新扫描变量的生效区域,针对unsigned uTime = TimeGet(TIME_YYMMDDHHMM); int nTime = uTime;
uTime
进行类似TimeGet
返回值的检查。值得注意的是,如果接收者是局部变量,则只要搜索当前块即可,如果接收者是全局变量,则需要搜索全部代码。
目前版本未针对处理引用变量,如下问题工具无法扫描出来。unsigned uTime = 0; unsigned& rTime = uTime; rTime = TimeGet(TIME_YYMMDDHHMM); int nTime = uTime;
- 接收者为成员变量
此时,为了简化逻辑,将递归检查对象设定为struct User { unsigned nLoginTime; }; void func(User& user) { user.nLoginTime = TimeGet(TIME_YYMMDDHHMM); }
User::nLoginTime
,即所有User
对象的nLoginTime
都视为检查目标,不关心是否真的传递过TimeGet
返回值。 - 接收者为函数返回值
此时,需要检查unsigned func() { return TimeGet(TIME_YYMMDDHHMM); }
func
所有调用位置。 - 接收者为函数参数
此时,需要检查void func(unsigned uTime) { int nTime = uTime; } func(TimeGet(TIME_YYMMDDHHMM));
func
参数列表中的uTime
变量。
- 接收者为变量
- 确定合法的接收类型:
-
标签功能
基于cppcheck 的框架,扫描时并没有一份全部代码的符号库,而是遍历扫描每一个cpp 文件,同时间仅有当前扫描cpp 文件的完整内容,及其关联头文件的函数声明等。这意味着,发现向某函参传递TimeGet
时,如果该函数体未被cppcheck 载入分析,此时只能判断参数类型是否合法,无法跟踪函数参数后续的使用是否合法。
因为上述问题,引入标签功能,对于当次运行无法扫描的功能,记录标签写入配置文件,下次运行cppcheck 时读取标签文件进行扫描。
标签类型如下:- 初始函数
[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)
的使用。 - 普通函数
[FUNCTION]
,即出现return TimeGet
并且返回值类型合法的函数。与[INIT_FUNCTION]
的区别在于后续扫描未增加该标签,则会被清除(如函数丢失或函数的return
语句不再返回TimeGet
等)。标签类型;函数标识;追查堆栈 [FUNCTION];Test() at /project8/main.cpp;$G_TimeGet at (/project8/main.cpp, 76)
- 函数参数
[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)
- 变量
[VARIABLE]
,即出现user.time = TimeGet
并且变量类型合法的情况。标签类型;变量声明的文件路径|变量归属|变量类型|变量名;追查堆栈 [VARIABLE];/project8/base.h|User|unsigned int|time;$G_TimeGet at (/project8/main.cpp, 83)
- 初始函数
-
源码地址