浅谈 ARM64 基于硬件 tag 的 KASAN
作者简介
bang,linux内核爱好者,目前就职于杭州某安防公司,主要从事于SOC的bring up及驱动开发,喜欢分析linux内核内存管理和调度子系统。
本文所述的基于硬件 tag 的 KASAN 只有理论没有实践,主要参考了一些国外的文献,以及自己对源码的阅读和手册的理解,算是对硬件 tag 的 KASAN 的抛砖引玉。由于水平有限,只能浅谈。内核版本为 kernel-5.16
1.概述
踩内存是非常让人头疼的一类问题,导致的现象也是千奇百怪。如果踩的内存位于数据区域,则可能只是导致逻辑异常,并不会直接产生 kernel painc。虽然只是逻辑错误,但可能会产生各种奇怪的现象,严重时可能会出现系统宕机。如果踩的内存存储的是指针,可能会导致 kernel panic,而且产生 kernel panic的元凶很可能不能直接通过 log 定位出来,因为被踩的指针只是受害者,元凶还在逍遥法外,这时候检测踩内存的工具就呼之欲出了。目前常用的检测内核态踩内存工具有 hardware breakpoint、SLUB_DEBUG、KCSAN、KASAN 等,其中检测类型最全面最有效的要属 KASAN 工具了。KernelAddressSANitizer(KASAN)是一个动态的内存错误检测工具,主要用来发现 use-after-free 和 out-of-bounds等内存访问异常问题。KASAN 目前有三种实现方法,分别为 Generic KASAN、Software tag-based KASAN 和 Hardware tag-based KASAN,其中 Generic KASAN、Software tag-based KASAN 是基于软件实现,Hardware tag-based KASAN 是基于硬件实现的。下面我们先了解下三种 KASAN 的基本原理,然后重点分析下基于硬件 tag 的 KASAN 的实现。
1.1 Generic KASAN
软件 KASAN 模式使用影子内存来记录每个内存字节是否可以安全访问,并在编译时插入影子内存检查指令。Generic KASAN 将 1/8 的内存专用于其影子内存,并使用固定偏移映射方式将对应内存地址转换为其相应的影子地址。地址转换关系如下函数:
static inline void *kasan_mem_to_shadow(const void *addr)
{
return (void *)((unsigned long)addr >> KASAN_SHADOW_SCALE_SHIFT)
+ KASAN_SHADOW_OFFSET;
}
编译器在每次访问大小为 1、2、4、8 或 16 字节的内存之前插入检测函数(``__asan_load*(addr)``、``__asan_store*(addr)``),这些函数会检测内存的访问是否有效。
1.2 Software tag-based KASAN
基于软件 tag 的 KASAN 使用软件内存标记方法来检查访问的内存是否有效,目前仅在 arm64 架构上实现。基于软件 tag 的 KASAN 使用 arm64 CPU 的 Top Byte Ignore(TBI)功能将指针 tag 存储在指针的顶部字节中。它使用影子内存来存储与每个 16 字节内存单元关联的内存 tag(因此,它将内核内存的 1/16 专用于影子内存)。在每次内存分配时,生成一个随机 tag,用这个 tag 标记分配的内存,并将相同的 tag 嵌入到返回的指针中。编译时,在每次内存访问之前插入检测指 令,这些检测指令会比较正在访问的内存的 tag(影子区)和用于访问该内存的指针的 tag 是否匹配。如果不匹配,基于软件 tag 的 KASAN 会打印错误报告。Software tag-based KASAN 使用 0xFF 作为匹配所有指针 tag(不检查通过带有 0xFF 指针 tag 的指针进行的访问)。0xFE 用于标记释放的内存区域。目前Software tag-based KASAN 只支持 slab 和 page_alloc 内存检测。我们简单总结下 Software tag-based KASAN 的特点:1、编译时插入用于检测内存访问指令。2、专用的影子内存大小为检测内存大小的 1/16。3、通过指令对比内存 tag(影子区域)和指针 tag。
1.3 Hardware tag-based KASAN
基于硬件 tag 的 KASAN 和基于软件 tag 的 KASAN 原理非常类似,Software tag-based KASAN 的内存的 tag(影子区)和用于访问该内存的指针的 tag 的比较是由插入的汇编指令完成的,而 Hardware tag-based KASAN 是由硬件自动完成比较的,对于软件是透明的。Hardware tag-based KASAN 目前也是仅支持 arm64 架构,并且它的实现是基于 ARMv8.5 指令集架构中引入的内存标记扩展 (MTE) 和 Top Byte Ignore(TBI)。在每次内存访问时,硬件都会比较正在访问的内存的 tag 和用于访问该内存指针的 tag。如果 tag 不匹配,则会触发一个异常。基于硬件 tag 的 KASAN 使用 0xFF 作为匹配所有指针 tag(不检查通过带有0xFF 指针 tag 的指针进行的访问)。0xFE 用于标记释放的内存区域。目前只支持slab 和 page_alloc 检测。这些特性和 Software tag-based KASAN 一样。如果硬件不支持 MTE(ARMv8.5 之前),则不会启用 Hardware tag-based KASAN。在这种情况下,所有 KASAN 引导参数都将被忽略。
2.基本概念和实现原理
1、 MTE的pointer tags就是利用Armv8-A 的TBI(Top Byte Ignore)特性,使用指针的高4 bits存储tag。因为用4 bits表示,所以共有16种不同的tag。
2、 pointer tags存放在虚拟地址的[59:56] bit。
3、 pointer tags可以通过使用自定义算法或者新增的MTE汇编指令(如IRG指令)生成对应的tag。
4、 MTE的pointer tags也称为logical tags。
1、 MTE的memory tags放在专用内存中,存储的位置对软件是透明的,无需软件参与显示分配。
2、 每一个tag占用4 bits,所以有16种不同的tag。
3、 4-bits tag对应16字节内存块,则memory tags的内存消耗~3%。
4、 memory tags生成和pointer tags类似,而存储是通过新增的MTE汇编指令实现(如STG指令)的。每一个memory tags对应16字节内存块,对于大内存的申请,这意味需要多次执行STG指令才能完全覆盖分配的内存。
5、 MTE memory tags也称为allocation tags。
#define KASAN_TAG_KERNEL 0xFF /* native kernel pointers tag */
#define KASAN_TAG_INVALID 0xFE /* inaccessible memory tag */
#define KASAN_TAG_MAX 0xFD /* maximum value for random tags */
#define KASAN_TAG_MIN 0xF0 /* minimum value for random tags */
/* Generate a random tag. */
static inline u8 mte_get_random_tag(void)
{
void *addr;
asm(__MTE_PREAMBLE "irg %0, %0"
: "=r" (addr));
return mte_get_ptr_tag(addr);
}
static inline u8 mte_get_ptr_tag(void *ptr)
{
/* Note: The format of KASAN tags is 0xF<x> */
u8 tag = 0xF0 | (u8)(((u64)(ptr)) >> MTE_TAG_SHIFT);
return tag;
}
从mte_get_ptr_tag函数中,我们可以看出Hardware tag-based KASAN的tag值从irg获取后,会再或上0xF0。从上面的描述我们可以猜测irg生成随机tag的取值范围为[0x0,0xD]。通过下表我们可以了解下其它MTE的新增指令
do_mem_abort
->do_tag_check_fault /* 1 */
->do_bad_area
->__do_kernel_fault
->do_tag_recovery /* 2 */
->report_tag_fault
->sysreg_clear_set
1、 产生异常后,读取ESR_EL1寄存器的值,若该值为0x11,判定是一个Synchronous Tag Check Fault异常,然后执行do_tag_check_fault函数。
2、 Report同步异常相关信息,同时关闭tag检测功能。
1. Context switching
2. Return to user/EL0 (Not required in entry from EL0 since the kernel did not run)
3. Kernel entry from EL1
4. Kernel exit to EL1
__schedule
->context_switch
->__switch_to
->mte_thread_switch
->mte_check_tfsr_el1
void mte_check_tfsr_el1(void)
{
u64 tfsr_el1 = read_sysreg_s(SYS_TFSR_EL1);
if (unlikely(tfsr_el1 & SYS_TFSR_EL1_TF1)) {
write_sysreg_s(0, SYS_TFSR_EL1);
kasan_report_async();
}
}
1、pointer tags是如何分配和储存的?
2、memory tags是如何分配和存储的?
3、有多少不同的tag?
4、何时对tag进行检测的?
5、发生tag mismatch会发生什么?
3.MTE在kernel中的使用
1、假设我们释放 4 个 pages 到伙伴系统中。
2、在释放过程中会调用 kasan_free_pages 函数填充 memory tags 为 0xe,表示此内存块已经被释放,不能直接访问,需要先申请后使用。
3、填充 memory tags 的大小为 512 bytes。
1、假设我们从伙伴系统申请 4 个 pages。系统首先从 order=2 的链表中摘下一块连续的内存块。
2、在获取内存块的过程中会调用 kasan_unpoison_pages 函数填充 pointer tags 和 memory tags。在填充 tag 之前,我们首先会通过 kasan_random_tag函数 irg 指令获取一个随机的 tag(假设生成的 tag 为 0x2),然后或上 0xf0,最后再通过 page_kasan_tag_set 函数把 pointer tags 设置到 struct page的 flags 字段中,如下函数:
static inline void page_kasan_tag_set(struct page *page, u8 tag)
{
unsigned long old_flags, flags;
if (!kasan_enabled())
return;
tag ^= 0xff;
old_flags = READ_ONCE(page->flags);
do {
flags = old_flags;
flags &= ~(KASAN_TAG_MASK << KASAN_TAG_PGSHIFT);
flags |= (tag & KASAN_TAG_MASK) << KASAN_TAG_PGSHIFT;
} while (unlikely(!try_cmpxchg(&page->flags, &old_flags, flags)));
}
#define page_to_virt(x)({\
__typeof__(x) __page = x;\
u64 __idx = ((u64)__page - VMEMMAP_START) / sizeof(struct page);\
u64 __addr = PAGE_OFFSET + (__idx * PAGE_SIZE);\
(void *)__tag_set((const void *)__addr, page_kasan_tag(__page));\
})
3、通过kasan_unpoison函数填充memory tags为0x2。
4、填充 memory tags 的大小为 512 bytes。
1、假设通过 kmalloc 申请 35 bytes,对于 ARM64 而言会匹配到 kmalloc-128 的kmem_cache,因此实际分配的 object 大小是 128 bytes。至于为什么不是匹配到 kmalloc-64,可以从__kmalloc_index 函数中找到答案。
2、通过 kasan_slab_alloc 函数填充 pointer tags 和 memory tags。首先会通过 kasan_random_tag 函数(irg 指令)生成一个随机的 tag(say,0xc),或上0xf0 后的 tag 是 0xfc。其次再通过 set_tag(object, tag)函数把 tag 设置到pointer 中,最后通过 kasan_unpoison 函数把 0xc 设置到 memory tags 中。
3、kmalloc(35)只申请了 35 个字节,由于 mte 的特性,会把前 48 个字节的 memory tags 标记为 0xc,表示能够正常访问。当使用完成之后,会调用 kfree 进行释放,释放之后的 memory tags 情况又是怎样的呢?往下看。
1、根据释放地址 p,找到对应的 object,然后释放 object。
2、在释放 object 的过程中调用 kasan_slab_free 函数填充 memory tags 为 0xe,表示释放的内存,不能再访问。
3、把释放的 object 的前 48 bytes 对应 memory tags 填充为 0xe。
➢ allocating memory
1、kmalloc(35)会从kmalloc-128 slub描述符中获取object。
2、通过IRG指令生成一个random tag (say,0x2)。
3、用0x2标记pointer tags and memory tags,其中memory tags以16 bytes向上取整,即对应[0,48)。
4、用 KASAN_TAG_INVALID(0xe)标记剩余内存 80 bytes[48,128),表示不能使用的内存。
5、不同颜色的内存块对应的 memory tags 不同。
A:p 申请的内存大小为 35 bytes,访问 p[70]属于越界访问。由于 pointer tags != memory tags,会触发一个异常(out-of-bounds),表示非法访问。
B:如果 p[136]对应的 memory tags 和 p 对应的内存块的 memory tags 不一样,则同样会触发一个异常(out-of-bounds)。
A:kmaollc(35)申请 35 bytes,因为 memory tags 以 16 bytes 为大小向上取整填充,所以[32,48)这个范围内的 memory tags 和 pointer tags 一样。所以如果我们访问 p[36],MTE 是无法检测出来的(out-of-bounds),因为此时的 pointer tags == memory tags。
B:对于 p[136]属于越界访问,填充的 memory tags 值有可能和 p 对应的内存块的 memory tags 一样,由于 pointer tags == memory tags,此时就无法检测出来(out-of-bounds)。
4.使用方法
5.案例分析
1、触发问题的位置是kmalloc_oob_right+0x1ac/0x23c。
2、CPU0,PID为102的kunit_try_catch进程读取0xf2ff0000039ca880地址数据造成的tag mismatch,其中pointer tag:0xf2, memory tag:0xfe。同时能获取到造成异常时的call trace信息。
3、触发异常的内存块的申请和释放的call trace信息及对应的进程pid号。
4、从kmalloc-128的kmem_cache中分配的object,kmalloc起始地址是0x ffff0000039ca800,申请的内存大小为(96,112]字节之间,触发异常的地址位于object起始地址偏移128 bytes的位置,属于out-of-bounds问题。