当前位置:   article > 正文

Linux mem 2.7 内存错误检测 (KASAN) 详解_global-out-of-bounds

global-out-of-bounds

1. 简介

KASAN (Kernel Address Sanitizer) 是一个动态检测内存错误的工具,主要功能是检查内存越界访问使用已释放的内存等问题。目前具体支持以下类型错误的检出:

ItemError Type
anyout-of-bounds
buddy/slabslab-out-of-bounds
buddy/slabuse-after-free
buddy/slabuse-after-scope
global variableglobal-out-of-bounds
local variablestack-out-of-bounds
anyalloca-out-of-bounds

它的核心思想,是给每 8 bytes 的 data,分配 1 byte 的 shadow。用 shadow 数据来标识 data 的访问权限和状态:
在这里插入图片描述

需要特别注意的是,shadow 并不是用每个 bit 来表示 1 byte data 的权限,而是用整体 8bit 的值来表示 8 bytes data 的访问权限。 一个 shadow 字节的合法取值如下:

0x0                     : 标识 8 bytes data 都能被访问
0x7                     : 标识前 7 bytes data 都能被访问
0x6                     : 标识前 6 bytes data 都能被访问
0x5                     : 标识前 5 bytes data 都能被访问
0x4                     : 标识前 4 bytes data 都能被访问
0x3                     : 标识前 3 bytes data 都能被访问
0x2                     : 标识前 2 bytes data 都能被访问
0x1                     : 标识前 1 bytes data 都能被访问
0xFF                    : 标识全部 8 bytes data 都不能被访问
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

这里存在一个假设,就是 8 bytes data 里面只会存在上图所示的可访问情况,而不会出现有空洞间插的情况。这是基于现有 linux 内存分配不会出现其他异常情况而设定的。

如果全部8 bytes data 都不能被访问,除了 0xFF 还有其他的数值来标识,这样可以精确定义出更详细的错误类型:

#define KASAN_FREE_PAGE         0xFF  /* page was freed */
#define KASAN_PAGE_REDZONE      0xFE  /* redzone for kmalloc_large allocations */
#define KASAN_KMALLOC_REDZONE   0xFC  /* redzone inside slub object */
#define KASAN_KMALLOC_FREE      0xFB  /* object was freed (kmem_cache_free/kfree) */
#define KASAN_GLOBAL_REDZONE    0xFA  /* redzone for global variable */

/*
 * Stack redzone shadow values
 * (Those are compiler's ABI, don't change them)
 */
#define KASAN_STACK_LEFT        0xF1
#define KASAN_STACK_MID         0xF2
#define KASAN_STACK_RIGHT       0xF3
#define KASAN_STACK_PARTIAL     0xF4
#define KASAN_USE_AFTER_SCOPE   0xF8

/*
 * alloca redzone shadow values
 */
#define KASAN_ALLOCA_LEFT	0xCA
#define KASAN_ALLOCA_RIGHT	0xCB
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

2. Shadow 区域初始化

因为每 8 字节的数据需要用 1 字节的 shadow 区域来存储权限,所以开启 KASAN 功能后系统中至少 1/9 的内存需要用来做 shadow。
在这里插入图片描述

Linux 在系统初始化的时候分配 shadow 需要的物理内存并将它们映射到对应的虚拟地址区域:

arch\x86\mm\kasan_init_64.c:

start_kernel() → setup_arch() → kasan_init():

