CVE-2022-22057_GPU漏洞分析(非完整版)

分析背景

目前发布的很多中高端手机中都存在GPU 芯片,这些GPU芯片和NPU芯片一起被集成在CPU 芯片中,当前主流的GPU芯片厂商主要是英伟达,高通晓龙,联发科。本文分析高通骁龙的GPU 驱动中存在的UAF 漏洞,在kernel 中,是以kgsl 驱动提供给用户空间,此驱动提供给用户空间接口来与Adreno GPU 进行通信。由于应用程序需要访问驱动程序才能进行自我渲染,并且需要以极低的延迟来调用GPU进行工作, 也是使用高通芯片组的手机可以从第三方应用程序直接访问的少数驱动程序之一,另外 kgsl 驱动代码对内存共享逻辑的复杂性,可能会出现内存问题,比如 条件竞争下的UAF、double free。

当前公开披露的GPU 漏洞和漏洞报告少之又少,分析难度也很大,本文从CVE-2022-22057 漏洞入手,分析 kernel msm 中的 gpu ,了解 GPU 的漏洞挖掘和分析思路。

主要参考报告:
https://github.blog/security/vulnerability-research/the-android-kernel-mitigations-obstacle-race/
https://securitylab.github.com/advisories/GHSL-2022-037_msm_kernel/
这些报告已经写的很详细了,看完报告后,虽然大概的理解了漏洞诱因,仍然是似懂非懂,很不过瘾,于是便入手找到源码,开始分析漏洞原因,这里不得不说一句,使用 cursor 来协助进行源代码分析可以很好的帮助理解某些函数的功能,以及kernel 特定的宏和函数定义。

adreno KGSL driver 初始化

Adreno GPU 子系统的架构图如下图所示,图来源于: https://docs.qualcomm.com/bundle/publicresource/topics/80-70014-19/graphics-overview.html

在一个装载有Adreno GPU 的设备中,为了能在GPU 中执行计算,应用程序利用Wayland 协议,使用 EGL/OpenGL ES 来开始图形渲染,其中会涉及到一些渲染的命令传递给OpenGL ES,随后在kernel 层,这些命令会被编译成GPU 指令集,通过 KGSL驱动 发送到GPU 芯片。

根据架构图大概知道GPU芯片主要是与 kgsl 驱动 在kernel 和hardware 之间进行交互。kgsl 驱动在设备中表现为 /dev/kgsl-3d0, 其中的 0 表示GPU 处理单元的序号。

这里梳理一下有关Adreno GPU 在kernel 中初始化加载的流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int __init kgsl_3d_init(void) --->  1
struct platform_driver adreno_platform_driver ---> 2
int adreno_probe(struct platform_device *pdev) ---> 3
int adreno_bind(struct device *dev) ---> 4
struct adreno_gpu_core * adreno_identify_gpu(struct platform_device *pdev, u32 *chipid) ---> 5
struct adreno_gpu_core * _get_gpu_core(struct platform_device *pdev, unsigned int chipid) ---> 6
struct adreno_gpu_core *adreno_gpulist[] ---> 7
const struct adreno_a3xx_core adreno_gpu_core_a306 --> 8
struct adreno_gpudev adreno_a3xx_gpudev ---> 9
a3xx_probe(struct platform_device *pdev,
        u32 chipid, const struct adreno_gpu_core *gpucore) ---> 10
adreno.c/adreno_device_probe(struct platform_device *pdev,struct adreno_device *adreno_dev) ---> 11
adreno.c/adreno_setup_device(struct adreno_device *adreno_dev) ---> 12
kgsl.c/kgsl_device_platform_probe(struct kgsl_device *device) ---> 13
kgsl.c/_register_device(struct kgsl_device *device) ---> 14
device_create(kgsl_driver.class,&device->pdev->dev,dev, device,device->name);

