原文链接:

MEM51-CPP. Properly deallocate dynamically allocated resources


C 语言提供了几种分配内存的方式,例如 std::malloc(), std::calloc(), 和 std::realloc() 。这些也可以被 C++ 使用。然而,对于释放内存而言,C 语言只定义了一种简单的方式:std::free() 。关于 C 分配和释放内存要求的条款,见 MEM31-C. Free dynamically allocated memory when no longer neededMEM34-C. Only free memory allocated dynamically

C++ 语言提供了额外的集中分配内存的方式,例如运算符 new, new [] 和 placement new ,还有分配器对象(allocator objects) 。不像 C,C++ 提供了多种释放动态内存的方法,例如运算符 deletedelete []() 和分配器对象上的释放函数(deallocation functions)。

除了 nullptr ,不要对其他对象调用释放函数,或者下述相应分配函数返回的指针。

分配器 释放器
global operator new()/new global operator delete()/delete
global operator new[]()/new[] global operator delete[]()/delete[]
class-specific operator new()/new class-specific operator delete()/delete
class-specific operator new[]()/new[] class-specific operator delete[]()/delete[]
placement operator new() N/A
allocator<T>::allocate() allocator<T>::deallocate()
std::malloc(), std::calloc(), std::realloc() std::free()
std::get_temporary_buffer() std::return_temporary_buffer()

向不正确的释放函数传递一个指针将导致 未定义行为.

C++ 标准, [expr.delete], 段落 2 [ISO/IEC 14882-2014], 部分叙述如下:

首选 (delete object),delete 操作数可能是一个空指针值,一个由前述 new-表达式 创建的指向非数组对象的指针,或者指向一个表示类似 (Clause 10) 的基类对象的子对象 (1.8) 指针。如果不是,该行为是未定义的。其次 (delete array),delete 操作数可能是一个空指针值或者由先前数组 new-表达式 创建的指针。否则,该行为是未定义的。

释放一个非动态分配(包括由 placement new() 产生的非动态指针的指针)是未定义行为,因为该指针并不是从分配函数中获取的。释放一个已经传入过释放函数中的指针是未定义的,因为指针已经不再指向之前已动态分配过的内存。

当像 new 操作符被调用时,这导致同名的可重载的操作符被调用,比如 operator new() 。这些可重载的函数也可能被直接调用,但是和对应操作符有相同的限制。这就是说,调用 operator delete() ,并传入一个指针参数和对该指针调用 delete 操作符限制是一样的。进一步说,重载版本受作用域限制。因此,调用一个类型指定操作符来分配一个对象,但通过全局操作符来释放这个对象,这是可能的(但不允许)。

除了 newdelete 操作符,当使用其他内存管理函数,见 MEM53-CPP. Explicitly construct and destruct objects when manually managing object lifetime 获取关于对象生命周期管理信息。

不合规代码示例 (placement new())

在这个不合规的代码示例中,局部变量 space 被当作表达式传入 placement new 操作符中。调用返回的指针接着被传入到 ::operator delete() ,由于 ::operator delete() 试图去释放并不是由 ::operator new() 返回的指针,导致了 未定义行为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>

struct S {
S() { std::cout << "S::S()" << std::endl; }
~S() { std::cout << "S::~S()" << std::endl; }
};

void f() {
alignas(struct S) char space[sizeof(struct S)];
S *s1 = new (&space) S;

// ...

delete s1;
}

合规方案 (placement new())

这个合规方案移除了对 ::operator delete()的调用,而用显式地调用 s1 的析构函数来替代。这是为数不多的几次需要确保显式调用析构函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>

struct S {
S() { std::cout << "S::S()" << std::endl; }
~S() { std::cout << "S::~S()" << std::endl; }
};

void f() {
alignas(struct S) char space[sizeof(struct S)];
S *s1 = new (&space) S;

// ...

s1->~S();
}

不合规代码示例 (Uninitialized delete)

在这个不合规代码例子中,在同一个 try 块内试图进行两次内存分配。如果某一个失败了,那么 catch 块试图释放已经被分配的资源。然而,由于指针值并没有初始化为已知的值,i1 内存分配错误可能造成传入 ::operator delete() 的值 (i2中的)并不是之前调用 ::operator new() 所返回的,导致 未定义行为

1
2
3
4
5
6
7
8
9
10
11
12
#include <new>

