VS2019: 进一步新增了C++代码分析规则

VS2019: 进一步新增了C++代码分析规则

作者:BlogUpdater |  时间:2020-11-02 |  浏览:326 |  评论已关闭 条评论

官宣
在Visual Studio v16.8 Preview 3中,我们添加了一些新的安全编码规则到C++ Code Analysis中,开发者可以借助这些规则来发现一些比较常见的编码错误,这些错误看起来会不起眼,但是由此引发的Bug却十分难以追踪。这些编码规则都来自于真实运行的软件产品,在微软,每个项目都会执行这些编码规则检查,以确保软代码满足安全性和标准一致性的要求。

在之前的文章中,我们已经介绍了一些关于VARIANT的规则,包括VARIANT,VARIANTARG和PROPVARIANT等。

今天的文章将作为之前文章的一个延续,我们会介绍新的编码规则,主要是关于”枚举作为索引的用法”和”使用布尔值来作为HRESULT”这两种使用场景。
为了方便导入这些规则,我们创建了两个新的代码分析扩展,分别是EnumIndex和HResultCheck,它们可以用来检查上面提到的两个编码规则。
那么,我们开始吧。

使用枚举作为索引
枚举,可以看作是用户自定义的一个整数类型,它包含一个命名的整数常量集合。通常,我们也叫它为枚举常量。枚举主要使用在需要描述一段范围值的场景。
枚举的定义以一个enum关键字开始,后面可以接class或者struct关键字,如下所示:
enum class Suit { Diamonds, Hearts, Clubs, Spades };

我们也可以不加上class或者struct,这样的枚举称之为”Unscoped枚举”。

如果使用/std:c++17编译选项,则一个枚举可以使用一个显式的底层类型或者没有枚举器的形式来进行定义,由此可以引入一种新的,不能和其他类型进行隐式类型准换的整数类型。

Unscoped枚举可以被隐式转换为int类型,而Scoped枚举则不能被隐式转换为int,只能使用强制类型转换才能实现。类似的,一个int也只能通过强制类型转换来转为一个Scoped或者Unscoped枚举。

枚举是整数类型,通常包含一组有限的命名常量,可以将其隐式或显式转换为int,这一事实使得使用枚举数作为索引值非常普遍。例如下面的例子:
const auto& colorInfo = ColorTable[color];

在网上可以找到许多有关将枚举值作为数组索引的讨论。 在许多情况下,这确实很有意义。

通常,当开发人员将枚举类型的枚举器用作数组的索引时,他们知道枚举类型的枚举器的值从零开始到已知的最大值,且增量为1,并且任何一对连续的行之间没有间隔。
因此,大多数开发人员认为对照已知的最大值来检查枚举值将可以确保值的有效性。
但是,使用枚举器作为数组索引不是很安全。不幸的是,似乎没有太多关于为什么它可能很危险的讨论。
让我们看一个例子。考虑下面的枚举和一个函数指针表,我们要使用该枚举值作为索引:

现在,在源文件中,我们使用枚举的枚举数作为函数指针表的索引,定义一个从表中选择函数的工具函数:

上面的代码的逻辑看起来挺清晰的。为了调用者传入错误的参数,我们将枚举的值与FunctionId的已知最大值进行比较,以免导致函数访问超出其范围的表。 我们还知道,FunctionId枚举类型的枚举值将从零开始,以一个增量递增,并以[FunctionId::FunctionCount – 1]结束,FunctionCount是枚举中的最后一个枚举值。

让我们继续添加更多使用这个工具函数的代码。我们的客户代码将具有整数值作为函数的选择器,并希望我们通过函数返回整数值:

如上所述,我们需要进行强制转换,以将函数表索引的整数值转换为枚举类型,以传递给GetFunction。这将确保将int值正确转换为FunctionId枚举的枚举值。
希望到目前为止,一切都很好。
现在,让我们考虑这样一个调用GetValue函数:

