Zephyr用户模式-内存保护

Creative Commons
本作品采用知识共享署名

本文说明zephyr的用户模式下如何进行内存保护.

本文基于nrf52832分析cortex-m4下面zephyr如何使用mpu对用户线程进行内存隔离和保护.

内存对象

Zephyr用户模式-简介一文中已经介绍过Zephyr的内存保护手段有下面三种:

  • 线程堆栈
  • 内存域
  • 堆栈保护

其中线程堆栈和内存域是用户模式线程访问内存的基本对象

线程堆栈

线程堆栈是线程的栈空间,线程内的局部变量和线程函数调用的上下文都保存在线程的栈空间内,在zephyr中使用下面方式定义线程堆栈

1
K_THREAD_STACK_DEFINE(thread_stack, STACKSIZE);

展开宏可以看到,线程栈就是一个放在noinit section内的数组

1
2
#define K_THREAD_STACK_DEFINE(sym, size) \
struct _k_thread_stack_element __noinit __aligned(STACK_ALIGN) sym[size]

内存域(domain)

在zephyr下用户线程不能随意访问内存空间,用户线程之间除了通过内核交换数据外,还可以通过内存域的方法共享内存交换数据。
内存域的主要设计规则如下:

  • partition: 内存的基本单元,规定了内存的起始地址和大小
  • domain: partition的集合,一个domain可以包含多个partition,同一个partition可以出现在多个domain中
  • 一个用户线程只能绑定一个domain,用户线程拥有绑定domain内所有partition的访问权限
    从上面的设计规则可以看到,当一个partition同时属于2个domain,那么这两个domain的拥有者thread,就可以通过这个partition交换数据。下图摘自ELC2019 Europe,Andrew Boie的slider说明了thread1和thread2通过共享蓝色区域的partition交换数据。
    domain

Domain/Partition的使用

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
//定义4个partition
FOR_EACH(K_APPMEM_PARTITION_DEFINE, part0, part1, part2, part3, part4);
//定义3个domain,并为每个domain分配partition, 可以看到dom2和dom1都可以访问part3, dom0和dom1都可以访问part1
struct k_mem_domain dom0, dom1, dom2;
struct k_mem_partition *dom1_parts[] = {&part2, &part1, &part3};
struct k_mem_partition *dom2_parts[] = {&part4, &part3};
struct k_mem_partition *dom0_parts[] = {&part0, &part1};

//将thread和domain绑定,绑定后thread通过domain可共享内存交换数据,thread2和thread1都可以访问part3的内存, thread0和thread1都可以访问part1的内存
k_mem_domain_init(&dom1, 3, dom1_parts);
k_mem_domain_add_thread(&dom1, thread1);

k_mem_domain_init(&dom0, 2, dom0_parts);
k_mem_domain_add_thread(&dom0, thread0);

k_mem_domain_init(&dom2, 2, dom2_parts);
k_mem_domain_add_thread(&dom2, thread2);


//在part1的bss段中定义未初始化的变量,该变量可以被thread0和thread1访问
K_APP_BMEM(part1) char fBUFIN;
K_APP_BMEM(part1) char BUFIN[63];

//在part3的bss段中定义未初始化的变量,该变量可以被thread2和thread1访问
K_APP_BMEM(part3) char fBUFOUT;
K_APP_BMEM(part3) char BUFOUT[63];

//在part2的data段中定义初始化值的变量,该变量只能被threa1访问
K_APP_DMEM(part2) char W2[26] = START_WHEEL;
K_APP_DMEM(part2) char W3[26] = START_WHEEL;

当thread访问了不是自己doman partition内的内存时会出现exception

