一、线上现象:崩溃变多,但没有业务堆栈
某次版本发布后,监控后台出现异常:
-
崩溃量明显上升
-
主要集中在 ARM64 设备
-
无明显业务堆栈
-
随某个新功能模块上线而增加
典型堆栈:
#00 pc 000000000004f3c8 libc.so (abort+160)
#01 pc 0000000000079abc libc.so
#02 pc 0000000000012345 libEGL.so
#03 pc 000000000000xxxx vendor/libGLESv2_*.so
或者:
double free or corruption
malloc(): invalid pointer
SIGABRT
特征很明显:
-
崩在 libc / EGL / vendor
-
没有业务函数
-
偶现
-
难以本地复现
这类问题通常归类为:
内存破坏类问题(Memory Corruption)
二、为什么第一时间怀疑智能指针?
版本 diff 显示:
-
新增一个多线程渲染模块
-
使用大量 std::shared_ptr
-
线程中使用 shared_from_this()
经验判断:当崩溃无业务栈,随业务复杂度线性上升,和线程模型强相关时,优先怀疑:
-
double free
-
use-after-free
-
多控制块问题
-
生命周期错位
但猜测必须被工具验证。
三、第一步验证:ASan
构建 AddressSanitizer 版本:
-fsanitize=address
压测后成功复现,ASan 报告:
ERROR: AddressSanitizer: attempting double-free on 0x6040000012f0
#0 0x7f8a3c1 in free
#1 0x7f9bcd2 in std::__shared_ptr::~__shared_ptr()
#2 0x7f8caa1 in Player::~Player()
初步确认:
double free
但线上是 ARM64。ASan 开销大,不适合验证偶现问题。于是继续使用 HWASan。
四、关键一步:HWASan 报告拆解
开启 HWASan:
-fsanitize=hwaddress
压测后抓到如下报告(简化版):
ERROR: HWAddressSanitizer: invalid-free on address 0x0070001234a0
0x0070001234a0 is located 0 bytes inside of 64-byte region
freed by thread T1 here:
#0 0x7c8f1a in operator delete(void*)
#1 0x7d1234 in Player::~Player()
#2 0x7d5678 in std::__shared_count::~__shared_count()
previously freed by thread T2 here:
#0 0x7c8f1a in operator delete(void*)
#1 0x7d9abc in std::__shared_ptr::~__shared_ptr()
#2 0x7ddef0 in RenderThread::stop()
Thread T1 created by:
#0 pthread_create
#1 std::thread::_M_start_thread
逐行拆解。
1️⃣ invalid-free
invalid-free on address 0x0070001234a0
说明:
这块内存已经被释放过
2️⃣ freed by thread T1
freed by thread T1 here:
Player::~Player()
std::__shared_count::~__shared_count()
第一次释放路径:
- shared_count 析构
- 触发 Player 析构
- 调用 delete
3️⃣ previously freed by thread T2
previously freed by thread T2 here:
std::__shared_ptr::~__shared_ptr()
RenderThread::stop()
第二次释放路径:
-
另一个 shared_ptr 析构
-
触发 delete
关键点:
两次释放来自两个线程
析构路径不同
这几乎可以确定:
存在两个独立的控制块
五、根因还原
排查代码后发现:
Player* p = new Player();
std::shared_ptr<Player> sp1(p);
std::shared_ptr<Player> sp2(p); // 问题
或者 JNI 场景:
auto sp = std::make_shared<Player>();
return reinterpret_cast<jlong>(sp.get()); // Java 持裸指针
Java later:
delete playerPtr;
同时 native 内部 shared_ptr 析构。
结果:
同一块内存被释放两次
问题核心:
shared_ptr 共享的是控制块,不是裸指针。
多个 shared_ptr 不能从同一个裸指针构造。
六、第二类问题:逻辑野指针
排查中还发现另一类隐蔽问题:
std::thread([self = shared_from_this()]() {
self->renderLoop();
}).detach();
现象:
-
Java 层认为对象已销毁
-
线程仍持有 shared_ptr
-
对象未析构
-
但 Surface / EGL 已销毁
render 继续调用:
eglSwapBuffers()
最终崩在:
libEGL.so
vendor driver
这不是 UAF。
这是:
逻辑野指针
对象内存仍在。
但业务语义已经失效。
Sanitizer 抓不到这种问题。
只能通过:
-
生命周期分析
-
线程模型梳理
定位。
七、为什么 join 也不能完全解决?
即便改成:
m_thread.join();
如果:
-
Java 先销毁 Surface
-
EGLContext 被 destroy
-
渲染线程还在最后一次 swap
依然会崩。
join 只能保证线程结束。
不能保证:
外部资源生命周期仍然有效。
八、工程治理措施
整改包括:
1. 禁止从裸指针构造 shared_ptr
统一使用:
std::make_shared<T>()
2. JNI 不暴露裸指针
Java 不再持有 T*
统一由 native 管理生命周期。
3. 跨线程使用 weak_ptr
避免线程劫持生命周期。
4. CI 引入 Sanitizer 构建
- Debug 默认开启 ASan
- ARM64 灰度 HWASan
- 多线程模块定期跑 TSan
九、排障方法论总结
当在监控后台看到:
-
崩溃在 libc / EGL / vendor
-
没有业务堆栈
-
随新模块上线增加
-
偶现难复现
优先怀疑:
内存破坏 + 生命周期错位
排查路径:
- 版本 diff
- 检查 shared_ptr 用法
- 查是否裸指针混用
- 查线程是否持有 shared_ptr
- 开启 ASan / HWASan
- 梳理资源销毁顺序
十、结语
智能指针不会让你自动安全。
它只是把:
手动 delete
变成:
引用计数归零时 delete
如果:
-
所有权设计混乱
-
生命周期边界不清晰
-
线程模型不明确
它只会制造更隐蔽的崩溃。
这次事故的本质,不是 C++ 语法问题。
而是:
生命周期设计问题。