GlibC 在线程里引发use-after-free退出时才崩溃原因与分析

背景

最近在做Blackberry时遇到了一个错误tcache_thread_shutdown(): unaligned tcache chunk detected,这个错误是发生在多线程情况下没有做好互斥时发生一个指针被free之后再次使用也就是比较经典的use-after-free的错误,触发了Abort,虽然不是什么大问题,很容易修复,但是这引发了我一个比较好奇的事情,就是为什么会这样?为什么free之后继续使用没报错但是线程退出时导致崩溃了?

原理

我使用的是GCC编译器,标准库是Glibc,对于Glibc而言,每次malloc的内存都要比预期申请的要大一点,因为里面要放入一些malloc的元数据,基本上malloc的内存布局是这样的:

bash 复制代码
[ malloc chunk header ][ 用户区 64 bytes ]
                       ^
                       |
                       p

p是返回给我们使用的user地址,每次当我们释放时Glibc并不会把它归还给系统,而是自己维护一个内存chunk把这次的Malloc内存放入一个tcache链表维护起来,这样后续在使用可以从这里拿内存提高分配效率,这里就有一个点,就是如果没有归还给系统的话页表的映射还在当前进程上面,也就是说你free之后依然可以随意访问这块地址,并不会发生什么非法地址访问的情况,下面是一个示例:

c 复制代码
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    char *p = malloc(64);
    if (!p) {
        return 1;
    }

    free(p);
    p[0] = 0x01;
    p[1] = 0x02;
    
    printf("hello world!\n");
    return 0;
}

此时hello world会正常打印并且也不会出现任何问题,这种做法与我们平时接触到的C语言教学背道而驰,平常教科书上的教学是针对C语言这门语言特性而Glibc是C语言的标准实现库,它有自己的行为,并且在不同系统上也有不同区别,但是这里我们换一种方式用线程来执行:

c 复制代码
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

static void *worker(void *arg)
{
    char *a = malloc(64);
    printf("[worker] a=%p\n", a);
    free(a);
    memset(a, 0x55, 64);
    printf("[worker] corrupted freed chunk, now return\n");
    return NULL;
}

int main(void)
{
    pthread_t tid;
    if (pthread_create(&tid, NULL, worker, NULL) != 0) {
        perror("pthread_create");
        return 1;
    }
    pthread_join(tid, NULL);
    printf("[main] worker joined\n");
    return 0;
}

运行结果:

bash 复制代码
[worker] a=0x79d9f0000b70
[worker] corrupted freed chunk, now return
tcache_thread_shutdown(): unaligned tcache chunk detected
[1]    29787 IOT instruction (core dumped)  ./a.out

可以看到被异常结束了,是Glibc主动调用了Abort函数发送SIGABRT异常信号结束当前进程并且打印了tcache_thread_shutdown(),这个原因是C语言的线程检查行为,对于Main而言它虽然也是一个线程但是Glibc对它行为是它是主线程,结束时Glibc调用exit来回收,它与普通线程行为不同,Main的链路是:

_start

-> __libc_start_main(...)

-> 调用main(argc, argv, envp)

-> main return

-> exit(main_return_value)

-> 执行 atexit/on_exit 回调

-> flush/close stdio

-> glibc 内部清理

-> _exit(status)

-> exit_group 系统调用

-> 内核销毁整个进程

普通线程行为调用的是:

pthread_create()

-> glibc clone创建线程

-> 新线程从glibc 的 start_thread入口开始

-> start_thread 调用你的 thread_func(arg)

-> thread_func return

-> glibc 执行 pthread_exit 收尾逻辑

-> 清理当前线程资源

-> 当前线程结束

注意这里调用的是pthread_exit,而不是系统的exit,这里它会去调用tcache_thread_shutdown来检查chunk有没有被破坏,这点你可以在它的源码里面找到,这里我找的是2.x:malloc/malloc.c的源码:

c 复制代码
static void
tcache_thread_shutdown (void)
{
  ......
    for (i = 0; i < TCACHE_MAX_BINS; ++i) {
        while (tcache_tmp->entries[i]) {
            tcache_entry *e = tcache_tmp->entries[i];

            if (__glibc_unlikely (!aligned_OK (e)))
                malloc_printerr ("tcache_thread_shutdown(): "
                                 "unaligned tcache chunk detected");

            tcache_tmp->entries[i] = REVEAL_PTR (e->next);
            __libc_free (e);
        }
    }
......
}

可以看到它在循环调用aligned_OK来检查tcache里面的指针是否被破坏的,也就是判断是不是Malloc合法边界,这个是看当前CPU位数,64位是16字节,32位是8字节,用MALLOC_ALIGNMENT 宏表示,在不同平台位数上它的内存颗粒不同,它会多分配一些字节一部分用于维护Malloc元数据一部分给用户使用,在64位上,即使你分配没有16个字节,它仍然会将你的分配按16字节对齐,如果tcache记录的地址里面有一块不是按16字节对齐的,那么就会立马出错,它认为当前线程破坏了tcache,导致内存模型出现了问题可能引发异常错误,就会直接abort,在malloc_printerr里面,会调用abort。

因此当我们平时调试的时候不要总是依靠C语言的标准答案,而是去参考具体平台和标准实现细节,例如对着Glibc库源码分析原因。

Main之所以没有做这层检查是因为Main结束整个应用都要结束了,因此没必要在做这些事情了,直接调用exit让系统一并将这个task全部清理掉,因为它所有的内存数据这些都在task_struct里被记录着,内核一并释放掉就可以了。

相关推荐
云泽8086 天前
Visual Studio 2026 新手保姆级教程:环境搭建、界面优化与编译器配置详解
开发语言·c++·c·开发工具·visual studio
众少成多积小致巨7 天前
C 核心概念、语法
c
C++ 老炮儿的技术栈7 天前
OpenCV 实现社保卡透视矫正:Hough 直线提取四角并转正证件图像
c语言·c++·人工智能·opencv·算法·计算机视觉·c
poll_Ff8 天前
CSAPP:Shell Lab笔记
c
会周易的程序员9 天前
Libnodave S7 通信库:架构设计与实现解析
linux·c++·物联网·架构·c·s7·工业协议
wdfk_prog12 天前
嵌入式面试真题第 10 题:高优化等级下共享状态可见性、内存模型与系统级同步设计
java·linux·开发语言·面试·职场和发展·架构·c
shx_wei20 天前
从函数调用到进程替换:使用 execl、CMake 与 PowerShell 构建一个多可执行程序
linux·c语言·windows·c
piaoyiren20 天前
普中A6 STC8A 动态数码管显示讲解
嵌入式硬件·51单片机·c
C++ 老炮儿的技术栈21 天前
MFC 自定义纯色居中文字进度条控件
c语言·数据库·c++·windows·算法·c·visual studio
liulilittle23 天前
KCC 拥塞控制算法缺陷修复记录(26/07/05)
网络·tcp/ip·计算机网络·c·信息与通信·tcp·通信