原文链接:

OOP57-CPP. Prefer special member functions and overloaded operators to C Standard Library functions


有些 C 标准库函数对对象执行位域操作。举个例子,std::memcmp() 对比组成两个对象的字节,std::memcpy() 拷贝对象的字节到目标缓存中。然而,对于有些对象的字节,会造成未定义行为或者程序异常。

C++ 标准, [class], 段落 6 [ISO/IEC 14882-2014], 陈述如下:

A trivially copyable class is a class that:
— has no non-trivial copy constructors,
— has no non-trivial move constructors,
— has no non-trivial copy assignment operators,
— has no non-trivial move assignment operators, and
— has a trivial destructor.
A trivial class is a class that has a default constructor, has no non-trivial default constructors, and is trivially copyable. [Note: In particular, a trivially copyable or trivial class does not have virtual functions or virtual base classes. — end note]

一个平凡可拷贝类是:

—没有非平凡拷贝构造函数,

—没有非平凡移动构造函数,

—没有非平凡拷贝赋值操作符,

—没有非平凡移动赋值操作符,

—有平凡的析构函数

此外, C++ 标准, [class], 段落 7, 叙述如下:

A standard-layout class is a class that:
— has no non-static data members of type non-standard-layout class (or array of such types) or reference,
— has no virtual functions and no virtual base classes,
— has the same access control for all non-static data members,
— has no non-standard-layout base classes,
— either has no non-static data members in the most derived class and at most one base class with non-static data members, or has no base classes with non-static data members, and
— has no base classes of the same type as the first non-static data member.

一个标准布局类是:

—没有非标准布局类类型(或者这种数据类型)或引用类型的非静态的数据成员,

—没有虚函数且没有虚基类,

—对所有非静态数据成员有相同的访问权限,

—没有非标准布局的基类,

—既在最派生类中没有非静态数据成员且最多有一个基类有非静态数据成员,又没有非静态数据成员的基类,

—没有和首个非静态数据成员相同类型的基类

不要使用 std::memset()来初始化一个非平凡类类型对象,因为这可能不能正确初始化该对象的值表达。不要使用 std::memcpy() (或相关位域拷贝函数) 来初始化非平凡类类型的副本对象,因为这可能不能正确初始化该副本的值表达。不要使用 std::memcmp() (或相关位域比较函数) 来比较非标准副局的类类型对象,因为这可能不能正确比较对象的值表达。在所有的情况中,最好选用替换方法。

C 标准库函数 C++ 等价功能
std::memset() Class constructor
std::memcpy()``std::memmove()``std::strcpy() Class copy constructor or operator=()
std::memcmp()``std::strcmp() operator<(), operator>(), operator==(), or operator!=()

不合规代码示例

在这个不合规代码示例中,一个非平凡类对象通过调用它的默认构造函数来初始化,但是之后又通过std::memset 重新初始化为默认状态,重新初始化对象不正确。 不正确的重新初始化造成类不变性没有在后续类对象的使用中维持。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <cstring>
#include <iostream>

class C {
int scalingFactor;
int otherData;

public:
C() : scalingFactor(1) {}

void set_other_data(int i);
int f(int i) {
return i / scalingFactor;
}
// ...
};

void f() {
C c;

// ... Code that mutates c ...

// Reinitialize c to its default state
std::memset(&c, 0, sizeof(C));

std::cout << c.f(100) << std::endl;
}

上述不合规代码示例遵从 EXP62-CPP. Do not access the bits of an object representation that are not part of the object’s value representation ,因为值表达的所有位也被用在C的对象表达上。

合规方案

在这个合规方案中,std::memset() 被替换为默认初始化的 copy-and-swap 操作,clear(). 这个操作确保对象被正确初始化为默认状态,并且对那些已优化过但未能清除被赋值对象所有数据成员的赋值操作符的类类型,行为是正确的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <iostream>
#include <utility>

class C {
int scalingFactor;
int otherData;

public:
C() : scalingFactor(1) {}

void set_other_data(int i);
int f(int i) {
return i / scalingFactor;
}
// ...
};

template <typename T>
T& clear(T &o) {
using std::swap;
T empty;
swap(o, empty);
return o;
}

