CL.FFM.COPY

由于缺失复制构造函数而释放已释放的内存

类级别检查器会根据 Scott Meyer 的高效 C++ 规则类构造来生成建议。

CL.FFM.COPY 基于 Scott Meyer 的第 11 项:使用动态分配的内存为类声明复制建构函数和赋值运算符。此检查器可查找包含动态分配数据成员的类,并且不会定义复制构造函数。

如果没有实现复制构造函数,C++ 编译器将在需要时自动生成复制构造函数,但是编译器提供的实现始终处于显式的浅状态。

如果该复制未显式编码为管理这些数据成员,通过初始化或使用按值传递函数调用复制该对象,可生成引用一个动态分配数据成员的两个对象。浅层复制操作(其中的指针仅由值复制)将生成一对指向同一底层堆内存的对象。对该堆内存执行的任何操作都会影响对其保持引用的这两个对象。这可能导致多线程应用程序出现同步问题以及所有类型的程序出现意外结果。

此特定检查器引用的情况是释放已释放的内存可能导致的后果,当共享同一底层分配的两个对象超出范围时,便会发生这种情况。

漏洞与风险

在此情况下,第一个对象超出范围时,通常将释放所有相关的堆内存,包括现在已与另一个对象共享的缓冲区。当第二个对象超出范围时,该对象尝试释放其以为是自己的内存资源的行为将导致访问已释放的内存,在最糟糕的情况下,已释放的内存可能破坏相应的堆。

缓解与预防

为解决该问题,请始终为包含动态分配数据成员的类提供赋值运算符的显式实现,并始终确保赋值运算符对这些数据成员执行深层复制。

漏洞代码示例

复制
1    #include <iostream>
2    using namespace std;
3    class C{
4      char* data;
5      C& operator=(const C&) {return *this;}
6    public
7      C() { data = new char[10]; }
8      ~C(){
9        cout << "Calling delete for " << (void *)data << endl;
10        delete[] data;
11      }
12    };
13    int main(){
14      C c1;
15      C c2 = c1;
16      return 1;
17    }


输出:

0x602010 调用 delete
0x602010 调用 delete

在此示例中,在第 15 行调用了复制构造函数。由于 C 未定义复制构造函数,编译器将生成一个复制构造函数,该函数会将所有值从一个实例复制到另一个实例。因此,c1.data 和 c2.data 具有相同的值,并且在调用析构函数时会将其删除两次。在本例中,对于包含动态分配数据成员和未定义复制构造函数的类,CL.FFM.COPY 已找到典型示例。

修正代码示例

要解决此问题,根据情况,可在两个不同实现中选择其中一个来使用。

通常,应定义一个深层复制构造函数:

复制
    class C{
// ...
      C(const C& src){
        data = new char[10];
        memcpy(data, src.data, 10);
      }
// ...
    };

如果不打算复制实例,则应将复制构造函数声明为专用:

复制
    class C{
// ...
    private
      C(const C& src){ /* do not create copies */ }
// ...
    };

安全培训

应用程序安全培训材料由 Secure Code Warrior 提供。

扩展

此检查器可通过 Klocwork 知识库进行扩展。有关详情,请参阅调整 C/C++ 分析。