在上面的代码中,-1是从哪里来的呢?
对于此讨论,这并不重要。假设它来自用户的输入。无论如何,这显然是错误的。但是,即使使用/ Wall编译选项,我们也不会从编译器那里获得任何有关此调用潜在问题的提示。实际上,考虑到所涉及的类型及其使用方式,没有什么是“错误的”。但是我们知道这是错误的。 GetFunction是否真的保护自己免受此调用的侵害?一个简单的答案是 – 不。

问题是,你可以将任何int值强制转换为枚举类型,并且枚举的基础类型默认为int值(带符号的int)。对于带符号的值,如果您检查上限但不检查其下限,则最终将允许使用负值。在上面的示例中,它最终调用了危险的DoNotCallMe函数,该函数恰好在函数指针表之前。在现实生活中,这种错误可能导致可利用的安全漏洞。

开发者检查索引下限的可能性较小,但忘记检查上限。但是,通过允许超出数组范围的访问,这也可能导致相同的问题。

只是为了好玩,运行上面的示例会为我产生以下输出:

EnumIndex扩展
EnumIndex扩展可以发现如上所示的潜在问题,并通过以下警告进行报告:
> C33010:未选中枚举”enum”的下限,用作索引。
> C33011:枚举”enum”的未选中的上限用作索引。

C33010警告
如果检查上限(而不是下限)的值,则将对用作数组索引的枚举触发此警告。
这是一个简化的例子:

还可以通过检查索引值的下限来更正这些警告:

C33011警告
如果检查了下限值而不是上限值,则将对用作数组索引的枚举触发此警告。
这是一个简化的例子:

通过检查索引值的上限也可以更正这些警告:

在Visual Studio中启用EnumIndex规则
通过为项目选择不同的规则集,可以按如下所示在Visual Studio中启用EnumIndex规则:

使用布尔值作为HRESULT
尽管可能不是故意的,但我们已经看到了将布尔值用作HRESULT值的代码,反之亦然。 C/C++允许在它们之间进行隐式转换,并且编译器不会对这些隐式转换发出警告。 但是,布尔值和HRESULT具有不同的语义,因此不能互换使用。

这就是为什么已有禁止这种滥用的规则的原因。 考虑以下示例:

foo()函数的目的是比较两个值,并在它们相等时返回S_OK。 但是,如果值相等,它将返回S_FALSE,如果值不同,则返回S_OK。 这与预期的行为完全相反。 但是,此代码可能会编译得很好,而不会收到有关此潜在缺陷的警告。 幸运的是,C++代码分析可以检测到此情况,并将报告C6216警告,这是有关将布尔值隐式转换为HRESULT的常规警告。

在布尔值和HRESULT值的各种潜在滥用中,我们了解到,一种特定情况比其他情况更常发生,并导致更明显的错误。 我们创建了一个额外的扩展来解决这种情况 – HResultCheck。

HResult规则
HResultCheck扩展发现从函数以HRESULT值返回C样式BOOL FALSE的情况,导致在意图可能返回失败结果时返回S_OK:
> C33020:检测到错误的HRESULT用法。
> C33022:检测到可能不正确的HRESULT用法(低置信度)。

C33020规则
这是一个高置信度警告,指示HRESULT返回函数返回FALSE。 在许多情况下,开发人员将FALSE视为失败值,并从函数中返回它以指示失败。 但是,FALSE的值为0。当解释为HRESULT值时,该值变为S_OK,表示成功。

这是一个简化的示例:

可以固定返回正确的HRESULT值:

C33022警告
如果返回最终结果的行上某处存在FALSE,则对于返回HRESULT的函数,这是低置信度警告。
这是一个简化的示例:

可以固定返回正确的HRESULT值:

在Visual Studio中启用HResult规则
可以通过为项目选择不同的规则集,在Visual Studio中启用HResult规则,如下所示:

最后
Microsoft Visual C++团队的博客是我非常喜欢的博客之一,里面有很多关于Visual C++的知识和最新开发进展。大浪淘沙,如果你对Visual C++这门古老的技术还是那么感兴趣,则可以经常去他们那(或者我这)逛逛。
本文来自:《Even More New Safety Rules in C++ Code Analysis》

标签:

相关推荐

评论已关闭。