Skip to content

进程退出时在 atexit 中手动 delete 全局 bvar::LatencyRecorder 导致 UAF Coredump #3265

@wqshr12345

Description

@wqshr12345

Describe the bug
我们在运行单元测试结束时(以及偶发于线上服务停止时),遇到了进程 Coredump 的问题。崩溃的堆栈指向了 bvar 内部的 CHECK 失败:

F0409 17:44:02.410187 1487207 variable.
cpp:197] Check failed: false 
`spatcher_write_latency_percentiles' must 
exist
*** Check failure stack trace: ***

注意到这里报错的变量名原本应该是 event_dispatcher_write_latency_percentiles ,但其开头的 8 个字节丢失,被替换成了不可读的乱码。

通过排查 brpc/src/brpc/event_dispatcher.cpp 的代码,我们找到了根本原因。全局的指标指针 g_edisp_read_lantency 和 g_edisp_write_lantency 在 StopAndJoinGlobalDispatchers() 函数中被显式 delete 了,而这个函数是通过 atexit 注册的退出清理钩子:

static void StopAndJoinGlobalDispatchers
() {
    // ... join 所有的 epoll 线程 ...
    delete g_edisp_read_lantency;   // 
    <--- 问题出在这里
    delete g_edisp_write_lantency;  // 
    <--- 问题出在这里
}

尽管业务层面的 epoll 线程已经被 Join,但 bvar 框架自身存在一个后台采样线程( bvar_sampler_collector )。这个线程是一个 Leaky Singleton,伴随进程存活,永远不会被 Join 停止。当主线程通过 atexit 析构并释放这些 LatencyRecorder 时,如果后台采样线程恰好正在并发访问它们,就会发生 Use-After-Free (UAF) 。底层内存分配器(如 tcmalloc)会立刻将释放内存块的前 8 个字节覆写为一个指向空闲链表下一个节点的指针(Free-list pointer)。这直接导致了 bvar 内部的 std::string _name 被破坏(表现为前 8 个字符变成乱码),随后在 bvar::Variable::hide() 查找 Map 时必然失败并触发 CHECK(false) 导致 Coredump。
Versions
1.14.1
OS:
Compiler:
brpc:
protobuf:

Additional context/screenshots

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions