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里被记录着,内核一并释放掉就可以了。

相关推荐
charlie1145141912 小时前
通用GUI编程技术——图形渲染实战(四十三)——D3D12设计哲学:显式控制与性能解锁
学习·3d·c·图形渲染·win32
liulilittle2 天前
TCP UCP v1.0:BBR 的非破坏性约束层
网络·c++·网络协议·tcp/ip·算法·c·通信
lightqjx4 天前
【Linux】第一个小程序:进度条
linux·服务器·学习·缓存·c·进度条实现
Bruce_kaizy5 天前
c++ linux环境编程——从应用层到linux内核深入了解文件io的调用机制(爆肝)
linux·c++·c·嵌入式linux·文件io
光电笑映7 天前
从环境变量到进程虚拟地址空间——Linux 内存管理的底层脉络
linux·服务器·c++·c
GanGanGanGan_7 天前
CentOS 7.9 glibc 2.17 源码编译升级到 glibc 2.31
linux·运维·centos·glibc
charlie1145141919 天前
AwesomeQt:最小的Qt6系列迷你版本教程发布!
linux·c++·qt·c
weixin_4217252611 天前
2026年C/C++/C#全解析:底层语言的进化与场景抉择,选错直接掉队
c语言·c++·c·编程语言·技术选择
charlie11451419113 天前
Linux 字符设备驱动:cdev、设备号与设备模型
linux·开发语言·驱动开发·c