void f() {
int *i1, *i2;
try {
i1 = new int;
i2 = new int;
} catch (std::bad_alloc &) {
delete i1;
delete i2;
}
}

合规方案 (Uninitialized delete)

这个兼容方案将两个指针均初始化为 nullptr ,可以被合法传入 ::operator delete().

1
2
3
4
5
6
7
8
9
10
11
12
#include <new>

void f() {
int *i1 = nullptr, *i2 = nullptr;
try {
i1 = new int;
i2 = new int;
} catch (std::bad_alloc &) {
delete i1;
delete i2;
}
}

不合规代码示例 (Double-Free)

一旦指针被传入到正确的释放函数中,该指针值将不可用。当并未在调用后续分配函数返回时重新赋值,该指针第二次传入释放函数,造成试图释放未动态分配的内存。管理着堆的潜在数据结构(注:该指针指向的已经被释放的内存堆)某种程度上会造成崩毁——会在程序中引入安全 漏洞 (vulnerabilities)。 这种类型的问题被称为 二次释放漏洞 (double-free vulnerabilities) 。实践中,二次释放漏洞可能会 利用 执行任意代码。

在这个不合规的代码例子中,类 C 拥有一个 P * 的所有权——后续会在类的析构函数中释放。C++ 标准,[class.copy],段落 7 [ISO/IEC 14882-2014], 陈述如下:

如果在类的定义未显式声明一个拷贝构造函数,将会隐式声明一个拷贝构造函数。如果类的定义声明了一个移动构造函数或者移动赋值运算,被隐式声明的拷贝构造函数将被定义为 deleted;否则,其(注:被隐式声明的拷贝构造函数)将被定义为 default (8.4)。如果该类包含一个用户自定义的拷贝赋值运算或者析构函数,后者要被弃用(注:用户需要显式自定义拷贝构造函数)。

If the class definition does not explicitly declare a copy constructor, one is declared implicitly. If the class definition declares a move constructor or move assignment operator, the implicitly declared copy constructor is defined as deleted; otherwise, it is defined as defaulted (8.4). The latter case is deprecated if the class has a user-declared copy assignment operator or a user-declared destructor.

C 尽管存在用户定义的析构函数,但是含有一个隐式的默认拷贝构造函数,并且这个默认的拷贝构造函数将拷贝存储在 p 中的指针值,这将导致二次释放:第一次释放发生在当 g() 退出时,第二次释放发生在当 h() 退出时。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct P {};

class C {
P *p;

public:
C(P *p) : p(p) {}
~C() { delete p; }

void f() {}
};

void g(C c) {
c.f();
}

void h() {
P *p = new P;
C c(p);
g(c);
}

合规方案 (Double-Free)

在这个合规方案中,C 的拷贝构造函数和拷贝赋值运算被显式删除。这个删除将会使先前不合规代码示例程序 不合语法的 (ill-formed) ——由于 g 的定义使用了被删除的拷贝构造函数。因此,g() 被修改为接受其引用参数,消除二次释放。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct P {};

class C {
P *p;

public:
C(P *p) : p(p) {}
C(const C&) = delete;
~C() { delete p; }

void operator=(const C&) = delete;

void f() {}
};

void g(C &c) {
c.f();
}

void h() {
P *p = new P;
C c(p);
g(c);
}

不合规代码示例 (array new[])

在接下来这个不合规的代码示例中,一个使用数组 new[] 来分配的数组,但是通过 delete 而不是 delete[] 来释放内存,导致 未定义行为.

1
2
3
4
5
void f() {
int *array = new int[10];
// ...
delete array;
}

合规方案 (array new[])

在这个合规方案中,通过调用 delelte[] 替换 delete 来修复,使代码遵循使用正确的配对的内存分配和释放函数

1
2
3
4
5
6

void f() {
int *array = new int[10];
// ...
delete[] array;
}

不合规代码示例 (malloc())

在这个不合规的代码示例中,混用了 malloc()delete 的调用。

1
2
3
4
5
6
7

#include <cstdlib>
void f() {
int *i = static_cast<int *>(std::malloc(sizeof(int)));
// ...
delete i;
}

这个例子没有违背 MEM53-CPP. Explicitly construct and destruct objects when manually managing object lifetime 因为它遵循 MEM53-CPP-EX1 异常.

实现细节

