上周排查CamX进程的chifeature2base组件时,遇到一个极其诡异的内存问题。日志中明确报出"Cause: use-after-free",但常规的内存检测工具却无法准确定位到问题源头。这个案例的特殊性在于:崩溃现场的函数调用栈显示内存访问发生在对象释放后,但代码审查却显示所有引用计数操作都符合预期。
CamX作为图像处理流水线框架,其chifeature2base模块负责基础特征提取。在连续处理高分辨率图像时,该模块会间歇性崩溃,崩溃点总是指向同一处内存地址。通过HWASAN(硬件辅助地址消毒)报告可以看到以下关键信息:
code复制==ERROR: HWASAN: tag-mismatch on address 0x0042...
READ of size 4 at 0x0042... thread T0
#0 chifeature2base::FeatureProcessor::process()
#1 chifeature2base::PipelineNode::execute()
#2 camx::Node::ProcessRequest()
0x0042... is located 20 bytes inside of 80-byte region [0x0042...,0x0042...)
freed by thread T0 here:
#0 free
#1 chifeature2base::FeatureData::~FeatureData()
#2 RefCounted::Release()
allocated by thread T0 here:
#0 malloc
#1 chifeature2base::FeatureData::Create()
这个报告表面看是典型的UAF(Use-After-Free),但深入分析后发现三个反常现象:
HWASAN作为基于ARMv8.5-A架构的硬件辅助检测工具,其核心机制是通过内存标记(Memory Tagging)实现实时越界检测。每个内存分配会获得一个随机4-bit tag,指针会存储相同的tag。当访问内存时,硬件会比较指针tag与内存tag,不匹配即触发异常。
在本次案例中,HWASAN报告显示访问的是已释放内存(tag不匹配),但我们需要更精确的追踪手段。通过以下命令启用增强检测:
code复制export HWASAN_OPTIONS=stack_history_size=7:heap_history_size=5
这会在崩溃时保留更多的调用栈历史,帮助我们重建内存对象的完整生命周期。
由于标准工具无法解释引用计数正常但仍有UAF的现象,我们增加了以下追踪点:
cpp复制void AddRef() {
m_count++;
LOGD("AddRef %p count=%d callers:%s",
this, m_count, GetCallStack().c_str());
}
void Release() {
m_count--;
LOGD("Release %p count=%d callers:%s",
this, m_count, GetCallStack().c_str());
if(m_count == 0) delete this;
}
python复制import gdb
class MemWatchpoint(gdb.Breakpoint):
def __init__(self, addr):
super().__init__("*" + hex(addr), gdb.BP_WATCHPOINT)
def stop(self):
frame = gdb.selected_frame()
print(f"Access at {frame.name()} from {frame.block()}")
return True
通过增强日志发现关键线索:存在一个时间窗口,其中:
这种情况在常规检测中很难发现,因为:
CamX采用三级流水线线程模型:
code复制SensorThread → PipelineThread → JobDispatcher
↘ MetadataThread ↗
chifeature2base模块运行在JobDispatcher线程,但会跨线程共享FeatureData对象。问题复现需要满足以下时序:
问题核心在于弱引用升级为强引用的实现:
cpp复制RefCounted* WeakRef::Lock() {
if(m_ptr && m_ptr->m_weakCount > 0) {
m_ptr->AddRef(); // 竞态条件点
return m_ptr;
}
return nullptr;
}
当执行m_ptr->AddRef()时,对象可能已被其他线程销毁。修复方案是引入双检查锁:
cpp复制RefCounted* WeakRef::Lock() {
auto ptr = m_ptr.load(std::memory_order_acquire);
if(!ptr) return nullptr;
std::lock_guard lock(m_mutex);
if(ptr->m_weakCount > 0) {
ptr->AddRef();
return ptr;
}
return nullptr;
}
在ARM架构下,还需要添加内存屏障确保可见性:
cpp复制void Release() {
int newCount = --m_count;
std::atomic_thread_fence(std::memory_order_release);
if(newCount == 0) {
std::atomic_thread_fence(std::memory_order_acquire);
delete this;
}
}
构造特定测试用例强制触发竞态条件:
python复制def stress_test():
obj = create_feature_data()
threads = []
for i in range(8):
t = Thread(target=race_thread, args=(obj,))
threads.append(t)
t.start()
while obj.refcount > 1:
obj.release()
for t in threads:
t.join()
修复前后的性能对比数据(基于SDM865平台):
| 测试场景 | 原方案(ms) | 修复方案(ms) | 开销 |
|---|---|---|---|
| 1080p单帧 | 12.4 | 12.6 | +1.6% |
| 4K连拍(10帧) | 143.2 | 145.8 | +1.8% |
| 弱引用高频调用 | 89.7 | 92.1 | +2.7% |
在delete后立即填充特殊模式:
cpp复制~FeatureData() {
// 填充不可访问模式
memset(this, 0xDEADBEEF, sizeof(*this));
m_magic = 0;
actual_delete(this);
}
引入带线程安全检查的智能指针:
cpp复制template<typename T>
class SafePtr {
public:
explicit SafePtr(T* ptr) : m_ptr(ptr) {
if(m_ptr) m_ptr->Lock();
}
~SafePtr() {
if(m_ptr) m_ptr->Unlock();
}
// 禁用拷贝构造/赋值
private:
T* m_ptr = nullptr;
};
bash复制echo "module=chifeature2base" > /sys/kernel/debug/hwasan/filter
bash复制export HWASAN_OPTIONS=report_threads=1:report_errors=2
经验总结:在多线程环境下,引用计数只能保证内存不会提前释放,但不能保证对象可用性。必须配合适当的同步原语才能实现真正的线程安全。