二进制漏洞分析-35.Samsung NPU的Reversing与 Exploiting (第一部分下)
2024-1-7 09:22:17 Author: 安全狗的自我修养(查看原文) 阅读量:12 收藏

调度算法

要了解该算法,让我们来看看schedule_task。我们可以看到,位字段中的值被检索,然后用作索引。g_priority_table

/* Finds the priority of the next task to schedule */
u32 prio_grp0_idx = g_scheduler_state.prio_grp0;
u32 prio_grp0_val = g_priority_table[prio_grp0_idx];
u32 prio_grp1_idx = g_scheduler_state.prio_grp1[prio_grp0_val];
u32 prio_grp1_val = g_priority_table[prio_grp1_idx];
u32 prio_grp2_idx = g_scheduler_state.prio_grp2[prio_grp0_val][prio_grp1_val];
u32 prio_grp2_val = g_priority_table[prio_grp2_idx];

我们从下表中得到的值(如下所示)实际上是最低有效集位的索引。

u8 g_priority_table[] = {
0,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,
4,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,
5,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,
4,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,
6,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,
4,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,
5,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,
4,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,
7,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,
4,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,
5,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,
4,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,
6,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,
4,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,
5,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,
4,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0,
};

例如,假设您有一个介于 和 之间的值,则在此索引处返回的值为 ,因为 和 设置的最低有效位位于 索引 。由于给定位在位域中的位置直接由任务的优先级决定,因此找到最低有效位集可以让我们提取和重建可以调度的最低优先级值。0x000x1000xc830xc8 = 0b110010003g_scheduler_state

使用此属性,我们能够计算下一个要调度的任务的优先级,即优先级值最低的任务。下面是此过程的示例,其中包含优先级为 54、77、153 和 252 的四项任务。我们可以看到,在应用算法后,我们得到了最低优先级值,在这种情况下。54

找到最低优先级值后,检查它是否低于当前正在运行的任务的优先级,选择相应的就绪列表并运行该列表中的第一个任务。schedule_taskg_scheduler_state.ready_list

使用调度程序

在这一点上,我们对调度程序如何做出决策有了很好的了解。但是,我们仍然不知道是什么触发了调度程序。我们通常最熟悉的操作系统使用抢占来停止运行时间过长的任务。NPU 的情况并非如此;据我们所知,没有抢占,任务必须显式地让出 CPU 才能运行其他任务。这可以通过多种方式完成,例如通过使进程休眠、等待事件等,但是它要求相关任务在某个时间点显式调用这些函数,否则它们将独占 CPU 时间。

上面列出的所有操作都会在某个时候调用函数调度。它负责保存当前任务的上下文,调用(在上一节中已解释),如果要计划的任务与当前任务不同,则执行上下文切换。schedule_task

调度程序唯一需要解释的部分是用于管理任务的不同列表。

准备清单

如前所述,就绪列表是按其所包含任务的优先级编制索引的列表数组。可以使用 __add_to_ready_list 和 __del_from_ready_list 在就绪列表中添加和删除任务。简而言之,这些函数更新优先级组,并在与任务优先级对应的条目中添加或删除任务。它们只是上一节中解释的算法的扩展。g_scheduler_stateg_scheduler_state.ready_list

延迟名单

在其他操作系统上,可以使进程在给定的持续时间内休眠。这就是 NPU 中延迟列表的用途。为了管理时间的流逝并知道何时唤醒任务,每个计时器滴答时发生的计时器 IRQ 的处理程序都会调用 schedule_tick,这会将任务的延迟减少 1。计时器滴答以及调度程序时间片的长度为 1 毫秒。

可以使用函数 __add_to_delayed_list 延迟任务。这个想法是将任务添加到 ,但需要先按延迟排序。需要注意的重要一点是,延迟列表中的任务延迟是相对的。例如,如果任务 A 延迟了 3 个计时器滴答,任务 B 延迟了 5 个计时器滴答,则 A 将延迟 3 添加到列表中,但 B 将延迟 2 (since )。下图说明了将任务添加到延迟列表的过程。g_scheduler_state.delayed_list3 + 2 = 5

可以通过两种方式从延迟列表中删除任务。

  • 第一个是对 __del_from_delayed_list 的显式调用,不会等待延迟自行完成。它采用我们想要唤醒的任务,将其延迟添加到下一个任务中,并将其从列表中删除。

  • 第二种方法是等待延迟达到零。每次调用时,它都会将延迟列表中第一个任务的延迟减少 1,但它也会检查一个或多个任务是否达到零(或更小)的延迟以唤醒它们。schedule_tick

待定名单

待处理列表用于引用等待特定事件发生的任务(例如,等待释放的锁)。

与就绪列表和延迟列表相反,待处理列表不是 的一部分,而是由以下结构定义的列表的一部分:g_scheduler_stateworkqueue

struct workqueue {
u32 task;
struct list_head head;
u32 service;
void* obj;
};

workqueue-s 与特定对象(信号量或事件)相关联,当任务等待来自这些对象之一的操作时,它会被添加到待处理列表中。此操作由最终调用 __add_to_pending_list 的函数执行。在待处理列表中,可以按优先级对任务进行排序,但这取决于字段的值(0 = 无排序,1 = 按优先级排序)。可以使用__del_from_pending_list删除任务,这基本上只是将任务与相关待处理列表取消链接。struct list_head headservice

我们将在信号量和事件部分中提供有关如何使用此列表的更多详细信息。

定时器

与时间相关的一切都源于函数timer_irq_handler,该函数每 1 毫秒调用一次。

void timer_irq_handler() {
time_tick();
if ( !g_scheduler_state.scheduler_stopped )
schedule_tick();
}