有些 ::operator new() 的实现会调用 std::malloc() 。在这类实现中, ::operator delete() 函数被要求调用 std::free() 去释放指针,并且非合规的代码示例将表现为正确定义的行为。然而,不应该依赖于实现,这里有个 实现 的细节,实现没有义务使用潜在的 C 内存管理函数来实现 C++ 内存管理操作。

合规方案 (malloc())

在这个合规方案中,指针由 std::malloc() 分配,调用 std::free() 来释放,而不是 delete.

1
2
3
4
5
6
7
#include <cstdlib>

void f() {
int *i = static_cast<int *>(std::malloc(sizeof(int)));
// ...
std::free(i);
}

不合规代码示例 ( new )

这个不合规的代码示例调用 std::free() 来释放通过 new 分配的内存。由此产生了一个 未定义行为 的副作用——由于使用了不正确的释放函数,该对象的析构函数并不会被调用,通过 std::free() 无法释放对象。

1
2
3
4
5
6
7
8
9
10
11
#include <cstdlib>

struct S {
~S();
};

void f() {
S *s = new S();
// ...
std::free(s);
}

此外,该代码违背了 MEM53-CPP. Explicitly construct and destruct objects when manually managing object lifetime.

合规方案 (new)

在这个合规方案中,由 new 分配的指针通过调用 delete 来释放,而不是 std::free()

