Visual Studio 2022 版本 17.0 预览版 4 中改进的空指针取消引用检测

327 人浏览 | 时间: 2022-06-13 22:23:36 | 作者: news

C++ 静态分析团队致力于让您的 C++ 编码体验尽可能安全。我们正在添加更丰富的代码安全检查并解决发布在C++ 开发人员社区 页面上的高影响客户反馈错误。感谢您与我们互动,并就过去的版本和导致这一点的早期预览向我们提供了很好的反馈。以下是可以检测空指针取消引用错误的新实验代码分析检查的详细概述  ,以及与具有相同目的的现有检查的比较。


概述

在内部,我们有多个分析引擎。从用户的角度来看,这是不透明的;无论我们用来实现它们的引擎如何,警告都会以相同的方式出现。我们的代码分析工具之一有许多检查来捕获空指针取消引用错误。这些包括C6011、C6387和C28196。虽然这些警告在历史上是成功的并防止了许多错误,但它们不适用于现代 C++ 的某些方面。而且,它们编写的数据流框架也有其局限性。EspXEngine 的创建是为了解决大多数这些问题。我们已经发布了许多基于 EspXEngine 强大的路径敏感数据流分析的分析,包括并发检查和在移动检查后使用。这些检查的成功使我们将空指针分析移植到 EspXEngine。我们很高兴能够试用新版本,与旧版本相比,它引入了许多改进。博客文章的其余部分是对一些改进的深入概述,并提供了一些提示如何使用高级用户功能,如注释。


路径敏感分析

两种分析引擎都能够进行路径敏感分析。让我们考虑下面的例子来理解这意味着什么:


void path_sensitive(int *p, bool cond) { 

    int state = 0; 


    // branch 1  

    if (p != nullptr) { 

        state = 1; 

    } 


    // branch 2 

    if (cond) { 

        state = 2; 

        p = nullptr; 

    } 


    // branch 3 

    if (state == 1) { 

        *p = 42; // Null dereference? 

    } 

上面的代码有多个分支。其中一些分支是相关的,但流量敏感分析不会对这些相关性进行推理。例如,流敏感分析可能会得出结论,由于潜在的 null 取消引用,代码是不安全的,因为在分支 2p中设置为nullptr,然后在分支 3 中取消引用。但是,这将是误报,因为分支 3不能如果已采用分支 2,则可以到达。另一方面,路径敏感分析会推理这些类型的可达性条件,因此会得出上述代码是安全的结论。因此,路径敏感分析更加精确。但是,这种精度是以分析时间和内存为代价的。两个引擎在此代码段上具有相同的行为。


局部分析

两个引擎都在进行过程内分析。他们无法跨越功能边界,只能依靠类型、类型扩展、模型和契约来弥合差距。


void local_analysis(int *p, int *q, bool cond) { 

    if (p == nullptr) 

        return; 

    q = nullptr; 

    std::swap(p, q); 

    *p = 42; // Null dereference 

上面的代码有一个错误。该指针p是nullptr由于调用交换。当前检查未发现此错误。然而,EspXEngine 模拟了一些常见的 API。结果,它可以找出错误并向用户报告警告。


不幸的是,当我们调用自己的 API 时,EspXEngine 不会知道被调用函数的语义。在这些情况下,我们可以使用类型或SAL 注释来描述我们函数的前置条件和后置条件:


_Notnull_ int *get_my_ptr(); 

gsl::not_null<int *> get_my_ptr2(); 

void local_analysis(int *p) { 

    _Analysis_assume_(p != nullptr); 

    *p = 42; 

在上面的代码中,我们使用了_Notnull_和_Analysis_assume_SAL 注解来描述对一些指针值的约束。这两个引擎都支持。一种更现代的方法是使用丰富的类型来表达这些契约。这仅在 EspXEngine 中受支持。此外,它将标记将空指针存储到指针中的代码:gsl::not_null


void assign_to_gsl_notnull() { 

    int* p = nullptr; 

    auto q = gsl::make_not_null(p); // C26822 warning 

虽然类型很适合编码我们的期望,但 SAL 有能力表达更广泛的合同。考虑下面的例子:


void postcondition_conditional(bool b, _When_(b == true, _Outptr_) int** p)  { 

    if (b == true) 

        *p = nullptr; // C26824 warning 

这个函数有一个复杂的后置条件。只要第一个参数为真,当函数存在时,位置的值必须不是- 。两个引擎都理解这些契约(尽管 EspXEngine 中的支持更复杂),并且许多 Windows API 都被注释以描述它们的行为。我们很想使用标准语言工具,但 C++20 不接受合约提案,我们需要一个适用于 C 和 C++ API 的解决方案。*pnull


我们现有的空指针检查的一些问题

我想展示一些示例,其中基于 EspXEngine 的空指针检查具有比当前更好的行为。首先,有一些当前检查未发现的低易捕获空指针取消引用:


void nullptr_constant_dereference() { 

    *(int*)nullptr = 5; // Previously, it was not found. 

还有一些噪音更大的情况:


struct Node { 

    int number; 

    Node* next; 

}; 


void add_number(Node*& head, Node*& tail, int data) { 

    if (head != nullptr) { 

        tail->next = (Node*)malloc(sizeof(Node)); 

        tail = tail->next; 

    } else { 

        head = (Node*)malloc(sizeof(Node)); 

        tail = head; 

    } 

    tail->number = data; // C6011 warning 

    tail->next = nullptr; 

在上面的代码中,当前版本会在注释行给出一个空指针取消引用警告。从技术上讲,当malloc失败并返回一个nullptr. 这是一个与许多应用程序无关的场景。EspXEngine 具有低置信度和高置信度警告,在这种情况下只会发出低置信度警告。大多数用户可能只对预期噪音较小的高置信度警告感兴趣,并关闭低置信度警告。


此外,我们决定让 EspXEngine 更严格地检测各种未定义的行为:


void method_null_dereference(Foo* p, Foo* q) { 

    if (p || q) 

        return; 


    p->method();            // C26822 warning 

    q->static_method(42);   // OK, not UB.  

在上面的代码中,与 EspXEngine 不同的是,当我们在空指针上调用方法时,当前的警告不会发出警告。严格来说,这段代码有未定义的行为,但是当method不取消引用this指针时,许多实现都可以正常工作。


结论

即将推出的 Visual Studio 2022 17.0 Preview 4 将采用新的实验性检查来查找空指针取消引用错误。这些检查旨在成为当前检查的更好版本,具有更高的精度和附加功能。这些新检查正在进行深入分析,预计会增加分析时间。它们默认关闭,可以使用CppCoreCheckExperimentalRules规则集启用。