CVE-2022-38181¶
约 1217 个字 53 行代码 预计阅读时间 7 分钟
Abstrct
Pixel 6 是第一台号称“全谷歌”的手机,但它存在一个“非谷歌”漏洞 —— Arm Mali GPU CVE-2022-38181,通过这个漏洞可以实现任意内核代码执行和获取 root 权限,本文将介绍这个漏洞的利用方法。
The Arm Mali GPU¶
Arm Mali GPU 被广泛集成于各类设备中。安卓手机的 GPU Driver 是一个易于受到攻击的目标,因为它们可以直接被不可信 app 访问并且所有的安卓设备 GPU 均为 Qualcomm's Adreno 或 Arm Mali GPU,这意味着任何微小的 bug 都将覆盖大量设备。
事实上,2021 年爆出的 7 个 Android 0-day 漏洞中有 5 个是针对 GPU 的,最近的在野利用 bug 为 CVE-2021-39793,于 2022 年 3 月披露。包括本漏洞在内,Qualcomm Adreno 和 Arm Mali GPU 各被爆出过 3 个漏洞。
由于用户空间应用和 GPU 间内存管理的复杂性,在 Arm Mali GPU 的内存管理代码中有许多隐患。本漏洞涉及到 GPU 内存中一个特殊类型:JIT memory
。
这里的 JIT
不是指编译里提到的即时编译,因为它是作为 non-excutable memory 创建的,被用作 GPU kernel Driver 的 memory cache,能够即时与用户程序共享。当内存不足时,它会返回给 kernel 使用。
许多其它类型的 GPU memory 都是通过 ioctl call(例如KBASE_IOCTL_MEM_IMPORT
) 直接创建的。但 JIT memory region 并非如此,它通过使用KBASSE_IOCTL_JOB_SUBMIT
ioctl 提交一个特殊的 GPU 指令来创建。
KBASE_IOCTL_JOB_SUBMIT
ioctl 可以被用来提交 "job chain" 给 GPU 处理。"job chain" 是包含多个 job 的列表,job 为不透明数据结构(opaque data structureKBASE_IOCTL_JOB_SUBMIT
通常用于向 GPU 发送指令,但也有一些 job 由 kernel 处理并运行在 host CPU 上。这些 software jobs(softjobs) 中包括 JIT memory 的 allocate 和 free(BASE_JD_REQ_SOFT_JIT_ALLOC and BASE_JD_REQ_SOFT_JIT_FREE)。
The life cycle of JIT memory¶
KBASE_IOCTL_JOB_SUBMIT
是一个通用的 ioctl 调用,包含许多负责处理不同类型 GPU jobs 的路径。BASE_JD_REQ_SOFT_JIT_ALLOC
本质上通过调用KBASE_JIT_allocate_process
,调用 KBASE_ JIT_allogate_process
来创建 JIT 内存区域。为了说明 JIT 内存的生命周期和使用情况,以下介绍一些相关概念。
当使用 Mali GPU 驱动时,用于程序首先需要创建并初始化 kbase_context
内核对象。涉及用户程序打开驱动文件并使用得到的文件描述符调用一系列 ioctl call。每个 file handle 都有各自的kbase_context
对象,负责管理打开的驱动文件的资源。它包括三个负责管理 JIT memory 的list_head
fields: thejit_active_head
,jit_pool_head
, jit_destroy_head
。正如其名,jit_active_head
包含当前正在使用的 JIT memory,jit_pool_head
包含可被再次使用的 JIT memory,jit_destroy_head
包含待销毁返回 kernel 的 JIT memory。
当kbase_jit_allocate
被调用时,它首先尝试在jit_pool_head
中找到合适的区域
if (info->usage_id != 0)
/* First scan for an allocation with the same usage ID */
reg = find_reasonable_region(info, &kctx->jit_pool_head, false);
...
if (reg) {
...
list_move(®->jit_node, &kctx->jit_active_head);
}
如果找到了合适的区域,它会被移动到jit_active_head
中,否则会创建新的内存区域并放入jit_active_head
中。被kbase_jit_allocate
分配的内存区域会通过kbase_jit_allocate_process
存储在kbase_context
的jit_alloc
数组中。
当用户程序不再需要 JIT memory 时,它会发送BASE_JD_REQ_SOFT_JIT_FREE
job 给 GPU,随后该 job 调用kbase_jit_free
释放内存。kbase_jit_free
并不会将内存区域的页直接返回给 kernel,而是先缩小内存区域到最小值,随后移除所有 CPU 侧的映射,如此一来区域中的页就不再被用户进程的地址空间可达。
void kbase_jit_free(struct kbase_context *kctx, struct kbase_va_region *reg)
{
...
//First reduce the size of the backing region and unmap the freed pages
old_pages = kbase_reg_current_backed_size(reg);
if (reg->initial_commit < old_pages) {
u64 new_size = MAX(reg->initial_commit,
div_u64(old_pages * (100 - kctx->trim_level), 100));
u64 delta = old_pages - new_size;
//Free delta pages in the region and reduces its size to old_pages - delta
if (delta) {
mutex_lock(&kctx->reg_lock);
kbase_mem_shrink(kctx, reg, old_pages - delta);
mutex_unlock(&kctx->reg_lock);
}
}
...
//Remove the pages from address space of user process
kbase_mem_shrink_cpu_mapping(kctx, reg, 0, reg->gpu_alloc->nents);
注意到区域 (reg) 的内存页在此阶段尚未被完全移除,并且 reg 也并非在这里被 free 的,它将被移入jit_pool_head
和kbase_context
的evict_list
中
kbase_mem_shrink_cpu_mapping(kctx, reg, 0, reg->gpu_alloc->nents);
...
mutex_lock(&kctx->jit_evict_lock);
/* This allocation can't already be on a list. */
WARN_ON(!list_empty(®->gpu_alloc->evict_node));
//Add reg to evict_list
list_add(®->gpu_alloc->evict_node, &kctx->evict_list);
atomic_add(reg->gpu_alloc->nents, &kctx->evict_nents);
//Move reg to jit_pool_head
list_move(®->jit_node, &kctx->jit_pool_head);
kbase_jit_free
完成后,其 caller kbase_jit_free_finish
将清除分配内存时存储在jit_alloc
中的 reference,即使 reg 在此阶段仍然有效。
static void kbase_jit_free_finish(struct kbase_jd_atom *katom)
{
...
for (j = 0; j != katom->nr_extres; ++j) {
if ((ids[j] != 0) && (kctx->jit_alloc[ids[j]] != NULL)) {
...
if (kctx->jit_alloc[ids[j]] !=
KBASE_RESERVED_REG_JIT_ALLOC) {
...
kbase_jit_free(kctx, kctx->jit_alloc[ids[j]]);
}
kctx->jit_alloc[ids[j]] = NULL; //<--------- clean up reference
}
}
...
}
正如我们之前看到的那样,jit_pool_head
中的内存区域可能在用户分配其他 JIT 区域时被重用。那么jit_destry_head
有什么用处呢?当 JIT 内存通过调用kbase_jit_free
被释放时,它同时也被放入evict_list
。evict_list
中的内存区域将在内存紧张时被释放。通过将不再使用的 JIT 区域放入evict_list
,Mali 驱动能够保留未使用的 JIT memory 用于快速重新分配,而在资源被需要时再返回给 kernel。
Linux 内核提供一种机制用于
创建日期: 2024年8月13日 19:15:40