原文链接:

MEM52-CPP. Detect and handle memory allocation errors


默认的内存分配操作符, ::operator new(std::size_t), 如果分配失败, 会抛出 std::bad_alloc 异常. 所以, 没必要去检查 ::operator new(std::size_t) 的调用结果是否是 nullptr. 不抛出形式, ::operator new(std::size_t, const std::nothrow_t &), 如果分配失败, 不抛出异常, 而是返回 nullptr. 同样的行为也体现在 operator new[] 的两个版本的分配函数上. 此外, 默认分配器 (std::allocator) 使用 ::operator new(std::size_t) 来执行分配操作, 应该要被同等看待.

1
2
3
4
5
T *p1 = new T; // Throws std::bad_alloc if allocation fails
T *p2 = new (std::nothrow) T; // Returns nullptr if allocation fails

T *p3 = new T[1]; // Throws std::bad_alloc if the allocation fails
T *p4 = new (std::nothrow) T[1]; // Returns nullptr if the allocation fails

进一步, operator new[] 能抛出类型错误 std::bad_array_new_length, 一个 std::bad_alloc 的子类, 如果传入 newsize 参数是个负数或者超大值.

当使用不抛出形式, 在访问该返回的指针前,检查其不是 nullptr 是非常必要的. 使用任一种形式, 确保遵循 ERR50-CPP. Do not abruptly terminate the program.

不合规代码示例

在这个不合规的代码示例中, 通过 ::operator new[](std::size_t) 构造了一个 int 数组, 但未检查分配的结果. 该函数被标记为 noexcept, 所以调用者假定这个函数不会抛出任何异常. 因为 ::operator new[](std::size_t) 在分配失败时能抛出异常, 这可能导致程序 异常终止 .

1
2
3
4
5
6
7
8
#include <cstring>

void f(const int *array, std::size_t size) noexcept {
int *copy = new int[size];
std::memcpy(copy, array, size * sizeof(*copy));
// ...
delete [] copy;
}

合规方案 (std::nothrow)

当使用 std::nothrow, new 操作符既不会返回空指针,也不会返回指向已分配空间的指针. 在引用这个返回的指针前, 始终得检查它是否是 nullptr. 这个合规方案恰当地处理了返回 nullptr 的这种错误情况.

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

void f(const int *array, std::size_t size) noexcept {
int *copy = new (std::nothrow) int[size];
if (!copy) {
// Handle error
return;
}
std::memcpy(copy, array, size * sizeof(*copy));
// ...
delete [] copy;
}

合规方案 (std::bad_alloc)

另外可选的是, 你可以使用不带 std::nothrow::operator new[] 来替换, 且如果没有分配到足够的内存, 捕捉 std::bad_alloc 异常.

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

void f(const int *array, std::size_t size) noexcept {
int *copy;
try {
copy = new int[size];
} catch(std::bad_alloc) {
// Handle error
return;
}
// At this point, copy has been initialized to allocated memory
std::memcpy(copy, array, size * sizeof(*copy));
// ...
delete [] copy;
}

合规方案 (noexcept(false))

如果函数设计为期望调用者处理异常情况, 则允许显式地标记该函数为可能会抛出异常, 如该合规方案中所示. 标记这类函数并非严格必须的, 和其他没有 noexcept 修饰符的函数一样, 默认是允许抛出异常.

If the design of the function is such that the caller is expected to handle exceptional situations, it is permissible to mark the function explicitly as one that may throw, as in this compliant solution. Marking the function is not strictly required, as any function without a noexcept specifier is presumed to allow throwing.

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

void f(const int *array, std::size_t size) noexcept(false) {
int *copy = new int[size];
// If the allocation fails, it will throw an exception which the caller
// will have to handle.
std::memcpy(copy, array, size * sizeof(*copy));
// ...
delete [] copy;
}

不合规代码示例

在这个不合规的代码示例中, 同一个表达式中执行了两个内存分配操作. 因为内存分配是被当作参数传入到函数调用里的, 其中一个 new 的调用结果抛出的异常可能导致内存泄漏.

1
2
3
4
5
6
7
struct A { /* ... */ };
struct B { /* ... */ };

void g(A *, B *);
void f() {
g(new A, new B);
}

考虑这种情况, A 被分配和构造在先, 然后 B 被分配并抛出异常. 将 g() 的调用包装在 try/catch 块中并不充分, 因为这不能释放已经分配好的 A 的内存.