1
struct` `S {`` ``~S();``};` `void` `f() {`` ``S *s = ``new` `S();`` ``// ...`` ``delete` `s;``}

不合规代码示例 (Class new)

在这个不合规代码示例中,operator new() 的类特定(class-specific)的实现重载了全局 new 操作符。当 new 被调用时,类特定的重载版本被选中,因此 S::operator new() 被调用。然而,由于该对象由作用域内的 ::delete 操作符销毁,全局的 operator delete() 函数被调用,而不是类他特定的实现的 S::operator delete(),导致 未定义行为.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <cstdlib>
#include <new>

struct S {
static void *operator new(std::size_t size) noexcept(true) {
return std::malloc(size);
}

static void operator delete(void *ptr) noexcept(true) {
std::free(ptr);
}
};

void f() {
S *s = new S;
::delete s;
}

合规方案 (class new)

在这个合规方案中,作用域内的 ::delete 调用被非域内的 delete 调用所替代, 引起 S::operator delete()被调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <cstdlib>
#include <new>

struct S {
static void *operator new(std::size_t size) noexcept(true) {
return std::malloc(size);
}

static void operator delete(void *ptr) noexcept(true) {
std::free(ptr);
}
};

void f() {
S *s = new S;
delete s;
}

不合规代码示例 (std::unique_ptr)

在这个不合规代码示例中,声明了一个 std::unique_ptr 来持有一个对象的指针——但是直接由对象数组来初始化的。当该 std::unique_ptr 被销毁时,默认的删除器调用 delete 而不是 delete[] ,导致了未定义行为。

1
2
3
4
5
6
7
#include <memory>

struct S {};

void f() {
std::unique_ptr<S> s{new S[10]};
}

合规方案 (std::unique_ptr)

在这个合规方案中,声明了一个 std::unique_ptr 来持有一个对象数组,而不是持有指向一个对象的指针。此外,用 std::make_unique() 来初始化这个智能指针。

1
2
3
4
5
6
7
#include <memory>

struct S {};

void f() {
std::unique_ptr<S[]> s = std::make_unique<S[]>(10);
}

使用 std::make_unique() 而不是直接初始化,如果 std::unique_ptr 的结果不是正确类型,将发出诊断 (emit a diagnostic) 。如果它被用在不合规的代码例子中,结果将会是不合规范的程序,而不是未定义行为。最好用 std::make_unique() 来代替其他手动初始化的方式。

不合规代码示例 (std::shared_ptr)

在这个不合规代码示例中,声明了一个 std::shared_ptr 来持有一个对象的指针——但是直接由对象数组来初始化的。和使用 std::unique_ptr 一样,当该 std::shared_ptr 被销毁时,默认的删除器调用 delete 而不是 delete[] ,导致了未定义行为。

1
2
3
4
5
6
7
#include <memory>

struct S {};

void f() {
std::shared_ptr<S> s{new S[10]};
}

合规方案 (std::shared_ptr)

不同于 std::unique_ptr 的合规方案,那些调用 std::make_unique() 来创建指向数组的独占指针的地方,调用数组类型的 std::make_shared() 是不合语法规范的。反之,这个合规方案为共享指针类型手动指定了一个自定义的删除器,来保证潜在的数组被正确的删除。

1
2
3
4
5
6
7
#include <memory>

struct S {};

void f() {
std::shared_ptr<S> s{new S[10], [](const S *ptr) { delete [] ptr; }};
}

风险评估

传递一个从先前并不匹配的分配函数获取的指针值到一个释放函数中会导致 未定义行为,可以引起可利用的 漏洞.

Rule Severity Likelihood Remediation Cost Priority Level
MEM51-CPP High Likely Medium P18 L1

Automated Detection

Tool Version Checker Description
Astrée 20.10 **invalid_dynamic_memory_allocation dangling_pointer_use **
Axivion Bauhaus Suite 7.2.0 CertC++-MEM51
Clang 3.9 clang-analyzer-cplusplus.NewDeleteLeaks -Wmismatched-new-deleteclang-analyzer-unix.MismatchedDeallocator Checked by clang-tidy, but does not catch all violations of this rule
CodeSonar 6.1p0 **ALLOC.FNH ALLOC.DF ALLOC.TM ** Free non-heap variable Double free Type mismatch
Helix QAC 2021.2 C++2110, C++2111, C++2112, C++2113, C++2118, C++3337, C++3339, C++4262, C++4263, C++4264
Klocwork 2021.1 CL.FFM.ASSIGN CL.FFM.COPY CL.FMM****FMM.MIGHT FMM.MUST FNH.MIGHT FNH.MUST FUM.GEN.MIGHT **FUM.GEN.MUSTUNINIT.CTOR.MIGHT UNINIT.CTOR.MUST UNINIT.HEAP.MIGHT UNINIT.HEAP.MUSTUNINIT.STACK.ARRAY.MIGHT UNINIT.STACK.ARRAY.PARTIAL.MUSTUNINIT.STACK.ARRAY.MUST UNINIT.STACK.MIGHT UNINIT.STACK.MUST**
LDRA tool suite 9.7.1 232 S, 236 S, 239 S, 407 S, 469 S, 470 S, 483 S, 484 S, 485 S, 64 D, 112 D** ** Partially implemented
Parasoft C/C++test 2021.1 CERT_CPP-MEM51-a CERT_CPP-MEM51-b CERT_CPP-MEM51-c CERT_CPP-MEM51-d Use the same form in corresponding calls to new/malloc and delete/free Always provide empty brackets ([]) for delete when deallocating arrays Both copy constructor and copy assignment operator should be declared for classes with a nontrivial destructor Properly deallocate dynamically allocated resources
Parasoft Insure++ Runtime detection
Polyspace Bug Finder R2021b CERT C++: MEM51-CPP Checks for:Invalid deletion of pointerInvalid free of pointerDeallocation of previously deallocated pointerRule partially covered.
PRQA QA-C++ 4.4 2110, 2111, 2112, 2113, 2118*,**3337, 3339*, 4262, 4263, 4264
PVS-Studio 7.15 V515, V554, V611, V701, V748, V773, V1066
SonarQube C/C++ Plugin 4.10 S1232

Search for vulnerabilities resulting from the violation of this rule on the CERT website.

SEI CERT C++ Coding Standard MEM53-CPP. Explicitly construct and destruct objects when manually managing object lifetime
SEI CERT C Coding Standard MEM31-C. Free dynamically allocated memory when no longer needed MEM34-C. Only free memory allocated dynamically
MITRE CWE CWE 590, Free of Memory Not on the Heap CWE 415, Double Free CWE 404, Improper Resource Shutdown or Release CWE 762, Mismatched Memory Management Routines

Bibliography

[Dowd 2007] “Attacking delete and delete [] in C++”
[Henricson 1997] Rule 8.1, “delete should only be used with new" Rule 8.2, “delete [] should only be used with new []"
[ISO/IEC 14882-2014] Subclause 5.3.5, “Delete” Subclause 12.8, “Copying and Moving Class Objects” Subclause 18.6.1, “Storage Allocation and Deallocation” Subclause 20.7.11, “Temporary Buffers”
[Meyers 2005] Item 16, “Use the Same Form in Corresponding Uses of new and delete
[Seacord 2013] Chapter 4, “Dynamic Memory Management”
[Viega 2005] “Doubly Freeing Memory”