time_tick递增全局滴答计数器(例如,在调试消息中使用),但也处理计时器。NPU 可能会使用计时器以固定间隔和/或给定时间执行功能。但是,此版本的 NPU 似乎并非如此,因此我们将保持本节简短并跳到下一部分。

事件

由于 NPU 与其他组件交互,并且在某种程度上与设备用户交互,因此交换本质上是异步的。需要有一种能够管理这些类型通信的机制。

为了处理此问题,NPU 使用名为 的对象,这些对象由以下结构定义:events

struct event {
u32 magic;
u32 id;
u32 _unk_0c;
u32 state;
struct list_head event_list_entry;
struct list_head waiting_list_entry;
struct workqueue wq;
};

正如我们稍后将看到的,事件用于使函数等待特定事件。例如,我们不知道何时将新邮件发送到邮箱。因此,操作系统不是在循环中主动等待,而是使任务进入休眠状态,将其添加到等待列表中,并在事件实际发生时将其从等待列表中取出以处理传入的消息。

事件在函数events_init中初始化,该函数配置活动事件、待处理和事件列表、计数和事件对象(直接存储在全局状态中)的数量。g_events_state

创建新事件是通过调用 __alloc_event 进行的。该函数在数组中查找一个空位,然后:g_events_state.events

  • 设置其 ID(使用作为参数传递的值);

  • 将其状态设置为EVENT_READY;

  • 将其添加到全局事件列表中g_events_state.event_list;

  • 更新 的计数、标志等;g_events_state

  • 初始化事件的 WorkQueue。

释放过程与__free_event完全相反,并在 中实现。它将取消初始化工作队列并重置 中的事件。__alloc_eventg_events_state

关于事件的使用,实现了两个功能。

  • 名思义,__wait_event使任务等待特定事件发生。此函数只是获取当前正在运行的任务,将其从就绪列表中删除,然后将其添加到事件工作队列的待处理列表中。此外,如果尚未完成,则该事件将添加到相应 ID(即 )的等待列表中。可以有多个事件使用相同的 ID,因此可以在同一等候列表中。struct list_head event_waiting_lists[MAX_EVENT_IDS + 1]

  • 用于表示事件发生的函数是__set_event_no_schedule。这似乎有点复杂,但事实并非如此,因为大多数代码都是重复的。一般的想法是,该函数从事件中获取 ID,并在 中检索与之关联的等待列表。然后,它会遍历等待列表中的每个事件,并尝试唤醒等待这些事件的所有任务。g_events_state

下图给出了此等待/设置过程的简化表示。

信号灯

信号量在操作系统中用于控制对共享资源的访问,并防止出现争用条件等问题。计数与它们相关联,每次任务采用信号量时,其计数都会减少 1。当计数为零时,下一个尝试获取信号量的任务必须等待另一个任务返回其信号量。

在 NPU 中,信号量使用的全局结构由以下结构定义:g_semaphores_state

struct semaphores_state_t {
u32 nb_semaphores;
struct list_head semaphore_list;
};

semaphores_init只是初始化信号量计数和信号量列表。此时,可以使用 __create_semaphore 创建新的信号量。它初始化其工作队列、计数和名称,然后将其添加到全局信号量列表中。相反,使用__delete_semaphore删除信号量,这将取消初始化并清理工作队列以及其余信号量参数。

信号量的行为与事件的行为非常相似。当使用 __down 获取信号量时,它只是将计数递减 1 个调用。但是,当计数小于零时,它会从就绪列表中删除当前任务,将其添加到信号量工作队列的待处理列表中,并计划新任务。用__up回馈信号量的工作方式与此类似。如果信号量的工作队列为空,则表示没有任务在信号量上等待,因此我们可以简单地将计数递增 1。但是,如果列表不为空,则需要唤醒等待的任务,并使用 __up_sema_no_schedule 将其添加回就绪列表。

沟通渠道

本节介绍NPU和AP(即运行Android的应用处理器)之间的通信信道的初始化。但是,与此部分相关的某些功能要么无关紧要,要么未使用,或者我们根本不确定它们的作用。值得庆幸的是,它们对于全面了解正在发生的事情并不太重要。

配置在函数comm_channels_init中进行。它从对邮箱的初始化开始,调用mailbox_init。此函数分配事件并设置中断处理程序,以通知系统高优先级和低优先级邮箱中的传入邮件。下一步是调用 cmdq_init,它通过执行与 类似的操作来配置 CMDQ(命令队列?)子系统。mailbox_init

其余部分初始化其他组件,但我们无法弄清楚它们究竟做了什么,或者系统的其余部分如何使用它们。comm_channels_init

运行系统

我们终于到达了初始化过程的尾声。在有效启动系统之前,剩下要做的最后一件事是配置处理 AP 和 NPU 之间所有交互的核心任务。在 NPU 的上下文中,这些任务称为本机任务,并基于以下结构:

struct native_task {
u32 unknown[0xc];
u32 id;
char name[8];
u32 priority;
u32 handler;
u32 max_sched_slices;
u32 stack_addr;
u32 stack_size;
struct task task;
u32 unknown;
};

本机任务在函数run_native_tasks中设置,其中有 11 个。下表总结了大部分信息。

编号名字优先权Sched 切片堆栈底座堆栈大小
0x00__星期一0x000x640x378000x400
0x01_怠0xff0x640x37c000x200
0x02__低0x140x640x37e000x800
0x03_高0x0a0x640x386000x400
0x04_RSPS0x090x640x38a000x400
0x05__RPT0x140x640x38e000x400
0x06__IMM0x0e0x640x392000x800
0x07__蝙蝠0x0f0x640x39a000x800
0x08工作Q00x150x640x3a2000x1000
0x09工作问10x160x640x3b2000x200
0x0A工作Q20x170x640x3b4000x200