\1. kernel 会根据设备的芯片型号,触发gpulist 中对应的回调函数。
\2.a3xx_probe 函数是Adreno A3XX 系列GPU 的探测函数主要用于初始化GPU 设备。
\3. 经过一些配置后,会调用probe 函数,将gpu 的进行初始化。
\4. 接着会进入到adreno_device_probe 函数中,初始化设备,比如kgsl-3d0,设置设备驱动数据。
\5. kgsl_device_platform_probe函数会创建字符设备到/dev,初始化电源,物理地址到虚拟地址的映射等。
\6. kgsl_core_init 用于 kgsl 驱动初始化,注册 kgsl_fops 中的ioctl 回调函数。

驱动ioctl 注册和调用过程

ioctl 注册 和 调用 流程:

1
2
3
4
5
6
7
adreno.c /kgsl_3d_init(void) --->  1
kgsl.c / __init kgsl_core_init(void) ---> 2
kgsl.c / struct file_operations kgsl_fops ---> 3
kgsl_ioctl.c / long kgsl_ioctl(struct file *filep, unsigned int cmd, unsigned long arg) ---> 4
kgsl_ioctl.c / long kgsl_ioctl_helper(struct file *filep, unsigned int cmd, unsigned long arg, const struct kgsl_ioctl *cmds, int len) ---> 5
kgsl_ioctl.c / struct kgsl_ioctl kgsl_ioctl_funcs[] ---> 6
kgsl_timeline.c / long kgsl_ioctl_timeline_fence_get(struct kgsl_device_private *dev_priv, unsigned int cmd, void *data) ---> 7

首先是进行init 初始化内核驱动模块,将 kgsl_fops 文件操作结构体注册到kgsl-3d0 设备驱动内,具体的file_operations 所示:

1
2
3
4
5
6
7
8
9
10
static const struct file_operations kgsl_fops = {
.owner = THIS_MODULE,
.release = kgsl_release,
.open = kgsl_open,
.mmap = kgsl_mmap,
.read = kgsl_read,
.get_unmapped_area = kgsl_get_unmapped_area,
.unlocked_ioctl = kgsl_ioctl,
.compat_ioctl = kgsl_compat_ioctl,
};

其中的 ioctl(kgsl_ioctl) 主要调用kgsl_ioctl_helper 来区分用户空间传入的ioctl 操作码对应的回调函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
long kgsl_ioctl(struct file *filep, unsigned int cmd, unsigned long arg)
{
struct kgsl_device_private *dev_priv = filep->private_data;
struct kgsl_device *device = dev_priv->device;
long ret;
ret = kgsl_ioctl_helper(filep, cmd, arg, kgsl_ioctl_funcs,
ARRAY_SIZE(kgsl_ioctl_funcs));
/*
* If the command was unrecognized in the generic core, try the device
* specific function
*/
if (ret == -ENOIOCTLCMD) {
if (is_compat_task() && device->ftbl->compat_ioctl != NULL)
return device->ftbl->compat_ioctl(dev_priv, cmd, arg);
else if (device->ftbl->ioctl != NULL)
return device->ftbl->ioctl(dev_priv, cmd, arg);
}
return ret;
}

这些不同ioctl cmd 的回调统一由 kgsl_ioctl_func 进行管理,如下所示,

1
2
3
4
5
6
7
8
9
10
11
12
KGSL_IOCTL_FUNC(IOCTL_KGSL_TIMELINE_CREATE,
kgsl_ioctl_timeline_create),
KGSL_IOCTL_FUNC(IOCTL_KGSL_TIMELINE_WAIT,
kgsl_ioctl_timeline_wait),
KGSL_IOCTL_FUNC(IOCTL_KGSL_TIMELINE_FENCE_GET,
kgsl_ioctl_timeline_fence_get),
KGSL_IOCTL_FUNC(IOCTL_KGSL_TIMELINE_QUERY,
kgsl_ioctl_timeline_query),
KGSL_IOCTL_FUNC(IOCTL_KGSL_TIMELINE_SIGNAL,
kgsl_ioctl_timeline_signal),
KGSL_IOCTL_FUNC(IOCTL_KGSL_TIMELINE_DESTROY,
kgsl_ioctl_timeline_destroy),

如果传入 IOCTL_KGSL_TIMELINE_FENCE_GET,将调用 kgsl_ioctl_timeline_fence_get 回调来处理传入的data数据。

