CL.FFM.ASSIGN

由于缺失运算符而释放已释放的内存

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

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

如果没有实现赋值运算符,C++ 编译器将在需要时自动生成赋值运算符,但是编译器提供的实现始终处于显式的浅状态。

如果某一复制未显式编码为管理这些数据成员,复制该对象可生成引用一个动态分配数据成员的两个对象。浅层复制操作(其中的指针仅由值复制)将生成一对指向同一底层堆内存的对象。对该堆内存执行的任何操作都会影响对其保持引用的这两个对象,这可能导致所有类型的程序出现意外结果。

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

漏洞与风险

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

缓解与预防

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

漏洞代码示例

复制
    #include <iostream>
    using namespace std;
    class C{
      char *data;
      C(const C&){}
    public
      C(){  data = new char[10]; }
      ~C() {
        cout << "Calling delete for " << (void *)data << endl;
        delete[] data;
      }
    };
    int main(){
      C c1;
      C c2;
      c1 = c2;
      return 1;
    }


Output: 

0x602030 调用 delete
0x602030 调用 delete

在此示例中,类 C 使用 C::data 的动态内存分配,但是未定义运算符 =。因此,执行第 16 行的函数 main() 时,c1.data 和 c2.data 具有相同的值,并且在被破坏时均会调用运算符 delete[ ],从而删除相同的指针两次。在本例中,有关因运算符 = 实现而再次释放已释放的内存,CL.FFM.ASSIGN 已找到一个典型示例。

修正代码示例

为了解决此问题,应定义运算符 =。根据具体情况,可以使用运算符 = 的不同实现。

  • 如果有必要分配新内存,运算符 = 实现应确认其未复制本身,释放旧内存,分配新内存,然后复制以下数据:
复制
    class C{
// ...
      C& operator=(const C& src){
        if (&src == this) return *this;
        delete[] data;
        data = new char[10];
        memcpy(data, src.data, 10);
        return *this;
      }
// ...
   };
  • 之前的实现执行了不必要的堆内存操作,因为被破坏的数据和已分配的数据大小相同。在这种情况下,可以重复使用旧内存:
复制
    class C{
// ...
      C& operator=(const C& src){
        if (&src == this) return *this;
        memcpy(data, src.data, 10);
        return *this;
      }
// ...
    };
  • 如果不希望复制类实例,运算符 = 应声明为私有。在这种情况下,如果尝试进行复制,编译器将生成一个错误。
复制
    class C{
// ...
    private
      C& operator=(const C&){ return *this;}
// ...
    };

安全培训

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

扩展

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