1
2
3
4
5
6
7
8
9
10
[00:00:27.203,948] <err> os: ***** MPU FAULT *****
[00:00:27.203,979] <err> os: Data Access Violation
[00:00:27.203,979] <err> os: MMFAR Address: 0x2000a118
[00:00:27.203,979] <err> os: r0/a1: 0x00000014 r1/a2: 0x00000014 r2/a3: 0x00000000
[00:00:27.203,979] <err> os: r3/a4: 0x2000a118 r12/ip: 0x00003a85 r14/lr: 0x00003a39
[00:00:27.203,979] <err> os: xpsr: 0x21000000
[00:00:27.204,010] <err> os: Faulting instruction address (r15/pc): 0x00031848
[00:00:27.204,010] <err> os: >>> ZEPHYR FATAL ERROR 0: CPU exception
[00:00:27.204,010] <err> os: Current thread: 0x2000078c (unknown)
[00:00:27.297,088] <err> os: Halting system

Partition Section

从前面的使用可以看出domain对partition是一个逻辑组织,和section没有实际的关联。这里我们看一下partition是如何放入section的, 这里分析两个宏K_APP_DMEM和K_APP_BMEM
zephyr/include/app_memory/app_memdomain.h

1
2
#define K_APP_DMEM(id) Z_GENERIC_SECTION(K_APP_DMEM_SECTION(id))
#define K_APP_BMEM(id) Z_GENERIC_SECTION(K_APP_BMEM_SECTION(id))

结合zephyr/include/toolchain/gcc.h的

1
2
#define __GENERIC_SECTION(segment) __attribute__((section(STRINGIFY(segment))))
#define Z_GENERIC_SECTION(segment) __GENERIC_SECTION(segment)

对下面定义进行展开

1
2
K_APP_BMEM(part1) char fBUFIN;
K_APP_DMEM(part2) char W2[26] = START_WHEEL;

展开为

1
2
__attribute__((section("data_smem_part1_bss"))) char fBUFIN;
__attribute__((section("data_smem_part2_data"))) char W2[26] = START_WHEEL;

可见用K_APP_DMEM(id)和K_APP_BMEM(id)定义的变量就是要把对应的变量定义放入id所在的data /bss section内。
再来看K_APPMEM_PARTITION_DEFINE的定义,就是将partition(id)所在的section(包含data/bss section)的起始地址和大小放入struct k_mem_partition 中, 并同时配置访问属性

1
2
3
4
5
6
7
8
9
10
11
#define Z_APP_START(id) z_data_smem_##id##_part_start
#define Z_APP_SIZE(id) z_data_smem_##id##_part_size

#define K_APPMEM_PARTITION_DEFINE(name) \
extern char Z_APP_START(name)[]; \
extern char Z_APP_SIZE(name)[]; \
struct k_mem_partition name = { \
.start = (u32_t) &Z_APP_START(name), \
.size = (u32_t) &Z_APP_SIZE(name), \
.attr = K_MEM_PARTITION_P_RW_U_RW \
}; \

从partition的section name可以看到,都包含data_smem的字符串,因此partiton会放入app_smem section,过程可以参考Zephyr libc简介和malloc分析一文的”K_APP_DMEM_SECTION”说明

内存对象信息

thread stack

thread stack信息在create thread的时候被保存到struct k_thread 中的stack_info中

domain

domain信息struct k_mem_domain在k_mem_domain_add_thread的时候被保存到struct mem_domain_info中

内存布局

nrf52832的RAM从0x20000000开始共计有64K,会被zephyr的ld文件分为多个section,内存保护的对象是放在noinit和app_smem ,如下图
memory

内存保护

内存保护的过程就是在进入用户模式线程前,把将要执行的用户线程thread stack和domain内的partition内存区域通过MPU设置为可读写,其它内存区域MPU不允许访问。这样就达到了用户线程不能随意访问自己权限外内存区域的目的。

切换MPU时机

在下面3种情况下会发生MPU切换内存保护区域
1.线程切换:zephyr/arch/arm/core/swap_helper.S z_arm_pendsv->z_arm_configure_dynamic_mpu_regions
2.第一次调度:zephyr/arch/arm/core/userspace.S z_arch_user_mode_enter -> z_arm_userspace_enter->z_arm_configure_dynamic_mpu_regions
3.设置其它thread的domain: zephyr/kernel/mem_domain.c z_arch_mem_domain_thread_add->z_arm_configure_dynamic_mpu_regions