void f() {
C c;

// ... Code that mutates c ...

// Reinitialize c to its default state
clear(c);

std::cout << c.f(100) << std::endl;
}

不合规代码示例

在这个不合规代码示例中, std::memcpy() 被用来创建一个非平凡类类型 C 的对象副本。然而,由于同一指针值也将被拷贝到 c2 中,每个对象示例试图在 C::~C() 中删除 int *,造成二次释放 漏洞,vulnerabilities

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <cstring>

class C {
int *i;

public:
C() : i(nullptr) {}
~C() { delete i; }

void set(int val) {
if (i) { delete i; }
i = new int{val};
}

// ...
};

void f(C &c1) {
C c2;
std::memcpy(&c2, &c1, sizeof(C));
}

合规方案

在这个合规方案中, C 定义了赋值操作符来代替 std::memcpy() 的使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class C {
int *i;

public:
C() : i(nullptr) {}
~C() { delete i; }

void set(int val) {
if (i) { delete i; }
i = new int{val};
}

C &operator=(const C &rhs) noexcept(false) {
if (this != &rhs) {
int *o = nullptr;
if (rhs.i) {
o = new int;
*o = *rhs.i;
}
// Does not modify this unless allocation succeeds.
delete i;
i = o;
}
return *this;
}

// ...
};

void f(C &c1) {
C c2 = c1;
}

不合规代码示例

在这个不合规代码示例中, std::memcmp() 被用来比较两个非标准布局类型的对象。由于std::memcmp() 执行了对象表达的位域比较,假如虚表指针是对象表达的实现一部分,它将比较虚表指针。假如 c1 或者 c2 的动态类型是类型 C 的派生类,尽管它们的值表达相同,比较也可能失败。

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

class C {
int i;

public:
virtual void f();

// ...
};

void f(C &c1, C &c2) {
if (!std::memcmp(&c1, &c2, sizeof(C))) {
// ...
}
}

由于虚表并不是对象的值表达的一部分,所以通过 std::memcmp() 来比较也违背了 EXP62-CPP. Do not access the bits of an object representation that are not part of the object’s value representation.

合规方案

在这个合规方案中, C 定义了一种等式运算符来代替 std::memcmp()。这个方案确保当对象进行比较时,只考虑对象的值表达。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class C {
int i;

public:
virtual void f();

bool operator==(const C &rhs) const {
return rhs.i == i;
}

// ...
};

void f(C &c1, C &c2) {
if (c1 == c2) {
// ...
}
}

Risk Assessment

Most violations of this rule will result in abnormal program behavior. However, overwriting implementation details of the object representation can lead to code execution vulnerabilities.

Rule Severity Likelihood Remediation Cost Priority Level
OOP57-CPP High Probable High P6 L2

Automated Detection

Tool Version Checker Description
Astrée 20.10 **stdlib-use-ato stdlib-use stdlib-use-getenv stdlib-use-system include-time stdlib-use-string-unbounded ** Partially checked
CodeSonar 6.2p0 BADFUNC.MEMCMP****BADFUNC.MEMSET Use of memcmpUse of memset
Helix QAC 2022.1 C++5017, C++5038
Klocwork 2022.1 CERT.OOP.CSTD_FUNC_USE
LDRA tool suite 9.7.1 44 S** ** Enhanced Enforcement
Parasoft C/C++test 2021.2 CERT_CPP-OOP57-a CERT_CPP-OOP57-b Do not initialize objects with a non-trivial class type using C standard library functions Do not compare objects of nonstandard-layout class type with C standard library functions
Polyspace Bug Finder R2021b CERT C++: OOP57-CPP Checks for bytewise operations on nontrivial class object (rule fully covered)
PRQA QA-C++ 4.4 5017, 5038
PVS-Studio 7.18 V598, V780
RuleChecker 20.10 ***stdlib-use-ato stdlib-use stdlib-use-getenv stdlib-use-system include-time stdlib-use-string-unbounded* ** Partially checked

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

SEI CERT C++ Coding Standard EXP62-CPP. Do not access the bits of an object representation that are not part of the object’s value representation

Bibliography

[ISO/IEC 14882-2014] Subclause 3.9, “Types” Subclause 3.10, “Lvalues and Rvalues” Clause 9, “Classes”

img img img