void __init kasan_init(void)
{
	int i;
	void *shadow_cpu_entry_begin, *shadow_cpu_entry_end;

#ifdef CONFIG_KASAN_INLINE
	register_die_notifier(&kasan_die_notifier);
#endif

	memcpy(early_top_pgt, init_top_pgt, sizeof(early_top_pgt));

	/*
	 * We use the same shadow offset for 4- and 5-level paging to
	 * facilitate boot-time switching between paging modes.
	 * As result in 5-level paging mode KASAN_SHADOW_START and
	 * KASAN_SHADOW_END are not aligned to PGD boundary.
	 *
	 * KASAN_SHADOW_START doesn't share PGD with anything else.
	 * We claim whole PGD entry to make things easier.
	 *
	 * KASAN_SHADOW_END lands in the last PGD entry and it collides with
	 * bunch of things like kernel code, modules, EFI mapping, etc.
	 * We need to take extra steps to not overwrite them.
	 */
	if (pgtable_l5_enabled()) {
		void *ptr;

		ptr = (void *)pgd_page_vaddr(*pgd_offset_k(KASAN_SHADOW_END));
		memcpy(tmp_p4d_table, (void *)ptr, sizeof(tmp_p4d_table));
		set_pgd(&early_top_pgt[pgd_index(KASAN_SHADOW_END)],
				__pgd(__pa(tmp_p4d_table) | _KERNPG_TABLE));
	}

	load_cr3(early_top_pgt);
	__flush_tlb_all();

    /* (1) 首先将 kasan 有效区域的 mmu 映射 pgd 全部清零 */
	clear_pgds(KASAN_SHADOW_START & PGDIR_MASK, KASAN_SHADOW_END);

    /* (2) 区域1:内核起始地址  -  线性映射起始地址。这段区域 shadow 都指向 zero 页面 */
	kasan_populate_zero_shadow((void *)(KASAN_SHADOW_START & PGDIR_MASK),
			kasan_mem_to_shadow((void *)PAGE_OFFSET));

    /* (3) 区域2:线性映射区域。这块是最大的有内存访问区域,分配 shadow 内存,,并建立映射 */
	for (i = 0; i < E820_MAX_ENTRIES; i++) {
		if (pfn_mapped[i].end == 0)
			break;

		map_range(&pfn_mapped[i]);
	}

	shadow_cpu_entry_begin = (void *)CPU_ENTRY_AREA_BASE;
	shadow_cpu_entry_begin = kasan_mem_to_shadow(shadow_cpu_entry_begin);
	shadow_cpu_entry_begin = (void *)round_down((unsigned long)shadow_cpu_entry_begin,
						PAGE_SIZE);

	shadow_cpu_entry_end = (void *)(CPU_ENTRY_AREA_BASE +
					CPU_ENTRY_AREA_MAP_SIZE);
	shadow_cpu_entry_end = kasan_mem_to_shadow(shadow_cpu_entry_end);
	shadow_cpu_entry_end = (void *)round_up((unsigned long)shadow_cpu_entry_end,
					PAGE_SIZE);

    /* (4) 区域3:线性映射结束地址  -  percpu 变量区域起始地址。这段区域 shadow 都指向 zero 页面 */
	kasan_populate_zero_shadow(
		kasan_mem_to_shadow((void *)PAGE_OFFSET + MAXMEM),
		shadow_cpu_entry_begin);

    /* (5) 区域4:percpu 变量区域。这块有内存访问区域,分配 shadow 内存,,并建立映射 */
	kasan_populate_shadow((unsigned long)shadow_cpu_entry_begin,
			      (unsigned long)shadow_cpu_entry_end, 0);

    /* (6) 区域5:percpu 变量区域结束地址  -  内核映像区域起始地址。这段区域 shadow 都指向 zero 页面 */
	kasan_populate_zero_shadow(shadow_cpu_entry_end,
				kasan_mem_to_shadow((void *)__START_KERNEL_map));

    /* (7) 区域6:内核映像区域。这块有内存访问区域,分配 shadow 内存,,并建立映射 */
	kasan_populate_shadow((unsigned long)kasan_mem_to_shadow(_stext),
			      (unsigned long)kasan_mem_to_shadow(_end),
			      early_pfn_to_nid(__pa(_stext)));

    /* (8) 区域7:模块区域结束地址  -  shadow区域结束地址。这段区域 shadow 都指向 zero 页面 */
	kasan_populate_zero_shadow(kasan_mem_to_shadow((void *)MODULES_END),
				(void *)KASAN_SHADOW_END);

	load_cr3(init_top_pgt);
	__flush_tlb_all();

	/*
	 * kasan_zero_page has been used as early shadow memory, thus it may
	 * contain some garbage. Now we can clear and write protect it, since
	 * after the TLB flush no one should write to it.
	 */
    /* (9) 把 zero 页面的 pte 映射关系建立起来 */
	memset(kasan_zero_page, 0, PAGE_SIZE);
	for (i = 0; i < PTRS_PER_PTE; i++) {
		pte_t pte;
		pgprot_t prot;

		prot = __pgprot(__PAGE_KERNEL_RO | _PAGE_ENC);
		pgprot_val(prot) &= __default_kernel_pte_mask;

		pte = __pte(__pa(kasan_zero_page) | pgprot_val(prot));
		set_pte(&kasan_zero_pte[i], pte);
	}
	/* Flush TLBs again to be sure that write protection applied. */
	__flush_tlb_all();

	init_task.kasan_depth = 0;
	pr_info("KernelAddressSanitizer initialized\n");
}
  • 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
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114