MPU设置

从上一小节可以看到MPU切换内存保护区时都是使用的z_arm_configure_dynamic_mpu_regions,这里分析代码看看MPU是如何和三种保护手段对应起来的

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
void z_arm_configure_dynamic_mpu_regions(struct k_thread *thread)
{
//将内存保护对象的信息转换为struct k_mem_partition保存在dynamic_regions中,用于配置MPU
struct k_mem_partition *dynamic_regions[_MAX_DYNAMIC_MPU_REGIONS_NUM];

u8_t region_num = 0U;

//从线程中获取domain信息,放到dynamic_regions中
struct k_mem_domain *mem_domain = thread->mem_domain_info.mem_domain;

if (mem_domain) {
u32_t num_partitions = mem_domain->num_partitions;
struct k_mem_partition partition;
int i;

//从domain中读出每一个partition的信息放到dynamic_regions
for (i = 0; i < CONFIG_MAX_DOMAIN_PARTITIONS; i++) {
partition = mem_domain->partitions[i];
if (partition.size == 0) {
/* Zero size indicates a non-existing
* memory partition.
*/
continue;
}

dynamic_regions[region_num] =
&mem_domain->partitions[i];

region_num++;
num_partitions--;
if (num_partitions == 0U) {
break;
}
}
}

//获取线程的堆栈信息放到dynamic_regions内
if (thread->arch.priv_stack_start) {
u32_t base = (u32_t)thread->stack_obj;
u32_t size = thread->stack_info.size +
(thread->stack_info.start - base);

dynamic_regions[region_num] = &thread_stack;

region_num++;
}

#if defined(CONFIG_MPU_STACK_GUARD)
struct k_mem_partition guard;

//将线程堆栈的最后MPU_GUARD_ALIGN_AND_SIZE设置为只读模式,做为Guard保护
u32_t guard_start;
u32_t guard_size = MPU_GUARD_ALIGN_AND_SIZE;
guard_start = thread->stack_info.start - guard_size;


guard = (const struct k_mem_partition)
{
guard_start,
guard_size,
K_MEM_PARTITION_P_RO_U_NA
};
dynamic_regions[region_num] = &guard;

region_num++;
#endif /* CONFIG_MPU_STACK_GUARD */

//将转换好的dynamic_regions 配置给MPU
arm_core_mpu_configure_dynamic_mpu_regions(
(const struct k_mem_partition **)dynamic_regions,
region_num);
}

读写允许

通过设置在用户线程下只允许该线程读写自己的线程堆栈和Domain,达到保护其它线程和内核内存安全的目的。

Guard

通常在线程堆栈的顶部会保存一些线程自己的敏感数据和线程管理块信息,这些数据内存区线程自己是有读写权限的。一些攻击可以利用堆栈溢出的手段写到敏感数据和线程管理块信息从而导致安全问题,Guard机制就是MPU在线程使用堆栈和敏感数据之间设置一片区域为只读,当堆栈溢出到敏感数据区域时就会触发exception。如下图:
guard

其它

我们看到了有z_arm_configure_dynamic_mpu_regions,也有对应的z_arm_configure_static_mpu_regions,其主要目的是完成nocache ram和ram function区域的设置,这里不做详细介绍,有兴趣可以参见代码

关于Malloc

Zephyr libc简介和malloc分析一文中提到了malloc的heap被放在z_malloc_partition中,这个partiton并没有授权给用户模式的线程,因此malloc无法在用户模式线程中使用。即使将z_malloc_partition放到用户线程的domain中,也会因为malloc中使用了其它全局变量和内核对象让用户线程无法使用。

参考

https://docs.zephyrproject.org/latest/reference/usermode/memory_domain.html
https://docs.zephyrproject.org/latest/reference/usermode/mpu_stack_objects.html