这个不合规的代码示例也违背了 EXP50-CPP. Do not depend on the order of evaluation for side effects, 因为传入 g() 的实参在计算时顺序时未确定的.

合规方案 (std::unique_ptr)

在这个合规方案中, 利用 std::unique_ptrRAII 来管理对象 AB 的资源. 在不合规代码示例所描述的情景中, B 抛出异常仍然将引起对象 A 的析构和释放, 接着 std::unique_ptr<A> 被销毁.

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

struct A { /* ... */ };
struct B { /* ... */ };

void g(std::unique_ptr<A> a, std::unique_ptr<B> b);
void f() {
g(std::make_unique<A>(), std::make_unique<B>());
}

合规方案 (References)

如果可能, 更适应的合规方案是, 完全移除内存分配, 取而代之通过传入对象的引用.

1
2
3
4
5
6
7
8
9
struct A { /* ... */ };
struct B { /* ... */ };

void g(A &a, B &b);
void f() {
A a;
B b;
g(a, b);
}

风险评估

未成功检测分配错误会导致 程序异常终止, abnormal program termination拒绝服务攻击, denial-of-service attacks.

如果被攻击的程序从返回值中引用了内存偏移地址, 攻击者可以利用这个程序来任意读写内存. 这种 漏洞, vulnerability 已经被用于执行任意代码 [VU#159523].

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

Automated Detection

Tool Version Checker Description
Compass/ROSE
Coverity 7.5 CHECKED_RETURN Finds inconsistencies in how function call return values are handled
Helix QAC img C++3225, C++3226, C++3227, C++3228, C++3229, C++4632
Klocwork img NPD.CHECK.CALL.MIGHT NPD.CHECK.CALL.MUST NPD.CHECK.MIGHTNPD.CHECK.MUSTNPD.CONST.CALL NPD.CONST.DEREF NPD.FUNC.CALL.MIGHT NPD.FUNC.CALL.MUST NPD.FUNC.MIGHT **NPD.FUNC.MUSTNPD.GEN.CALL.MIGHT NPD.GEN.CALL.MUST NPD.GEN.MIGHT NPD.GEN.MUSTRNPD.CALL RNPD.DEREF **
LDRA tool suite img 45 D** ** Partially implemented
Parasoft C/C++test img CERT_CPP-MEM52-a CERT_CPP-MEM52-b Check the return value of new Do not allocate resources in function argument list because the order of evaluation of a function’s parameters is undefined
Parasoft Insure++ Runtime detection
Polyspace Bug Finder img CERT C++: MEM52-CPP Checks for unprotected dynamic memory allocation (rule partially covered)
PRQA QA-C++ img 3225, 3226, 3227, 3228, 3229, *4632*
PVS-Studio img V522, V668

The vulnerability in Adobe Flash [VU#159523] arises because Flash neglects to check the return value from calloc(). Even though calloc() returns NULL, Flash does not attempt to read or write to the return value. Instead, it attempts to write to an offset from the return value. Dereferencing NULL usually results in a program crash, but dereferencing an offset from NULL allows an exploit to succeed without crashing the program.

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

SEI CERT C Coding Standard ERR33-C. Detect and handle standard library errors
MITRE CWE CWE 252, Unchecked Return Value CWE 391, Unchecked Error Condition CWE 476, NULL Pointer Dereference CWE 690, Unchecked Return Value to NULL Pointer Dereference CWE 703, Improper Check or Handling of Exceptional Conditions CWE 754, Improper Check for Unusual or Exceptional Conditions

Bibliography

[ISO/IEC 9899:2011] Subclause 7.20.3, “Memory Management Functions”
[ISO/IEC 14882-2014] Subclause 18.6.1.1, “Single-Object Forms” Subclause 18.6.1.2, “Array Forms” Subclause 20.7.9.1, “Allocator Members”
[Meyers 1996] Item 7, “Be Prepared for Out-of-Memory Conditions”
[Seacord 2013] Chapter 4, “Dynamic Memory Management”

SEI CERT C++ Coding Standard > SEI CERT C++ Coding Standard > button_arrow_left.png SEI CERT C++ Coding Standard > SEI CERT C++ Coding Standard > button_arrow_up.png SEI CERT C++ Coding Standard > SEI CERT C++ Coding Standard > button_arrow_right.png