系统给 shadow 区域定义了一个基地址 KASAN_SHADOW_OFFSET,任意数据需要查询 shadow 的值的话,用offset = 数据地址/8(右移3位),然后再加上基地址 KASAN_SHADOW_OFFSET:

static inline void *kasan_mem_to_shadow(const void *addr)
{
	return (void *)((unsigned long)addr >> KASAN_SHADOW_SCALE_SHIFT)
		+ KASAN_SHADOW_OFFSET;
}
  • 1
  • 2
  • 3
  • 4
  • 5

实际上KASAN只关心内核部分的地址,所以有效的 shadow 区域为内核地址对应区域:KASAN_SHADOW_START - KASAN_SHADOW_END。

/* 地址0的 shadow = KASAN_SHADOW_OFFSET */
#define KASAN_SHADOW_OFFSET _AC(CONFIG_KASAN_SHADOW_OFFSET, UL)
#define KASAN_SHADOW_SCALE_SHIFT 3

/*
 * Compiler uses shadow offset assuming that addresses start
 * from 0. Kernel addresses don't start from 0, so shadow
 * for kernel really starts from compiler's shadow offset +
 * 'kernel address space start' >> KASAN_SHADOW_SCALE_SHIFT
 */
/* 内核起始地址 shadow = KASAN_SHADOW_OFFSET + (0xffff800000000000 >> 3) */
#define KASAN_SHADOW_START      (KASAN_SHADOW_OFFSET + \
					((-1UL << __VIRTUAL_MASK_SHIFT) >> \
						KASAN_SHADOW_SCALE_SHIFT))
/*
 * 47 bits for kernel address -> (47 - KASAN_SHADOW_SCALE_SHIFT) bits for shadow
 * 56 bits for kernel address -> (56 - KASAN_SHADOW_SCALE_SHIFT) bits for shadow
 */
/* 内核结束地址 shadow = KASAN_SHADOW_OFFSET + (0xffffffffffffffff >> 3) */
#define KASAN_SHADOW_END        (KASAN_SHADOW_START + \
					(1ULL << (__VIRTUAL_MASK_SHIFT - \
						  KASAN_SHADOW_SCALE_SHIFT)))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

3. 权限的判断

在这里插入图片描述

它的基本操作分为两部分:

  • 1、在内存的分配、释放、初始化的时候,对 shadow 中内存访问权限进行设置。
  • 2、在读写访问内存前,先对权限进行判断,如果不能访问则报错。

本节先对读写访问时加入权限判断的流程进行阐述。

3.1 read/write

在 GCC 4.8 引入了一个新的内存错误检测工具: AddressSanitizer。使用选项 -fsanitize=address 能打开此检测器。 该检测器会对访存指令插装,帮助快速检测堆、栈以及全局的缓冲区溢出,以及use-after-free bug。

AddressSanitizer 是最初由Google开发的,用于运行时检测C/C++程序中的内存错误。它采用了CTI(CompileTime Instrumentation)技术,即在编译时进行代码插入,运行速度快。

这部分的核心机制就是 gcc 在所有读写内存的访问之前插入了一个判断权限的钩子,基本原型如下:

/* 往 0xffff800012345678 地址写 5 */

mov x0, #0x5678
movk x0, #0x1234, lsl #16
movk x0, #0x8000, lsl #32
movk x0, #0xffff, lsl #48
mov w1, #0x5
bl __asan_store1        // Gcc AddressSanitizer 插入的权限检查函数
strb w1, [x0]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

可以看到 gcc AddressSanitizer 功能会在所有的内存访问前插入桩函数 __asan_storexxx()__asan_loadxxx(),kasan 只需要在内核中实现这些函数就能进行权限判断。

内核中定义了一系列的这种宏来实现不同长度数据的读写权限判断:

