原文链接:

MEM56-CPP. Do not store an already-owned pointer value in an unrelated smart pointer


类似 std::unique_ptrstd::shared_ptr 的智能指针将指针所有权语义编码为类型系统的一部分. 它们包裹指针值, 通过 opereator *()operator->() 成员函数提供类似指针语义, 并且控制他们所管理指针的生命周期. 当从一个指针值构造出一个智能指针时, 称这个值被这个智能指针所拥有.

调用 std::unique_ptr::release() 将释放所管理指针值的所有权. std::unique_ptr 对象的析构, 移动赋值及对其调用 std::unique_ptr::reset() 也将会释放所管理指针值的所有权, 但是会导致所管理指针值析构. 如果调用 std::shared_ptr::unique() 返回 true, 那么 std::shared_ptr 的析构或对其调用 std::shared_ptr::reset() 将释放所管理的指针值, 但是会导致所管理指针值的析构.

有些智能指针, 比如 std::shared_ptr, 允许多个智能指针对象来管理同一个指针值. 在这类情况中, 最初的智能指针对象拥有该指针值, 其他后续的智能指针对象与最初的指针指针关联. 举个例子, 将一个 std::shared_ptr 对象通过拷贝赋值拷贝到另一个 std::shared_ptr 对象中, 这将创建两个智能指针间的联系, 然而, 从被另一个 std::shared_ptr 对象所属的指针值创建一个 std::shared_ptr 对象并不会 (注: 产生关联).

不要从被另一个指针指针对象所属的指针值创建一个不相关的智能指针对象, 包括将智能指针所管理的指针重置为已有所属的指针值, 比如通过调用 reset() .

不合规代码示例

在这个不合规代码示例中, 两个不相关的指针指针从同一个指针值从构造出. 当局部自动变量 p2 被销毁时, 它会删除它所管理的指针值. 然后, 当局部自动变量 p1 被销毁时, 它也会删除同一个指针值, 导致二次释放 漏洞, vulnerability.

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

void f() {
int *i = new int;
std::shared_ptr<int> p1(i);
std::shared_ptr<int> p2(i);
}

合规方案

在这个合规方案中, std::shared_ptr 通过拷贝构造和另一个关联. 当局部自动变量 p2 被销毁时, 共享指针的引用计数将递减, 但是依然不为零. 然后, 当局部自动变量 p1 被销毁时, 共享指针的引用计数将递减为零, 然后所管理的指针被销毁. 这个合规方案也通过调用 std::make_shared() 来代替分配一个裸指针且将其存储在局部变量中的操作.

1
2
3
4
5
6
#include <memory>

void f() {
std::shared_ptr<int> p1 = std::make_shared<int>();
std::shared_ptr<int> p2(p1);
}

不合规代码示例

在这个不合规代码示例中, 被一个 std::shared_ptr 对象所属的指针值 poly 通过 dynamic_cast 转换到了指针类型 D * , 来尝试获得一个多态派生类型的 std::shared_ptr . 然而, 这最终导致 未定义行为, undefined behavior , 因为同一个指针被存储在两个不同的 std::shared_ptr 对象中. 当 g() 退出时, 存储在 derived 的指针被默认删除器释放. 后续使用 poly 造成访问已经释放的内存. 当 f() 退出时, 存储在 poly 的指针被销毁, 造成二次释放漏洞.

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

struct B {
virtual ~B() = default; // Polymorphic object
// ...
};
struct D : B {};

void g(std::shared_ptr<D> derived);

void f() {
std::shared_ptr<B> poly(new D);
// ...
g(std::shared_ptr<D>(dynamic_cast<D *>(poly.get())));
// Any use of poly will now result in accessing freed memory.
}

合规方案

在这个合规方案中, dynamic_cast 被替换为调用 std::dynamic_pointer_cast() – 将返回一个含有有效共享指针值的多态类型的 std::shared_ptr . 当 g() 退出时, 指针 (注: B) 的引用计数随着 derived 的析构而递减. 但是由于由 poly (在 f() 中) 持有(注: D) 的引用, 存储的指针值在 g() 退出时依然有效.

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

struct B {
virtual ~B() = default; // Polymorphic object
// ...
};
struct D : B {};

void g(std::shared_ptr<D> derived);

void f() {
std::shared_ptr<B> poly(new D);
// ...
g(std::dynamic_pointer_cast<D, B>(poly));
// Any use of poly will now result in accessing freed memory.
}

不合规代码示例

在这个不合规代码示例中, 构造了一个类型 Sstd::shared_ptr , 存储于 s1 中. 调用 S::g() 来获取另一个值被 s1 管理的共享指针. 然而, 由 S::g() 返回的智能指针和存储在 s1 中的智能指针无关. 当 s2 被销毁时, 这会释放由 s1 管理的指针. 当 s1 被销毁时, 造成二次释放漏洞.

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

struct S {
std::shared_ptr<S> g() { return std::shared_ptr<S>(this); }
};

void f() {
std::shared_ptr<S> s1 = std::make_shared<S>();
// ...
std::shared_ptr<S> s2 = s1->g();
}

合规方案

这个合规方案利用 std::enable_shared_from_this::shared_from_this() 来从 S 中获取与之现存 std::shared_ptr 对象关联的共享指针. 一个常规的实现策略是使 std::shared_ptr 构造函数探测到继承于 std::enable_shared_from_this 的指针的存在, 并自动更新 std::enable_shared_from_this::shared_from_this() 执行所需的内部数据 ( the internal bookkeeping). 记住, std::enable_shared_from_this::shared_from_this() 需要一个管理着指向 this 指针的存活着的 std::shared_ptr 实例. 如果不满足这个条件将导致 未定义行为, undefined behavior, 因为这会造成这个试图管理对象生命周期的智能指针自身并没有生命周期管理的语义.

当不存在引用 thisstd::shared_ptr 对象时, 我们是否应该有准则来制止 shared_from_this() 的调用? 这么做将是未定义行为, 但是我觉得那也是一个非常具体的情景.

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

struct S : std::enable_shared_from_this<S> {
std::shared_ptr<S> g() { return shared_from_this(); }
};

void f() {
std::shared_ptr<S> s1 = std::make_shared<S>();
std::shared_ptr<S> s2 = s1->g();
}

风险预估

将一个先前并非由匹配的分配函数获取的指针值传递到析构函数中会造成 未定义行为, undefined behavior, 导致可利用 漏洞, vulnerabilities.

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

Automated Detection

Tool Version Checker Description
Astrée 20.10 **dangling_pointer_use **
Axivion Bauhaus Suite 7.2.0 CertC++-MEM56
Helix QAC 2021.2 C++4721, C++4722, C++4723
Parasoft C/C++test 2021.1 CERT_CPP-MEM56-a Do not store an already-owned pointer value in an unrelated smart pointer
Polyspace Bug Finder R2021b CERT C++: MEM56-CPP Checks for use of already-owned pointers (rule fully covered)
PVS-Studio 7.15 V1006

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

SEI CERT C++ Coding Standard MEM50-CPP. Do not access freed memory MEM51-CPP. Properly deallocate dynamically allocated resources
MITRE CWE CWE-415, Double Free CWE-416, Use After Free CWE 762, Mismatched Memory Management Routines

Bibliography

[ISO/IEC 14882-2014] Subclause 20.8, “Smart Pointers”

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