对于每个本机任务结构,都会调用 以有效地实例化关联的任务。 然后,在最终调用schedule_start之前将它们添加到各自的就绪列表中。create_taskresume_task

功能:schedule_start

  • 查找优先级组中编码的最低优先级值(在本例中为 0);

  • 将 和 设置为 0,以向操作系统表示现在可以进行调度;g_scheduler_state.scheduler_stoppedg_scheduler_state.forbid_scheduling

  • 将上下文切换到最高优先级的任务(在本例中为监视器)。

我们不会在本文中详细介绍所有本机任务,而只是简要描述一下它们的作用。

  • 监视器:初始化用于神经计算的内部库、IOE 调度程序 (?)、处理 AP 发送的对象的 NCP 管理器,最后是邮箱。它还每秒检查一次任务是否未超时。

  • 空闲任务:不执行任何操作,仅用于填充,直到另一个任务准备好运行。

  • 高优先级和低优先级邮箱:接收和处理来自AP的请求。

  • 响应任务:应答AP。

  • 上报任务:向AP发送日志。

  • Imm & Bat Dispatchers:不幸的是,我们没有花时间研究这些,因此不知道它们的作用。

  • 作业队列:仅打印调试消息并返回。

在下一节中,我们将提供有关邮箱实现的更多详细信息,并解释如何从 AP 与 NPU 进行交互。

正如我们在前面的部分中看到的,可以使用其邮箱向 NPU 发送邮件。但是,我们还没有解释它们的实现。由于通信通道也有一部分在 Android 内核中实现,因此我们将详细介绍从组成 ioctl 的那一刻到内核接收来自 NPU 的响应所执行的请求所执行的步骤。

在内核和NPU之间共享资源

在讨论 NPU 和 AP 之间的通信之前,我们需要解释它们如何能够交换数据,更具体地说,这两个芯片之间的内存共享是如何工作的。

我们之前已经介绍了负责执行此映射的函数init_iomem_area。它解析文件 arch/arm64/boot/dts/exynos/exynos9830.dts,更具体地说,解析下面给出的部分。

npu_exynos {
...
samsung,npumem-address = <0x00 0x17800000 0x100000 0x00 0x17900000 0x100000 0x00 0x17a00000 0x100000 0x00 0x17b00000 0x40000 0x00 0x17c00000 0x100000 0x00 0x17d00000 0x100000 0x00 0x17e00000 0x100000 0x00 0x17f00000 0x100000 0x00 0x1e00c000 0x1000 0x00 0x1e00b000 0x1000 0x00 0x10040100 0x08 0x00 0x106f0000 0x1000 0x00 0x179c0000 0x10000 0x00 0x17ac0000 0x10000 0x00 0x19400000 0x200000 0x00 0x50000000 0xe0000 0x00 0x50100000 0x200000 0x00 0x50300000 0x200000>;
samsung,npumem-names = "SFR_DNC\0SFR_NPUC0\0SFR_NPUC1\0TCUSRAM\0SFR_NPU0\0SFR_NPU1\0SFR_NPU2\0SFR_NPU3\0SFR_CORESIGHT\0SFR_STM\0SFR_MCT_G\0PWM\0MAILBOX0\0MAILBOX1\0IDPSRAM\0FW_DRAM\0FW_UNITTEST\0FW_LOG";
...
};

它从 中提取内存区域名称以及 中的相应映射。映射按照下面给出的结构进行编码:samsung,npumem-namessamsung,npumem-address

struct iomem_reg_t {
u32 dummy;
u32 start;
u32 size;
};

使用上述文件中的值,我们得到以下映射:

名字虚拟开始大小
SFR_DNC0x000x178000000x100000
SFR_NPUC00x000x179000000x100000
SFR_NPUC10x000x17a000000x100000
TCUSRAM (英语)0x000x17b000000x40000
SFR_NPU00x000x17c000000x100000
SFR_NPU10x000x17d000000x100000
SFR_NPU20x000x17e000000x100000
SFR_NPU30x000x17f000000x100000
SFR_CORESIGHT0x000x1e00c0000x1000
SFR_STM0x000x1e00b0000x1000
SFR_MCT_G0x000x100401000x08
脉宽调制(PWM)0x000x106f00000x1000
MAILBOX00x000x179c00000x10000
MAILBOX10x000x17ac00000x10000
IDPSRAM0x000x194000000x200000
FW_DRAM0x000x500000000xe0000
FW_UNITTEST0x000x501000000x200000
FW_LOG0x000x503000000x200000

我们在这里感兴趣的是 .它将 NPU 固件映射到第一个字节,将共享数据映射到其余字节。当遍历不同的映射时,一旦到达 ,它就会调用函数npu_memory_alloc_from_heap在内核和 NPU 之间创建共享内存映射。FW_DRAM0x800000x60000init_iomem_areaFW_DRAM

static int init_iomem_area(struct npu_system *system)
{
/* [...] */
iomem_count = of_property_count_strings(
dev->of_node, "samsung,npumem-names");
/* [...] */
for (i = 0; i < iomem_count; i++) {
ret = of_property_read_string_index(dev->of_node,
"samsung,npumem-names", i, &iomem_name[i]);
/* [...] */
}
/* [...] */
ret = of_property_read_u32_array(dev->of_node, "samsung,npumem-address", (u32 *)iomem_data,
iomem_count * sizeof(struct iomem_reg_t) / sizeof(u32));
/* [...] */
for (i = 0; iomem_name[i] != NULL; i++) {
/* [...] */
if (init_data[di].heapname) {
/* [...] */
ret = npu_memory_alloc_from_heap(system->pdev, *bd,
(iomem_data + i)->start, init_data[di].heapname);
/* [...] */
}
/* [...] */
}
/* [...] */
}