mm\kasan\kasan.c:

#define DEFINE_ASAN_LOAD_STORE(size)					\
	void __asan_load##size(unsigned long addr)			\
	{								\
		check_memory_region_inline(addr, size, false, _RET_IP_);\
	}								\
	EXPORT_SYMBOL(__asan_load##size);				\
	__alias(__asan_load##size)					\
	void __asan_load##size##_noabort(unsigned long);		\
	EXPORT_SYMBOL(__asan_load##size##_noabort);			\
	void __asan_store##size(unsigned long addr)			\
	{								\
		check_memory_region_inline(addr, size, true, _RET_IP_);	\
	}								\
	EXPORT_SYMBOL(__asan_store##size);				\
	__alias(__asan_store##size)					\
	void __asan_store##size##_noabort(unsigned long);		\
	EXPORT_SYMBOL(__asan_store##size##_noabort)

DEFINE_ASAN_LOAD_STORE(1);
DEFINE_ASAN_LOAD_STORE(2);
DEFINE_ASAN_LOAD_STORE(4);
DEFINE_ASAN_LOAD_STORE(8);
DEFINE_ASAN_LOAD_STORE(16);
  • 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

其中的核心函数是 check_memory_region_inline() 用来检查不同长度数据的访问权限:

__asan_load##size()/__asan_store##size() → check_memory_region_inline():

static __always_inline void check_memory_region_inline(unsigned long addr,
						size_t size, bool write,
						unsigned long ret_ip)
{
	if (unlikely(size == 0))
		return;

    /* (1) 判断传入的地址转换成 shadow 地址后,是否合法 */
	if (unlikely((void *)addr <
		kasan_shadow_to_mem((void *)KASAN_SHADOW_START))) {
		kasan_report(addr, size, write, ret_ip);
		return;
	}

    /* (2) 根据内存地址对应的shadow 值,判断内存的访问权限 */
	if (likely(!memory_is_poisoned(addr, size)))
		return;

    /* (3) 如果没有访问权限,构造出错报告 */
	kasan_report(addr, size, write, ret_ip);
}

↓

static __always_inline bool memory_is_poisoned(unsigned long addr, size_t size)
{
    /* (2.1) 数据长度为 1/2/4/8/16 的 shadow 值的读取 和 判断 */
	if (__builtin_constant_p(size)) {
		switch (size) {
		case 1:
			return memory_is_poisoned_1(addr);
		case 2:
		case 4:
		case 8:
			return memory_is_poisoned_2_4_8(addr, size);
		case 16:
			return memory_is_poisoned_16(addr);
		default:
			BUILD_BUG();
		}
	}

    /* (2.2) 其他更长数据长度的 shadow 值的读取 和 判断 */
	return memory_is_poisoned_n(addr, size);
}

|→

static __always_inline bool memory_is_poisoned_1(unsigned long addr)
{
    /* (2.1.1.1) 出具长度为1,首先取出1个字节所在的完整8字节数据,所对应的1字节的shadow值 */
	s8 shadow_value = *(s8 *)kasan_mem_to_shadow((void *)addr);

    /* (2.1.1.2) 因为8字节的必须是连续访问的,所以如果某个字节可以访问:
                必须 shadow 中的值 >  1字节数据在8个字节中的offset
     */
	if (unlikely(shadow_value)) {
		s8 last_accessible_byte = addr & KASAN_SHADOW_MASK;
		return unlikely(last_accessible_byte >= shadow_value);
	}

    /* (2.1.1.3) shadow 值为 0,8个字节都能被访问,其中一个字节肯定能访问 */
	return false;
}

|→

static __always_inline bool memory_is_poisoned_2_4_8(unsigned long addr,
						unsigned long size)
{
    /* (2.1.2.1) 2/4/8字节数据,有跨两个8字节的情况:
                步骤1:先取第一部分字节的权限
     */
	u8 *shadow_addr = (u8 *)kasan_mem_to_shadow((void *)addr);

	/*
	 * Access crosses 8(shadow size)-byte boundary. Such access maps
	 * into 2 shadow bytes, so we need to check them both.
	 */
    /* (2.1.2.2) 第一字节 > 0,必须  shadow 中的值 >  1字节数据在8个字节中的offset,第一部分数据才可以访问
                再继续判断最后字节的权限情况
     */
	if (unlikely(((addr + size - 1) & KASAN_SHADOW_MASK) < size - 1))
		return *shadow_addr || memory_is_poisoned_1(addr + size - 1);

    /* (2.1.2.3) 第一字节 = 0,8个字节都能被访问,第一部分字节肯定能访问
                再继续判断最后字节的权限情况
     */
	return memory_is_poisoned_1(addr + size - 1);
}

|→

static __always_inline bool memory_is_poisoned_16(unsigned long addr)
{
    /* (2.1.3.1) 16字节数据,有跨三个8字节的情况:
                步骤1:先取第一、二个8字节的权限,必须为0
     */
	u16 *shadow_addr = (u16 *)kasan_mem_to_shadow((void *)addr);

	/* Unaligned 16-bytes access maps into 3 shadow bytes. */
    /* (2.1.3.2) 如果没有8字节对应,需要继续判断第三个8字节中最后字节的权限 */
	if (unlikely(!IS_ALIGNED(addr, KASAN_SHADOW_SCALE_SIZE)))
		return *shadow_addr || memory_is_poisoned_1(addr + 15);

    /* (2.1.3.3) 如果8字节对齐,第一、二个8字节的权限为0,才可以访问 */
	return *shadow_addr;
}
  • 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
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110

最后一种最复杂的任意长度的权限判断:

static __always_inline bool memory_is_poisoned_n(unsigned long addr,
						size_t size)
{
	unsigned long ret;

    /* (2.1.4.1) 判断 数据的其实和结束 shadow 值是否都为 0 */
	ret = memory_is_nonzero(kasan_mem_to_shadow((void *)addr),
			kasan_mem_to_shadow((void *)addr + size - 1) + 1);

    /* (2.1.4.2) 不全为0,判断最后一个字节的权限情况 */
	if (unlikely(ret)) {
		unsigned long last_byte = addr + size - 1;
		s8 *last_shadow = (s8 *)kasan_mem_to_shadow((void *)last_byte);

        /* 必须 shadow 中的值 >  1字节数据在8个字节中的offset */
		if (unlikely(ret != (unsigned long)last_shadow ||
			((long)(last_byte & KASAN_SHADOW_MASK) >= *last_shadow)))
			return true;
	}

    /* (2.1.4.3) 全为0,所有数据都可以访问 */
	return false;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

3.2 memxxx()

对于一系列 mem 开头的函数也进行了替换,插入了权限检查:

#undef memset
void *memset(void *addr, int c, size_t len)
{
	check_memory_region((unsigned long)addr, len, true, _RET_IP_);

	return __memset(addr, c, len);
}

#undef memmove
void *memmove(void *dest, const void *src, size_t len)
{
	check_memory_region((unsigned long)src, len, false, _RET_IP_);
	check_memory_region((unsigned long)dest, len, true, _RET_IP_);

	return __memmove(dest, src, len);
}

#undef memcpy
void *memcpy(void *dest, const void *src, size_t len)
{
	check_memory_region((unsigned long)src, len, false, _RET_IP_);
	check_memory_region((unsigned long)dest, len, true, _RET_IP_);

	return __memcpy(dest, src, len);
}
  • 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

4. 权限的设置

在这里插入图片描述

和权限的判断一样,权限的设定也需要在各种关键时刻的钩子中插入权限设定操作。

4.1 buddy

Buddy 系统在 free 和 alloc 的时间点上插入了权限设置,所以 buddy 能检测出 use-after-free 类型的错误。

4.1.1 kasan_free_pages()

__free_pages() → free_the_page() → __free_pages_ok() → free_pages_prepare() → kasan_free_nondeferred_pages() → kasan_free_pages():

void kasan_free_pages(struct page *page, unsigned int order)
{
    /* (1) 在 page free 的时候,把 shadow 设置成 0xFF (KASAN_FREE_PAGE),不能访问 */
	if (likely(!PageHighMem(page)))
		kasan_poison_shadow(page_address(page),
				PAGE_SIZE << order,
				KASAN_FREE_PAGE);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

4.1.2 kasan_alloc_pages()

alloc_pages() → alloc_pages_current() → __alloc_pages_nodemask() → get_page_from_freelist() → prep_new_page() → post_alloc_hook() → kasan_alloc_pages():

void kasan_alloc_pages(struct page *page, unsigned int order)
{
    /* (1) 在 page alloc 的时候,把 shadow 设置成 0,可以访问 */
	if (likely(!PageHighMem(page)))
		kasan_unpoison_shadow(page_address(page), PAGE_SIZE << order);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

4.2 slub

在这里插入图片描述

slub 在 malloc 和 free 的基础上,增加了 redzone 区域的检测。这样除了 use-after-free 以外还能检测出 slab-out-of-bounds 类型的错误。

4.2.1 kasan_cache_create()

计算出每个 object 额外要分配的 redzone 区间:

__kmem_cache_create() → kasan_cache_create()
  • 1

4.2.2 kasan_slab_free()

kmem_cache_free() → slab_free() → slab_free_freelist_hook() → slab_free_hook() → kasan_slab_free() → __kasan_slab_free():

static bool __kasan_slab_free(struct kmem_cache *cache, void *object,
			      unsigned long ip, bool quarantine)
{
	s8 shadow_byte;
	unsigned long rounded_up_size;

	if (unlikely(nearest_obj(cache, virt_to_head_page(object), object) !=
	    object)) {
		kasan_report_invalid_free(object, ip);
		return true;
	}

	/* RCU slabs could be legally used after free within the RCU period */
	if (unlikely(cache->flags & SLAB_TYPESAFE_BY_RCU))
		return false;

    /* (1) 判断 free 的 object 的 shadow 权限是否合法 */
	shadow_byte = READ_ONCE(*(s8 *)kasan_mem_to_shadow(object));
	if (shadow_byte < 0 || shadow_byte >= KASAN_SHADOW_SCALE_SIZE) {
		kasan_report_invalid_free(object, ip);
		return true;
	}

    /* (2) 把数据区域的 shadow 设置成 0xFB (KASAN_KMALLOC_FREE),不能访问 */
	rounded_up_size = round_up(cache->object_size, KASAN_SHADOW_SCALE_SIZE);
	kasan_poison_shadow(object, rounded_up_size, KASAN_KMALLOC_FREE);

	if (!quarantine || unlikely(!(cache->flags & SLAB_KASAN)))
		return false;

	set_track(&get_alloc_info(cache, object)->free_track, GFP_NOWAIT);
	quarantine_put(get_free_info(cache, object), cache);
	return true;
}
  • 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

4.2.3 kasan_slab_alloc()

kmem_cache_alloc() → kasan_slab_alloc() → kasan_kmalloc():

void kasan_kmalloc(struct kmem_cache *cache, const void *object, size_t size,
		   gfp_t flags)
{
	unsigned long redzone_start;
	unsigned long redzone_end;

	if (gfpflags_allow_blocking(flags))
		quarantine_reduce();

	if (unlikely(object == NULL))
		return;

	redzone_start = round_up((unsigned long)(object + size),
				KASAN_SHADOW_SCALE_SIZE);
	redzone_end = round_up((unsigned long)object + cache->object_size,
				KASAN_SHADOW_SCALE_SIZE);

    /* (1) 把数据区域的 shadow 设置成 0,可以访问 */
	kasan_unpoison_shadow(object, size);
    /* (2) 把数据后 redzone 区域的 shadow 设置成 0xFC (KASAN_KMALLOC_REDZONE),不能访问 */
	kasan_poison_shadow((void *)redzone_start, redzone_end - redzone_start,
		KASAN_KMALLOC_REDZONE);

	if (cache->flags & SLAB_KASAN)
		set_track(&get_alloc_info(cache, object)->alloc_track, flags);
}
  • 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

4.3 kmalloc

kmalloc 的原理和 slub 类似。

4.4 global variable

对于全局变量的保护,增加了redzone,所以能检测出 global-out-of-bounds 类型的错误。

4.4.1 struct kasan_global

开启了 kasan 功能以后,对每一个全局变量会扩充成一个复杂的结构,主要是增加了 redzone 区域:

struct kasan_global {
    /* 全局变量起始地址 */
	const void *beg;		/* Address of the beginning of the global variable. */
    /* 全局变量原有 size */
	size_t size;			/* Size of the global variable. */
    /* 全局变量加上 redzone 以后的 size */
	size_t size_with_redzone;	/* Size of the variable + size of the red zone. 32 bytes aligned */
	const void *name;
	const void *module_name;	/* Name of the module where the global variable is declared. */
	unsigned long has_dynamic_init;	/* This needed for C++ */
#if KASAN_ABI_VERSION >= 4
	struct kasan_source_location *location;
#endif
#if KASAN_ABI_VERSION >= 5
	char *odr_indicator;
#endif
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

4.4.2 __asan_register_globals()

在系统初始化的时候,在调用构造函数时会调用到 kasan 全局变量的初始化函数:

start_kernel() → rest_init() → kernel_init() → kernel_init_freeable() → do_basic_setup() → do_ctors() → __asan_register_globals() → register_global():

static void register_global(struct kasan_global *global)
{
	size_t aligned_size = round_up(global->size, KASAN_SHADOW_SCALE_SIZE);

    /* (1) 把全局变量的数据区域的 shadow 设置成 0,可以访问 */
	kasan_unpoison_shadow(global->beg, global->size);

    /* (2) 把全局变量的 redzone 区域的 shadow 设置成 0xFA (KASAN_GLOBAL_REDZONE),不可以访问 */
	kasan_poison_shadow(global->beg + aligned_size,
		global->size_with_redzone - aligned_size,
		KASAN_GLOBAL_REDZONE);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

4.5 local variable

局部变量的保护也是使用插入 redzone 的形式来进行保护。但是这部分的调用时怎么串起来的,现在还没搞清楚。

4.5.1 例子

别人举的例子:

/* (1) c 语言 */
void foo()
{
    char a[328];
}

↓

void foo()
{
    char rz1[32];   // 编译器添加的redzone
    char a[328];
    char rz2[56];   // 编译器添加的redzone
    int *shadow = (&rz1 >> 3)+ KASAN_SHADOW_OFFSE;
    shadow[0] = 0xffffffff;
    shadow[11] = 0xffffff00;
    shadow[12] = 0xffffffff;
    /*------------------------使用完毕----------------------------------------*/
    shadow[0] = shadow[11] = shadow[12] = 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

4.5.2 相关函数

内核定义了一些相关函数,但是怎么样和栈保护编译链接起来的还没研究:

static void __kasan_unpoison_stack(struct task_struct *task, const void *sp)
{
	void *base = task_stack_page(task);
	size_t size = sp - base;

	kasan_unpoison_shadow(base, size);
}

/* Unpoison the entire stack for a task. */
void kasan_unpoison_task_stack(struct task_struct *task)
{
	__kasan_unpoison_stack(task, task_stack_page(task) + THREAD_SIZE);
}

/* Unpoison the stack for the current task beyond a watermark sp value. */
asmlinkage void kasan_unpoison_task_stack_below(const void *watermark)
{
	/*
	 * Calculate the task stack base address.  Avoid using 'current'
	 * because this function is called by early resume code which hasn't
	 * yet set up the percpu register (%gs).
	 */
	void *base = (void *)((unsigned long)watermark & ~(THREAD_SIZE - 1));

	kasan_unpoison_shadow(base, watermark - base);
}

/*
 * Clear all poison for the region between the current SP and a provided
 * watermark value, as is sometimes required prior to hand-crafted asm function
 * returns in the middle of functions.
 */
void kasan_unpoison_stack_above_sp_to(const void *watermark)
{
	const void *sp = __builtin_frame_address(0);
	size_t size = watermark - sp;

	if (WARN_ON(sp > watermark))
		return;
	kasan_unpoison_shadow(sp, size);
}
  • 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

4.6 vmalloc

因为 vmalloc 的分配和释放都是以 page 为单位的,所以他的 kasan 保护沿用 buddy 的就行了。

参考文档:

1.The Kernel Address Sanitizer (KASAN)
2.KASAN实现原理
3.内存管理三 内核内存检测KASAN
4.Kasan - Linux 内核的内存检测工具
5.Linux内核内存检测工具KASAN
6.linux内核(5.4.81)——KASAN
7.asan的接口变更
8.利用Address Sanitizer工具检查内存访问错误
9.gcc address sanitizer

声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号