漏洞分析

根据漏洞描述和其他的漏洞分析报告,可以明确的知道这是一个由条件竞争引发的 refcount 计数错误造成的释放后重用的漏洞。而且产生漏洞的代码是当时在Qualcomm msm5.4 kernel 分支中引入新功能以及相关的新的ioctl 时产生的。

这个漏洞主要发生在kgsl_timeline.c 中,其中有一些相关的ioctl 进行调用,这些函数均注册在kgsl_ioctl_func 数组内。

本漏洞的条件竞争主要涉及这三个函数:

  • kgsl_ioctl_timeline_fence_get(struct kgsl_device_private *dev_priv, unsigned int cmd, void *data) 或者 kgsl_ioctl_timeline_wait()
  • timeline_fence_release(struct dma_fence *fence)
  • kgsl_ioctl_timeline_destroy(struct kgsl_device_private *dev_priv, unsigned int cmd, void *data)

接下来将围绕这三个函数进行介绍:

kgsl_ioctl_timeline_fence_get

kgsl_ioctl_timeline_fence_get函数可以通过 IOCTL_KGSL_TIMELINE_FENCE_GET 进行调用,这个函数执行逻辑如下:

  • kgsl_timeline_by_id 获取传入data 中定义的timeline
  • kgsl_timeline_fence_alloc 接着将生成一个 fence,并初始化 dma_fence 引用计数1,然后将生成的fence 添加到timeline 的fences列表并按照升序排列。
  • sync_file_create 函数创建一个sync_file 用于和文件描述符fd 对应,同时会将fence 引用计数再+1,然后将文件handle 返回给用户。
  • 函数执行到末尾会调用 dma_fence_put 来让在sync_file_create 增加的引用计数 -1, 因为fence在这里已经使用完了。
  • 但如果 fd 获取不到,那么将直接跳到 dma_fence_put 将fence 引用计数-1,而这会触发dma_fence_release。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    long kgsl_ioctl_timeline_fence_get(struct kgsl_device_private *dev_priv, unsigned int cmd, void *data) {
        struct kgsl_device *device = dev_priv->device;
        struct kgsl_timeline_fence_get *param = data;
        struct kgsl_timeline *timeline;
        struct sync_file *sync_file;
        struct dma_fence *fence;
        int ret = 0, fd;

        timeline = kgsl_timeline_by_id(device, param->timeline);
        if (!timeline)
            return -ENODEV;
        fence = kgsl_timeline_fence_alloc(timeline, param->seqno);      // 分配fence,fence引用计数+1  

        if (IS_ERR(fence)) {
            kgsl_timeline_put(timeline);
            return PTR_ERR(fence);
        }
        fd = get_unused_fd_flags(O_CLOEXEC);        // 获取一个未使用的fd
        if (fd < 0) {
            ret = fd;
            goto out;
        }
        sync_file = sync_file_create(fence); // 创建sync_file fence引用计数+1  = 2
        if (sync_file) {
            fd_install(fd, sync_file->file);
            param->handle = fd;
        } else {
            put_unused_fd(fd);
            ret = -ENOMEM;
        }
    out:
        dma_fence_put(fence); // fence -1 = 1    kref_put(&fence->refcount, dma_fence_release); 如果引用计数为0,则调用dma_fence_release
        kgsl_timeline_put(timeline);
        return ret;
    }
    为了能够理解dms_fence 和fence 的概念,这是kgsl_timeline_fence 结构体
    1
    2
    3
    4
    5
    struct kgsl_timeline_fence {
        struct dma_fence base;
        struct kgsl_timeline *timeline;
        struct list_head node;
    };
    在经过kgsl_timeline_fence_alloc 函数执行的时候,声明的是kgsl_timeline_fence 结构体,其中包含 dma_fence 和 kgsl_timeline 结构体。可能是便于将dma_fence 添加到 kgsl_timeline 时间线内。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    struct dma_fence *kgsl_timeline_fence_alloc(struct kgsl_timeline *timeline,u64 seqno)
    {
        struct kgsl_timeline_fence *fence;

        fence = kzalloc(sizeof(*fence), GFP_KERNEL);    // 分配fence
        ...
        fence->timeline = kgsl_timeline_get(timeline);     //  增加timeline的引用计数
    ...
    dma_fence_init(&fence->base, &timeline_fence_ops,
            &timeline->lock, timeline->context, seqno);  // 初始化fence 并且将fence引用计数设置为1
    ...
        kgsl_timeline_add_fence(timeline, fence);   // 将 fence 添加到 timeline 的 fences 列表中,并且fences 按照升序排列   
        return &fence->base;   

当使用dma_fence_put 减少引用计数时,如果计数降到0,就会触发dma_fence_release 函数。

1
2
3
4
5
static inline void dma_fence_put(struct dma_fence *fence)
{
    if (fence)
        kref_put(&fence->refcount, dma_fence_release);
}

在dma_fence_release 函数中,会触发 release(timeline_fence_release )回调:

1
2
3
4
    if (fence->ops->release)
        fence->ops->release(fence);
    else
        dma_fence_free(fence);

其中timeline_fence_release 函数会释放 dma_fence

timeline_fence_release

当dma_fence 引用计数为0 的之后会触发dma_fence_put 函数 中的timeline_fence_release 函数。这个函数会调用dma_fence_free 来释放dma_fence。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static void timeline_fence_release(struct dma_fence *fence)
{
    struct kgsl_timeline_fence *f = to_timeline_fence(fence); // 获取fence对应的timeline_fence
    struct kgsl_timeline *timeline = f->timeline;
    struct kgsl_timeline_fence *cur, *temp;
    unsigned long flags;

    spin_lock_irqsave(&timeline->fence_lock, flags); // 锁住fence_lock
    /* If the fence is still on the active list, remove it */
    list_for_each_entry_safe(cur, temp, &timeline->fences, node) {
        if (f != cur)
            continue;
        list_del_init(&f->node); // 删除fence
        break;
    }
    spin_unlock_irqrestore(&timeline->fence_lock, flags); // 解锁fence_lock
    trace_kgsl_timeline_fence_release(f->timeline->id, fence->seqno);
    kgsl_timeline_put(f->timeline);
    dma_fence_free(fence); // 释放fence
}

kgsl_ioctl_timeline_destroy

kgsl_ioctl_timeline_destroy 函数是用于销毁timeline 中的fence。

  • 前面一个锁内的内容,遍历&timeline->fences 列表内的所有fence,然后增加一个引用计数。然后将timeline->fences 复制到另一个列表 temp,现在temp 中包含了所有的fence。
  • 后面的锁内的内容,会一次遍历temp 中的每个fence。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    long kgsl_ioctl_timeline_destroy(struct kgsl_device_private *dev_priv,
            unsigned int cmd, void *data) // 销毁timeline
    {
        struct kgsl_device *device = dev_priv->device;
        struct kgsl_timeline_fence *fence, *tmp;
        struct kgsl_timeline *timeline;
        struct list_head temp;
        u32 *param = data;
        ...
        INIT_LIST_HEAD(&temp);
        spin_lock(&timeline->fence_lock);   // 锁住fence_lock
        list_for_each_entry_safe(fence, tmp, &timeline->fences, node)
            dma_fence_get(&fence->base);
        list_replace_init(&timeline->fences, &temp);
        spin_unlock(&timeline->fence_lock); // 解锁fence_lock

        spin_lock_irq(&timeline->lock); // 锁住lock
        list_for_each_entry_safe(fence, tmp, &temp, node) {
            dma_fence_set_error(&fence->base, -ENOENT);
            dma_fence_signal_locked(&fence->base);
            dma_fence_put(&fence->base);
        }
        spin_unlock_irq(&timeline->lock); // 解锁lock
        kgsl_timeline_put(timeline);
        return timeline ? 0 : -ENODEV;
    }

悬空指针的产生

UAF 漏洞触发的关键是,在释放内存后产生悬空指针。在timeline_fence_release函数释放fence会产生一个dma_fence 指针,因此如果dma_fence指针变成了悬空指针,那么就有触发 use 的可能性。

前面我们了解到,如果在kgsl_ioctl_timeline_fence_get 函数中,文件描述符被用户关闭了,就会立马调用 timeline_fence_release 释放它们。如果有另一个线程在timeline_fence_release 释放之后获取线程锁,并获取kgsl_timeline_fence引用,就会导致dma_fence悬空指针被使用。

多线程条件竞争操作如下所示:

在 Thread1 中,kgsl_ioctl_timeline_destroy函数会将dma_fence 加锁,防止其他的线程操作这个指针,在 Thread1 中的第一个线程锁内,会调用dma_fence_get 函数给fence增加 refcount ,然后将timeline 中的fences列表清空并整个复制给 temp临时变量。
同时在 Thread2 中,如果进入到 kgsl_ioctl_timeline_fence_get 函数内,但用户关闭 和 sync_file 绑定的文件描述符 fd 时,会调用dma_fence_put函数减少 fence 的引用计数,从而触发 timeline_fence_release 函数。
在条件竞争的状态下,timeline_fence_release 函数会接过 timeline->fence_lock ,进而释放fence,造成悬空指针。
在 Thread2 释放完fence内存之后, Thread1 会继续接管 timeline->lock ,然后遍历temp临时变量中 fence 引用,从而触发释放后重用。

漏洞缓解措施

kgsl_ioctl_timeline_destroy 函数中的一块锁的区域中加入了kref_get_unless_zero 函数用以对fence 的refcount 判断,如果当前fence的refcount 为 0 ,那就直接将这个fence节点从列表内移除。

1
2
3
4
5
6
spin_lock(&timeline->fence_lock);
list_for_each_entry_safe(fence, tmp, &timeline->fences, node)
if (!kref_get_unless_zero(&fence->base.refcount))
list_del_init(&fence->node);
list_replace_init(&timeline->fences, &temp);
spin_unlock(&timeline->fence_lock);

另外修复的情况还有另一处: kgsl_timeline_signal , 代码逻辑和 kgsl_ioctl_timeline_destroy 是一致的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void kgsl_timeline_signal(struct kgsl_timeline *timeline, u64 seqno)
{
struct kgsl_timeline_fence *fence, *tmp;
struct list_head temp;
INIT_LIST_HEAD(&temp);
spin_lock_irq(&timeline->lock);
if (seqno < timeline->value)
goto unlock;
trace_kgsl_timeline_signal(timeline->id, seqno);
timeline->value = seqno;
spin_lock(&timeline->fence_lock);
list_for_each_entry_safe(fence, tmp, &timeline->fences, node)
if (timeline_fence_signaled(&fence->base) &&
kref_get_unless_zero(&fence->base.refcount))
list_move(&fence->node, &temp);
spin_unlock(&timeline->fence_lock);
list_for_each_entry_safe(fence, tmp, &temp, node) {
dma_fence_signal_locked(&fence->base);
dma_fence_put(&fence->base);
}
unlock:
spin_unlock_irq(&timeline->lock);
}

总结

现阶段归纳总结了不少有关内核驱动的漏洞,目前在google 和Qualcomm 安全公告中相当多的漏洞高危漏洞是由于条件竞争引发的UAF,或者是由隐含假设造成的引用计数错误的问题,以及OOB。发现这些漏洞目前最有效的办法,就是关注kernel分支中引入的功能,以及添加的新的代码,虽然这些新引入的内容在发布之前仍然进行了源代码审计和安全性评估,但依旧可能存在一些没有考虑到的方面,比如条件竞争、共享内存分配管理、引用计数是否合理等等。
比如本篇文章中是在Qualcomm msm5.4 内核分支中引入的kgsl_timeline 功能的同时,加入了大量的新ioctl,这些ioctl 是可以直接从用户空间调取,这些新的代码会导致新的问题出现。因此在进行kernel 安全研究的时候,如果持续关注这些kernel新的特性,或许会有机会。


CVE-2022-22057_GPU漏洞分析(非完整版)
https://tig3rhu.github.io/2024/12/20/36__CVE-2022-22057_GPU_漏洞/
Author
Tig3rHu
Posted on
December 20, 2024
Licensed under