1. 简介
KASAN (Kernel Address Sanitizer) 是一个动态检测内存错误的工具,主要功能是检查内存越界访问
和使用已释放的内存
等问题。目前具体支持以下类型错误的检出:
Item | Error Type |
---|---|
any | out-of-bounds |
buddy/slab | slab-out-of-bounds |
buddy/slab | use-after-free |
buddy/slab | use-after-scope |
global variable | global-out-of-bounds |
local variable | stack-out-of-bounds |
any | alloca-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 都不能被访问
这里存在一个假设,就是 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
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");
}
系统给 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;
}
实际上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)))
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]
可以看到 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);
其中的核心函数是 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;
}
最后一种最复杂的任意长度的权限判断:
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;
}
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);
}
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);
}
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);
}
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()
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;
}
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);
}
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
};
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);
}
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;
}
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);
}
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