然后,npu_memory_alloc_from_heap在调用 iommu_map 之前获取与设备关联的 IOMMU 域。

static int npu_memory_alloc_from_heap(struct platform_device *pdev, struct npu_memory_buffer *buffer, dma_addr_t daddr, phys_addr_t paddr, const char *heapname)
{
/* [...] */
struct iommu_domain *domain = iommu_get_domain_for_dev(&pdev->dev);
ret = iommu_map(domain, daddr, paddr, size, 0);
if (ret) {
npu_err("fail(err %pad) in iommu_map\n", &daddr);
ret = -ENOMEM;
goto p_err;
}
/* [...] */
}

iommu_map最终调用,通过 IOMMU 有效地映射 NPU 和 AP 之间的内存区域。domain->ops->map

int iommu_map(struct iommu_domain *domain, unsigned long iova,
phys_addr_t paddr, size_t size, int prot)
{
/* [...] */
ret = domain->ops->map(domain, iova, paddr, pgsize, prot);
/* [...] */
}

但是,您可能想知道域是什么以及与之相关的操作,为此,我们必须更深入地挖掘。从 NPU 设备初始化到设置域的路径如下:

  • npu_device_init

  • platform_driver_register(使用 platform_bus_type)

  • __platform_driver_register

  • driver_register

  • bus_add_driver

  • driver_attach

  • __driver_attach

  • driver_probe_device

  • really_probe

  • dma_configure(从platform_bus_type使用.dma_configure)

  • platform_dma_configure

  • of_dma_configure

  • of_iommu_configure

  • of_iommu_xlate(从exynos_iommu_ops使用.of_xlate)

  • exynos_iommu_of_xlate

exynos_iommu_of_xlate根据客户端列表查找与驱动程序关联的域。

static int exynos_iommu_of_xlate(struct device *master,
struct of_phandle_args *spec)
{
/* [...] */
if (!owner) {
/* [...] */
list_for_each_entry_safe(client, buf_client,
&exynos_client_list, list) {
if (client->master_np == master->of_node) {
domain = client->vmm_data->domain;
vmm_data = client->vmm_data;
list_del(&client->list);
kfree(client);
break;
}
}
/* [...] */
owner->domain = domain;
owner->vmm_data = vmm_data;
owner->master = master;
/* [...] */
}
/* [...] */
}

IOMMU 域的客户端在设备树文件中的字段 中定义。它们引用与此域关联的组件。例如,在 中,客户端是 和 。domain-clientsphandleiommu-domain_dnc0x1620x163

iommu-domain_dnc {
compatible = "samsung,exynos-iommu-bus";
#dma-address-cells = <0x01>;
#dma-size-cells = <0x01>;
dma-window = <0x80000000 0x50000000>;
domain-clients = <0x162 0x163>;
};

0x163对应于 NPU。phandle

npu_exynos {
...
phandle = <0x163>;
};

这些值在函数exynos_iommu_create_domain中解析,该函数也调用 exynos_create_single_iovmm

static int __init exynos_iommu_create_domain(void)
{
struct device_node *domain_np;
int ret;

for_each_compatible_node(domain_np, NULL, "samsung,exynos-iommu-bus") {
/* [...] */
ret = of_get_dma_window(domain_np, NULL, 0, NULL, &d_addr, &d_size);
if (!ret) {
/* [...] */
start = d_addr;
end = d_addr + d_size;
}
/* [...] */
while ((np = of_parse_phandle(domain_np, "domain-clients", i++))) {
if (!vmm) {
vmm = exynos_create_single_iovmm(np->name,
start, end);
/* [...] */
/* HACK: Make one group for one domain */
domain = to_exynos_domain(vmm->domain);
vmm->group = iommu_group_alloc();
iommu_attach_group(vmm->domain, vmm->group);
}
/* Relationship between domain and client is added. */
ret = exynos_client_add(np, vmm);
/* [...] */
}
/* [...] */
}

return 0;
}

exynos_create_single_iovmm然后调用 iommu_domain_alloc,这是 __iommu_domain_alloc 的包装器。

struct exynos_iovmm *exynos_create_single_iovmm(const char *name,
unsigned int start, unsigned int end)
{
/* [...] */
vmm->domain = iommu_domain_alloc(&platform_bus_type);
}
struct iommu_domain *iommu_domain_alloc(struct bus_type *bus)
{
return __iommu_domain_alloc(bus, IOMMU_DOMAIN_UNMANAGED);
}

最后,在 __iommu_domain_alloc 中,设置为 。domain->opsplatform_bus_type.iommu_ops

static struct iommu_domain *__iommu_domain_alloc(struct bus_type *bus,
unsigned type)
{
/* [...] */
domain->ops = bus->iommu_ops;
/* [...] */
}

iommu_ops设置为exynos_iommu_init调用bus_set_iommu

static int __init exynos_iommu_init(void)
{
/* [...] */
ret = bus_set_iommu(&platform_bus_type, &exynos_iommu_ops);
/* [...] */
}
static struct iommu_ops exynos_iommu_ops = {
.domain_alloc = exynos_iommu_domain_alloc,
.domain_free = exynos_iommu_domain_free,
.attach_dev = exynos_iommu_attach_device,
.detach_dev = exynos_iommu_detach_device,
.map = exynos_iommu_map,
.unmap = exynos_iommu_unmap,
.iova_to_phys = exynos_iommu_iova_to_phys,
.pgsize_bitmap = SECT_SIZE | LPAGE_SIZE | SPAGE_SIZE,
.of_xlate = exynos_iommu_of_xlate,
};

毕竟,我们知道当被调用时,它实际上是被执行的。domain->ops->mapiommu_mapexynos_iommu_map

int iommu_map(struct iommu_domain *domain, unsigned long iova,
phys_addr_t paddr, size_t size, int prot)
{
/* [...] */
ret = domain->ops->map(domain, iova, paddr, pgsize, prot);
/* [...] */
}

现在我们的共享内存映射已经创建,我们可以开始分析源自内核并发送到 NPU 的请求所采用的路径。

从内核到 NPU

为了说明用户发送的请求所采用的路径,我们将以 ioctl 为例。VS4L_VERTEXIOC_S_FORMAT

Ioctl 处理从函数 vertex_ioctl 开始,在我们的示例中到达大小写VS4L_VERTEXIOC_S_FORMAT

long vertex_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
/* [...] */
int ret = 0;
struct vision_device *vdev = vision_devdata(file);
const struct vertex_ioctl_ops *ops = vdev->ioctl_ops;

/* temp var to support each ioctl */
union {
/* [...] */
struct vs4l_format_list vsf;
/* [...] */
} vs4l_kvar;

/* [...] */
switch (cmd) {
/* [...] */
case VS4L_VERTEXIOC_S_FORMAT:
ret = get_vs4l_format64(&vs4l_kvar.vsf,
(struct vs4l_format_list __user *)arg);
if (ret) {
vision_err("get_vs4l_format64 (%d)\n", ret);
break;
}

ret = ops->vertexioc_s_format(file, &vs4l_kvar.vsf);
if (ret)
vision_err("vertexioc_s_format (%d)\n", ret);

put_vs4l_format64(&vs4l_kvar.vsf,
(struct vs4l_format_list __user *)arg);
break;
/* [...] */

get_vs4l_format64只是从用户空间中检索结构并检查值是否合理。然后将执行流程移交给 ,它对应于函数 。ops->vertexioc_s_formatnpu_vertex_s_format

然后,npu_vertex_s_format在发送传出请求时调用 npu_session_NW_CMD_LOAD

static int npu_vertex_s_format(struct file *file, struct vs4l_format_list *flist)
{
/* [...] */
if (flist->direction == VS4L_DIRECTION_OT) {
ret = npu_session_NW_CMD_LOAD(session);
ret = chk_nw_result_no_error(session);
if (ret == NPU_ERR_NO_ERROR) {
vctx->state |= BIT(NPU_VERTEX_FORMAT);
} else {
goto p_err;
}
}
/* [...] */
}

npu_session_NW_CMD_LOAD将当前会话和命令传递给 npu_session_put_nw_req

int npu_session_NW_CMD_LOAD(struct npu_session *session)
{
int ret = 0;
nw_cmd_e nw_cmd = NPU_NW_CMD_LOAD;

if (!session) {
npu_err("invalid session\n");
ret = -EINVAL;
return ret;
}

profile_point1(PROBE_ID_DD_NW_RECEIVED, session->uid, 0, nw_cmd);
session->nw_result.result_code = NPU_NW_JUST_STARTED;
npu_session_put_nw_req(session, nw_cmd);
wait_event(session->wq, session->nw_result.result_code != NPU_NW_JUST_STARTED);
profile_point1(PROBE_ID_DD_NW_NOTIFIED, session->uid, 0, nw_cmd);
return ret;
}

npu_session_put_nw_req填充请求对象并将其传递给 npu_ncp_mgmt_put

static int npu_session_put_nw_req(struct npu_session *session, nw_cmd_e nw_cmd)
{
int ret = 0;
struct npu_nw req = {
.uid = session->uid,
.bound_id = session->sched_param.bound_id,
.npu_req_id = 0,
.result_code = 0,
.session = session,
.cmd = nw_cmd,
.ncp_addr = session->ncp_info.ncp_addr,
.magic_tail = NPU_NW_MAGIC_TAIL,
};

BUG_ON(!session);

req.notify_func = get_notify_func(nw_cmd);

ret = npu_ncp_mgmt_put(&req);
if (!ret) {
npu_uerr("npu_ncp_mgmt_put failed", session);
return ret;
}
return ret;
}

npu_ncp_mgmt_put 将请求推送到 FIFO 列表中。FREE

int npu_ncp_mgmt_put(const struct npu_nw *frame)
{
int ret;

BUG_ON(!frame);
ret = kfifo_in_spinlocked(&ctx.ncp_mgmt_list, frame, 1, &ctx.ncp_mgmt_lock);
if (ret > 0) {
if (ctx.ncp_mgmt_callback) {
ctx.ncp_mgmt_callback();
}
}
return ret;
}

npu_protodrv_handler_nw_free retrieves this request and pushes it into the list.REQUESTED

static int npu_protodrv_handler_nw_free(void)
{
/* [...] */

/* Take a entry from FREE list, before access the queue */
while ((entry = proto_nw_lsm.lsm_get_entry(FREE)) != NULL) {
/* Is request available ? */
if (nw_mgmt_op_get_request(entry) != 0) {
/* [...] */
/* Move to REQUESTED state */
proto_nw_lsm.lsm_put_entry(REQUESTED, entry);
handle_cnt++;
goto finally;
/* [...] */

npu_protodrv_handler_nw_requested检索请求并将其传递给__mbox_nw_ops_put

static int npu_protodrv_handler_nw_requested(void)
{
/* [...] */
/* Process each element in REQUESTED list one by one */
LSM_FOR_EACH_ENTRY_IN(proto_nw_lsm, REQUESTED, entry,
/* [...] */
switch (entry->nw.cmd) {
/* [...] */
default:
/* Conventional command -> Publish message to mailbox */
if (__mbox_nw_ops_put(entry) > 0) {
/* Success */
proto_nw_lsm.lsm_move_entry(PROCESSING, entry);
proc_handle_cnt++;
}
break;
}
) /* End of LSM_FOR_EACH_ENTRY_IN */
/* [...] */
}

__mbox_nw_ops_put 只是 nw_mbox_ops_put 的包装器,它调用 npu_nw_mbox_ops_put

static int __mbox_nw_ops_put(struct proto_req_nw *entry)
{
/* [...] */
ret = nw_mbox_ops_put(entry);
/* [...] */
}
static int nw_mbox_ops_put(struct proto_req_nw *src)
{
return npu_nw_mbox_ops_put(&npu_proto_drv.msgid_pool, src);
}

然后,npu_nw_mbox_ops_put调用该请求,该请求解析为 .npu_if_protodrv_mbox_ops.nw_post_requestnw_req_manager

int npu_nw_mbox_ops_put(struct msgid_pool *pool, struct proto_req_nw *src)
{
/* [...] */
/* Generate mailbox message with given msgid and post it */
if (npu_if_protodrv_mbox_ops.nw_post_request)
ret = npu_if_protodrv_mbox_ops.nw_post_request(msgid, &src->nw);
/* [...] */
}
struct npu_if_protodrv_mbox_ops npu_if_protodrv_mbox_ops = {
/* [...] */
.nw_post_request = nw_req_manager,
/* [...] */
};

nw_req_manager格式化我们的消息并将其传递给npu_set_cmd。请注意,该请求应使用标志在低优先级邮箱中发送。NPU_MBOX_REQUEST_LOW

int nw_req_manager(int msgid, struct npu_nw *nw)
{
/* [...] */
switch (nw->cmd) {
/* [...] */
case NPU_NW_CMD_LOAD:
cmd.c.load.oid = nw->uid;
cmd.c.load.tid = nw->bound_id;
hdr_size = get_ncp_hdr_size(nw);
if (hdr_size <= 0) {
npu_info("fail in get_ncp_hdr_size: (%zd)", hdr_size);
ret = FALSE;
goto nw_req_err;
}

cmd.length = (u32)hdr_size;
cmd.payload = nw->ncp_addr.daddr;
msg.command = COMMAND_LOAD;
msg.length = sizeof(struct command);
break;
/* [...] */
}
msg.mid = msgid;

ret = npu_set_cmd(&msg, &cmd, NPU_MBOX_REQUEST_LOW);
if (ret)
goto nw_req_err;
/* [...] */
}

最后,npu_set_cmd调用将我们的消息写入共享内存并发送中断以表示已发送新消息。mbx_ipc_put

static int npu_set_cmd(struct message *msg, struct command *cmd, u32 cmdType)
{
int ret = 0;

ret = mbx_ipc_put((void *)interface.addr, &interface.mbox_hdr->h2fctrl[cmdType], msg, cmd);
if (ret)
goto I_ERR;
__send_interrupt(msg->command);
return 0
I_ERR:
switch (ret) {
case -ERESOURCE:
npu_warn("No space left on mailbox : ret = %d\n", ret);
break;
default:
npu_err("mbx_ipc_put err with %d\n", ret);
break;
}
return ret;
}

在 NPU 中处理命令

在执行的这个阶段,我们知道邮件正在低优先级邮箱中等待,并且中断已发送到 NPU。在本节中,我们将简要介绍邮箱控件,NPU 和 AP 使用这些控件来传达其读/写游标在共享环形缓冲区中的位置。然后,我们详细介绍了 NPU 如何接收、解析和处理请求。最后,我们解释如何将响应发送回内核。

邮箱控件

NPU 使用的邮箱的实现分为五个部分。一个是邮箱的标头,另外四个是以下人员使用的环形缓冲区:

  • 低优先级邮箱;

  • 高优先级邮箱;

  • 响应邮箱;

  • 报表邮箱。

其布局如下所示:

邮箱头使用以下结构进行定义,用于跟踪环形缓冲区中的不同读/写指针。struct mailbox_hdr

struct mailbox_hdr {
u32 max_slot;
u32 debug_time;
u32 debug_code;
u32 log_level;
u32 log_dram;
u32 reserved[8];
struct mailbox_ctrl h2fctrl[MAILBOX_H2FCTRL_MAX];
struct mailbox_ctrl f2hctrl[MAILBOX_F2HCTRL_MAX];
u32 totsize;
u32 version;
u32 signature2;
u32 signature1;
};

NPU 使用的四个邮箱控制器由监视器通过调用函数 mailbox_controller_init 进行初始化。

mailbox_controller_init首先调用 init_ncp_handlers 以初始化 AP 发送的命令的处理程序。

u32 mailbox_controller_init() {
/* [...] */

/* Initializes the NCP handlers used for neural computation */
init_ncp_handlers(&g_ncp_handler_state);

/* [...] */
}

这些处理程序是在 NCP 处理程序全局状态结构中设置的。g_ncp_handler_state

void init_ncp_handlers(struct ncp_handler_state_t *ncp_handler_state) {
ncp_handler_state->_unk_0x364 = 4;

/* Sets all messages as unused */
for (int i = 0; i < NB_MESSAGES; i++) {
ncp_handler_state->messages[i].inuse = 0;
}

/* Initializes the handlers for the request commands */
ncp_handler_state->handlers[0] = ncp_manager_load;
ncp_handler_state->handlers[1] = ncp_manager_unload;
ncp_handler_state->handlers[2] = ncp_manager_process;
ncp_handler_state->handlers[3] = profile_control;
ncp_handler_state->handlers[4] = ncp_manager_purge;
ncp_handler_state->handlers[5] = ncp_manager_powerdown;
ncp_handler_state->handlers[7] = ncp_manager_policy;
ncp_handler_state->handlers[6] = ut_main_func;
ncp_handler_state->handlers[8] = ncp_manager_end;
}

mailbox_controller_init然后,对高优先级和低优先级邮箱调用mbx_dnward_init两次,mbx_upward_init响应邮箱和报告邮箱。mbx_report_init

u32 mailbox_controller_init() {
/* [...] */

/* Initializes the low priority mailbox */
ret = mbx_dnward_init(&g_mailbox_h2fctrl_lpriority, 0,
&g_mailbox_hdr.h2fctrl[0].sgmt_ofs, 0x80000);

/* Initializes the high priority mailbox */
ret = mbx_dnward_init(&g_mailbox_h2fctrl_hpriority, 2,
&g_mailbox_hdr.h2fctrl[1].sgmt_ofs, 0x80000);

/* Initializes the response mailbox */
ret = mbx_upward_init(&g_mailbox_f2hctrl_response, 3,
&g_mailbox_hdr.f2hctrl[0].sgmt_ofs, 0x80000);

/* Initializes the report mailbox */
ret = mbx_report_init(&g_mailbox_f2hctrl_report, 4,
&g_mailbox_hdr.f2hctrl[1].sgmt_ofs, 0x80000);

/* [...] */
}

简而言之,这些函数配置与相应邮箱关联的控件值、邮件列表和事件。以下各节详细介绍了这些不同的设置。

向下邮箱:接收来自 AP 的邮件

向下邮箱接收并处理来自 AP 的传入请求。低优先级邮箱和高优先级邮箱的邮件处理是相同的,这就是为什么我们采用低优先级邮箱来说明本节中的说明。这些邮箱基于以下结构:

struct mailbox_dnward {
u32 event_id_off;
struct event* event;
u32 start;
struct mailbox_ctrl *hctrl;
struct mailbox_ctrl fctrl;
};

请求处理在邮箱任务处理程序中开始:

  • TASK_mailbox_lowpriority。此函数是一个无限循环,它调用两个函数:

  • mbx_dnward_get:根据邮箱中找到的值创建邮件对象。

  • mbx_msghub_req:处理请求并将邮件发送到响应邮箱。

在调用mbx_ipc_get_msg之前,等待设置事件,这意味着邮箱中有新邮件可用。此函数使用 AP 发送的值创建一个新的消息对象。mbx_dnward_get

NPU 为验证消息是否确实可用而执行的另一项检查是检查 中的值。struct mailbox_ctrl

struct mailbox_ctrl {
u32 sgmt_ofs;
u32 sgmt_len;
u32 wptr;
u32 rptr;
};

hctrl是主机的状态和固件的状态。这些结构还将读取和写入指针存储在邮箱的环形缓冲区中。在AP发送消息的情况下,当固件的读取指针和主机的写入指针都指向同一位置时,则表示没有新消息,所有请求都已经处理完毕,因为固件“赶上”了主机。但是,如果主机已经发送了一条消息,但固件还没有机会处理它,则意味着主机领先于固件,即它的写入指针指向固件的读取指针指向的位置之后的位置。当它是由 NPU 发送的消息时,同样的情况也适用。下面是此过程的图示。fctrl

邮箱中的邮件分为两部分:

  • 遵循以下格式的标头struct message;

  • 有效载荷。

struct message {
u32 magic;
u32 mid;
u32 command;
u32 length;
u32 self;
u32 data;
};

length表示有效负载的大小及其与邮箱段开头的偏移量。例如,低优先级邮箱段位于地址。如果低优先级邮箱中的邮件位于偏移量,则有效负载的地址为 。data0x80000-0x1400 = 0x7ec00data0x600x7ec60

然后,提取的消息将传递给 ,后者将从消息中检索命令并调用相应的 NCP 处理程序。此处理程序在某处执行实际的神经处理。但是,本文不会对此进行解释。但需要注意的一点是,所有处理程序都调用以下函数:mbx_msghub_req

  • mbx_msghub_inp:在处理程序的开头设置消息的状态从 到RESPONSE_READYRESPONSE_IN_PROGRESS;

  • mbx_msghub_res:在处理程序的末尾,用结果值填充响应。

如果一切按预期进行,则操作结果存储在 a 中,如果发生错误,则存储在 a 中。由于这两种结构非常相似,因此仅给出以下说明:struct ncp_messagestruct ncp_errorncp_message

struct ncp_message {
struct message msg;
struct message result;
struct mailbox_dnward *mbox;
u32 sgmt_cursor;
u32 _unk_38;
u32 _unk_3c;
u32 _unk_40;
u32 _unk_44;
u32 _unk_48;
u32 _unk_4c;
u32 _unk_50;
u32 id;
u32 _unk_58;
u32 _unk_5c;
u32 state;
u32 _unk_64;
};

在这两种情况下,生成的响应都会传递给函数 mbx_upward_issue,该函数会将结果添加到要发送到 AP 的待处理响应列表中。我们将在下一节中回到这个函数。

向上邮箱:向 AP 发送消息

现在我们的请求已经处理完毕,其结果也可用,我们可以详细说明将响应发送回内核所需的最后步骤。负责此操作的邮箱是响应邮箱,它使用下面给出的结构来跟踪其状态。

struct mailbox_upward {
u32 event_id_off;
struct event* event;
u32 start;
struct mailbox_ctrl *hctrl;
struct response responses[NB_MESSAGES];
struct response_list available;
struct response_list pending;
};

此结构中的两个列表和 用于引用:availablepending

  • 可用于存储结果的可用响应;

  • 等待发送到内核的待处理响应。

若要将响应添加到待处理列表,请使用函数 mbx_upward_issue。它从可用列表中获取一个可用的响应对象,将结果附加到该对象,并使用add_to_rsp_list将其添加到待处理列表中。最后,它通知响应任务有一条新消息可用。__set_event

我们之前已经看到响应任务的处理程序是TASK_mailbox_response的。这是一个调用三个函数的无限循环。

  • mbx_upward_get:它等待响应,当响应可用时,从待处理列表中获取该响应,并从中检索原始请求的结果。wait_event

  • mbx_dnward_put:更新主机的读取指针,以指向下一条未处理的消息。

  • mbx_upward_put:写入响应并将有效负载复制到响应邮箱中。它还会更新主机的写入指针。

从 NPU 回到内核

我们终于进入了发送结果并准备好由内核处理的阶段。本部分回溯响应所采用的路径,一直追溯到原始请求。

函数 nw_rslt_manager 探测邮箱,以检查新邮件是否已到达。为此,它会查看我们之前介绍的邮箱控件结构中的值。

int nw_rslt_manager(int *ret_msgid, struct npu_nw *nw)
{
/* [...] */
ret = mbx_ipc_peek_msg((void *)interface.addr, &interface.mbox_hdr->f2hctrl[0], &msg);
/* [...] */
ret = mbx_ipc_get_msg((void *)interface.addr, &interface.mbox_hdr->f2hctrl[0], &msg);
/* [...] */
ret = mbx_ipc_get_cmd((void *)interface.addr, &interface.mbox_hdr->f2hctrl[0], &msg, &cmd);
/* [...] */
}

nw_rslt_manager实际上是在函数npu_nw_mbox_ops_get中调用的回调。

struct npu_if_protodrv_mbox_ops npu_if_protodrv_mbox_ops = {
/* [...] */
.nw_get_result = nw_rslt_manager,
/* [...] */
};
int npu_nw_mbox_ops_get(struct msgid_pool *pool, struct proto_req_nw **target)
{
/* [...] */
if (npu_if_protodrv_mbox_ops.nw_get_result)
ret = npu_if_protodrv_mbox_ops.nw_get_result(&msgid, &nw);
/* [...] */
}

对的调用来自包装器nw_mbox_ops_getnpu_nw_mbox_ops_get

static int nw_mbox_ops_get(struct proto_req_nw **target)
{
return npu_nw_mbox_ops_get(&npu_proto_drv.msgid_pool, target);
}

然后我们可以看到,它起源于npu_protodrv_handler_nw_processing。此函数将响应放入队列中。nw_mbox_ops_getCOMPLETED

static int  npu_protodrv_handler_nw_processing(void)
{
/* [...] */
while (nw_mbox_ops_get(&entry) > 0) {
/* [...] */
default:
/* Result code already set on nw_mbox_ops_get() -> Just change its state */
proto_nw_lsm.lsm_move_entry(COMPLETED, entry);
handle_cnt++;
break;
}
/* [...] */
}
/* [...] */
}

最后,调用 npu_protodrv_handler_nw_completed 来处理列表中的响应。如果一切按预期进行,它将运行之前在 npu_session_put_nw_req 中设置的回调,并调用 nw_mgmt_op_put_resultCOMPLETED

static int npu_protodrv_handler_nw_completed(void)
{
int ret = 0;
int handle_cnt = 0;
int entryCnt = 0; /* For trace */
struct proto_req_nw *entry;
struct session_ref_entry *session_ref_entry;
int transition;
char stat_buf[TIME_STAT_BUF_LEN];

/* Process each element in REQUESTED list one by one */
LSM_FOR_EACH_ENTRY_IN(proto_nw_lsm, COMPLETED, entry,
/* [...] */
switch (entry->nw.cmd) {
case NPU_NW_CMD_LOAD:
/* [...] */
transition = 1;
break;
/* [...] */
}
/* Post result if processing can be completed */
if (transition) {
/* [...] */
if (!nw_mgmt_op_put_result(entry)) {
npu_uinfo("(COMPLETED)NW: notification sent result(0x%08x)\n",
&entry->nw, entry->nw.result_code);
}
/* [...] */
}
) /* End of LSM_FOR_EACH_ENTRY_IN */
/* [...] */
}

此回调通知驱动程序响应已准备就绪,用户现在可以访问计算结果,从而结束内核和 NPU 之间的事务。

在本文中,从单个二进制文件中,我们设法很好地了解了三星为其神经处理单元实现的操作系统。还有很多地方需要涵盖,例如NCP处理程序或一般的神经计算例程,但至少我们知道操作系统核心组件的内部结构以及它如何与Android内核交互。本系列的下一篇文章将重点介绍我们在 NPU 中发现并利用的一些漏洞来攻击内核。

  • Exynos - 维基百科,自由的百科全书

    • https://en.wikipedia.org/wiki/Exynos

  • 共享内存解析中的三星 NPU(神经处理单元)内存损坏 - Project Zero 的 Bug 跟踪器

    • https://bugs.chromium.org/p/project-zero/issues/detail?id=2073

  • SM-G980F G980FXXS5CTL5固件 - SamMobile

    • https://www.sammobile.com/samsung/galaxy-s20/firmware/SM-G980F/XEF/download/G980FXXS5CTL5/1117440/

  • 手机源代码 - 三星开源

    • https://opensource.samsung.com/uploadList?menuItem=mobile&classification1=mobile_phone

  • 洞课程()

  • windows

  • windows()

  • USB()

  • ()

  • ios

  • windbg

  • ()


文章来源: http://mp.weixin.qq.com/s?__biz=MzkwOTE5MDY5NA==&mid=2247491091&idx=2&sn=92a18683c56c85d2399a946ebe902a7d&chksm=c019b01645ea11347e2f49bf1ab7eeb5e834ca9aa93dffada6a3ec9d6abca3ecf2dcb3bf2054&scene=0&xtrack=1#rd
如有侵权请联系:admin#unsafe.sh