kasan原理及解析
1. 前言
Kernel Address SANitizer(KASAN)是一种动态内存安全错误检测工具,主要功能是检查内存越界访问和使用已释放内存的问题。
KASAN有三种模式:1.通用KASAN;2.基于软件标签的KASAN;3.基于硬件标签的KASAN
- 用CONFIG_KASAN_GENERIC启用的通用KASAN,是用于调试的模式,类似于用户空 间的ASan。这种模式在许多CPU架构上都被支持,但它有明显的性能和内存开销。
- 基于软件标签的KASAN或SW_TAGS KASAN,通过CONFIG_KASAN_SW_TAGS启用, 可以用于调试和自我测试,类似于用户空间HWASan。这种模式只支持arm64,但其适度的内存开销允许在内存受限的设备上用真实的工作负载进行测试。
- 基于硬件标签的KASAN或HW_TAGS KASAN,用CONFIG_KASAN_HW_TAGS启用,被用作现场内存错误检测器或作为安全缓解的模式。这种模式只在支持MTE(内存标签扩展)的arm64 CPU上工作,但它的内存和性能开销很低,因此可以在生产中使用
本文后续所述的皆是基于通用KASAN
1.1 支持的体系架构
在x86_64、arm、arm64、powerpc、riscv、s390和xtensa上支持通用KASAN,而基于标签的KASAN模式只在arm64上支持。
1.2 编译器
软件KASAN模式使用编译时工具在每个内存访问之前插入有效性检查,因此需要一个 提供支持的编译器版本。基于硬件标签的模式依靠硬件来执行这些检查,但仍然需要一个支持内存标签指令的编译器版本。
通用KASAN需要GCC 8.3.0版本或更高版本,或者内核支持的任何Clang版本。
基于软件标签的KASAN需要GCC 11+或者内核支持的任何Clang版本。
基于硬件标签的KASAN需要GCC 10+或Clang 12+。
1.3 可检查的内存类型
通用KASAN支持在所有的slab、page_alloc、vmap、vmalloc、堆栈和全局内存中查找错误。
基于软件标签的KASAN支持slab、page_alloc、vmalloc和堆栈内存。
基于硬件标签的KASAN支持slab、page_alloc和不可执行的vmalloc内存。
对于slab,两种软件KASAN模式都支持SLUB和SLAB分配器,而基于硬件标签的 KASAN只支持SLUB。
1.4 开启kasan
其中,KASAN mode 固定位 generic mode (通用KASAN)不可更改,Instrumentation type有以下两种选项:
关于该点的描述为:使用inline插桩,编译器不进行函数调用,而是直接插入代码来检查影子内存。此选项 显著地增大了内核体积,但与outline插桩内核相比,它提供了x1.1-x2的性能提升。即使用inline插桩方式,内核体积会变得更大,但是性能得到提成。
另外CONFIG_KASAN_VMALLOC也需要看架构是否支持,若不支持则无法检测vmalloc要将受影响的slab对象的alloc和free堆栈跟踪包含到报告中,请启用 CONFIG_STACKTRACE。要包括受影响物理页面的分配和释放堆栈跟踪的话, 请启用 CONFIG_PAGE_OWNER 并使用 page_owner=on 进行引导
在未开启kasan之前的内核大小为(arm64,使用gz压缩方式):
使用outline大小为:
使用inline大小为:
编译时工具用于插入内存访问检查。编译器在每次访问大小为1、2、4、8或16的内存之前插入函数调用( __asan_load*(addr) , __asan_store*(addr))。这些函数通过 检查相应的影子内存来检查内存访问是否有效。如下图为开启kasan(outline方式)后,内存操作会有插桩函数调用。
但是用设备实测,使用outline的方式并没有触发程序中已知的一个数组越界,而inline的方式是可以的 (使用inline插桩,编译器不进行函数调用,而是直接插入代码来检查影子内存),因此这里怀疑可能是因为使用outline的方式插桩的函数没有找到对应的函数调用。所以后续使用皆以inline为准
2. Kasan工作原理
2.1 触发kasan
加载test_kasan_module.ko,触发一个典型的oob,触发后的打印如下所示:
1.输出检测出的错误类型,类型包括以下几类:
2.打印出栈回溯
3.所访问内存分配位置的堆栈跟踪(对于访问了slab对象的情况)以及对象被释放的位置的堆栈跟踪(对于访问已释放内存的问题报告)比如下图,use-after-free会将如何释放的对应内存和释放后又有谁使用的堆栈打印出来
4.对访问的slab对象的描述以及关于访问的内存页的信息
5 展示了访问地址周围的内存状态
2.2 Report
结合测试demo和报告输出,先对kasan的report进行分析。大致调用的函数和流程如下:
kasan_report
| -> start_report
| -> print_error_description (输出信息1)
| -> print_address_description
| | - -> dump_stack (输出信息2)
| | - -> describe_object (输出信息3)
| | - -> dump_page (输出信息4)
| -> print_shadow_for_address (输出信息5)
| -> end_report
2.3 错误类型报告
函数源码如下,可以看到该函数打印BUG类型和读/写到具体的地址,大小,以及是哪个进行调用的
static void print_error_description(struct kasan_access_info *info)
{
pr_err("BUG: KASAN: %s in %pS\n",
get_bug_type(info), (void *)info->ip);
pr_err("%s of size %zu at addr %px by task %s/%d\n",
info->is_write ? "Write" : "Read", info->access_size,
info->access_addr, current->comm, task_pid_nr(current));
}
官方对于该处的描述为:
If the shadow byte value is in the range of [0, KASAN_SHADOW_SCALE_SIZE), it indicates that the accessed memory region is within the bounds of the allocated buffer, but the access is potentially invalid. In such cases, we can look at the next shadow byte to determine the type of bad access. The next shadow byte will contain the value 0 for a valid access, and a non-zero value for an invalid access. By examining this shadow byte, we can determine the type of the invalid access, such as whether it is a use-after-free or out-of-bounds access. This information helps in identifying and fixing the bugs in the code. KASAN_SHADOW_SCALE_SIZE is a compile-time constant that sets the number of shadow bytes per byte of memory.
翻译过来为:如果阴影字节值在 [0, KASAN_SHADOW_SCALE_SIZE) 范围内,我们可以查看下一个阴影字节来确定无效访问的类型。这意味着访问的内存区域在分配的缓冲区范围之内,但访问可能不合法。在这种情况下,我们可以查看下一个阴影字节来确定坏访问的类型。下一个阴影字节将包含值 0 表示有效访问,非零值表示无效访问。通过检查这个阴影字节,我们可以确定无效访问的类型,例如是否为使用已释放的内存或越界访问。这些信息有助于识别和修复代码中的错误。KASAN_SHADOW_SCALE_SIZE 是一个编译时常量,设置每个字节内存的阴影字节数。
其核心调用为,其中KASAN_SHADOW_SCALE_SHIFT = 3,也就是KASAN_SHADOW_SCALE_SIZE值为8
#define KASAN_SHADOW_SCALE_SIZE (1UL << KASAN_SHADOW_SCALE_SHIFT)
static const char *get_shadow_bug_type(struct kasan_access_info *info)
{
const char *bug_type = "unknown-crash";
u8 *shadow_addr;
shadow_addr = (u8 *)kasan_mem_to_shadow(info->first_bad_addr);
/*
* If shadow byte value is in [0, KASAN_SHADOW_SCALE_SIZE) we can look
* at the next shadow byte to determine the type of the bad access.
*/
if (*shadow_addr > 0 && *shadow_addr <= KASAN_SHADOW_SCALE_SIZE - 1)
shadow_addr++;
switch (*shadow_addr) {
case 0 ... KASAN_SHADOW_SCALE_SIZE - 1:
/*
* In theory it's still possible to see these shadow values
* due to a data race in the kernel code.
*/
bug_type = "out-of-bounds";
break;
case KASAN_PAGE_REDZONE:
case KASAN_KMALLOC_REDZONE:
bug_type = "slab-out-of-bounds";
break;
case KASAN_GLOBAL_REDZONE:
bug_type = "global-out-of-bounds";
break;
case KASAN_STACK_LEFT:
case KASAN_STACK_MID:
case KASAN_STACK_RIGHT:
case KASAN_STACK_PARTIAL:
bug_type = "stack-out-of-bounds";
break;
case KASAN_FREE_PAGE:
case KASAN_KMALLOC_FREE:
case KASAN_KMALLOC_FREETRACK:
bug_type = "use-after-free";
break;
case KASAN_ALLOCA_LEFT:
case KASAN_ALLOCA_RIGHT:
bug_type = "alloca-out-of-bounds";
break;
case KASAN_VMALLOC_INVALID:
bug_type = "vmalloc-out-of-bounds";
break;
}
return bug_type;
}
由该类型报告,可以引出kasan的核心机制:影子内存
2.4 影子内存
它的核心思想,是给每 8 bytes 的 data,分配 1 byte 的 shadow。用 shadow 数据来标识 data 的访问权限和状态。需要特别注意的是,shadow 并不是用每个 bit 来表示 1 byte data 的权限,而是用整体 8bit 的值来表示 8 bytes data 的访问权限。比如该8bytes的内存中有3个字节可以访问,那shadow就是3 。再者需要注意的是,0代表全部可以访问,而不是8。
一个 shadow 字节的合法取值如下:
2.5 Kasan如何检测
对于inline方式,插入的是__kasan_check_read/write
实际上就是调用check_memory_region_inline函数实现
源码如下:
2.5.1 如何理解各项检查项:
-
Size不太可能为0,很好理解,因为既然有内存操作,操作的大小正常也不应该是0。
-
地址+大小不太可能小于地址,也很好理解,毕竟不太可能出现size为”负数”的情况
-
如何理解地址不太可能小于影子内存?这个我们需要看下影子内存的内存分布。Arm64的内存布局如下(VA_BITS=48,实际我的设备是39,这里使用的内核官方文档的内存布局),可以看到kasan的影子内存基本属于内核区的最顶端,也就是说,内核地址不太可能小于影子内存,如果小于了,那么基本说明该地址可能已经是用户态内存了,那确信就是存在问题。
-
可能的memory_is_poisoned,也就是说这点是kasan最关注的一个检查项
2.5.2 Kasan检测基本原理:
首先看继续跟踪代码memory_is_poisoned,源码如下:
而memory_is_poisoned _2_4_8/_16最终都是调用的 x_1,故看源码如下
/*
下面的所有函数都总是内联的,因此编译器可以在每个
__asan_loadX / __assn_storeX中针对内存访问大小X执行更好的优化。 */
static __always_inline bool memory_is_poisoned_1(unsigned long addr)
{
/* 将地址转换成影子内存(每8byte有对应的1byte影子内存,后面分析具体映射关系) */
s8 shadow_value = *(s8 *)kasan_mem_to_shadow((void *)addr);
/* 如果shadow_value不为0,比如说是负数或者1-7的值,
那么就需要进行判断,看看对应要访问的字节的影子内存对应是否能够访问 */
if (unlikely(shadow_value)) {
/* KASAN_SHADOW_MASK 的值为7 */
#define KASAN_SHADOW_MASK (KASAN_SHADOW_SCALE_SIZE - 1)
/* 这里把虚拟地址 &7 目的就是为了看访问的地址(实际上已经是地址+size)
是否大于剩余可访问的字节数,注意这里就是kasan的最根本的原理 */
s8 last_accessible_byte = addr & KASAN_SHADOW_MASK;
return unlikely(last_accessible_byte >= shadow_value);
}
/* shadow 值为 0,8个字节都能被访问,其中一个字节肯定能访问,
返回false说明kasan没有检测出问题 */
return false;
}
对于kasan的根本原理,在解析一下:使用下图来说明关系吧。假设内存是从地址8~15一共8 bytes。对应的shadow memory值为5,假如现在访问11(11&7=3 3<5)地址,那么就是可以访问,假如想要访问地址13(13&7=5 5>=5),那么就不能访问,就检测出了问题。
对于memory_is_poisoned_n,最复杂的任意长度的权限判断,源码如下:
static __always_inline unsigned long bytes_is_nonzero(const u8 *start,
size_t size)
{
while (size) {
/* 这里如果对应的影子地址的值非0,就需要进行权限的判断了 */
if (unlikely(*start))
return (unsigned long)start;
start++;
size--;
}
return 0;
}
static __always_inline unsigned long memory_is_nonzero(const void *start, const void *end)
{
unsigned int words;
unsigned long ret;
unsigned int prefix = (unsigned long)start % 8;
if (end - start <= 16)
return bytes_is_nonzero(start, end - start);
/* 如果影子地址差了16个以上(对应16*8=128 即size大于128) */
if (prefix) {
prefix = 8 - prefix;
/* 将start按8对齐,先把未对齐的前 prefix 长度权限先校验 */
ret = bytes_is_nonzero(start, prefix);
if (unlikely(ret))
return ret;
start += prefix; /* start补齐成8的倍数 */
}
/* 在计算end到start有多少个8字节影子地址(即对应words倍的128长度的实际内存) */
words = (end - start) / 8;
while (words) {
if (unlikely(*(u64 *)start))
return bytes_is_nonzero(start, 8);
/* 再次进行权限判断,如果有一个不为0,则说明有问题 */
start += 8;
words--;
}
/* 最后,将剩余长度的影子地址进行权限判断,同样的有一个不为0,就可能有问题 */
return bytes_is_nonzero(start, (end - start) % 8);
}
static __always_inline bool memory_is_poisoned_n(unsigned long addr,
size_t size)
{
unsigned long ret;
/* 判断 内存对应的影子内存中,起始和结束 shadow 值是否都为 0
注意:这里影子内存起始就是直接转换来的,而结束比较有意思,
找的永远是对应地址对应长度的影子地址的下一个影子地址 */
ret = memory_is_nonzero(kasan_mem_to_shadow((void *)addr),
kasan_mem_to_shadow((void *)addr + size - 1) + 1);
/* 根据前面的判断,如果ret不为0(可能的值为负数或1-7),
就说明内存权限可能有问题,需要进一步判断 */
if (unlikely(ret)) {
/* 只判断起始地址,连续size长度的最后一字节所在影子内存所在位置的权限值 */
unsigned long last_byte = addr + size - 1;
s8 *last_shadow = (s8 *)kasan_mem_to_shadow((void *)last_byte);
/* 如果ret!=last_shadow 可能是因为在连续的内存检测过程中,
就已经检测到了一个非法权限,那么肯定就是有问题的 */
/* ||后面的检测方案和 memory_is_poisoned_1 实现是相同的 */
if (unlikely(ret != (unsigned long)last_shadow ||
((long)(last_byte & KASAN_SHADOW_MASK) >= *last_shadow)))
return true;
}
return false;
}
至此,我们分析了kasan基于影子内存和对应权限值是如何检测出问题的原理。那么剩下的重点就是:kasan是如何维护和标定影子内存所对应的权限值的?以及,kasan的影子内存是如何及映射的
附两张官方pdf,对于原理的简单介绍:
2.6 Kasan对影子内存的标定
其实原理也很简单,就是内存申请,释放的时候调用kasan的相关函数。
2.6.1 对于buddy:
Buddy 系统在 free 和 alloc 的时间点上插入了权限设置,所以 buddy 能检测出 use-after-free 类型的错误。调用的函数为:kasan_alloc_pages和kasan_free_pages
调用路径为:
alloc_pages() → alloc_pages_current() → __alloc_pages_nodemask() → get_page_from_freelist() → prep_new_page() → post_alloc_hook() → kasan_alloc_pag
__free_pages() → free_the_page() → __free_pages_ok() → free_pages_prepare() → kasan_free_nondeferred_pages() → kasan_free_pages():
/*
* 该函数会为从“addr”开始的“size”字节的阴影内存添加tag。内存地址应该对齐到KASAN_SHADOW_SCALE_SIZE */
void kasan_poison_shadow(const void *address, size_t size, u8 value)
{
void *shadow_start, *shadow_end;
address = reset_tag(address);(实际就是调用了__tag_reset)
shadow_start = kasan_mem_to_shadow(address);
shadow_end = kasan_mem_to_shadow(address + size);
/* 将影子内存中对应的权限值设置成value,
对于alloc来说,实际上就是设置成0
对于free来说,实际上就是设置了0xFF
*/
__memset(shadow_start, value, shadow_end - shadow_start);
}
void kasan_unpoison_shadow(const void *address, size_t size)
{
u8 tag = get_tag(address);(实际上就是调用了__tag_get,也就是tag = 0 [CONFIG_KASAN_SW_TAGS 没开])
address = reset_tag(address);
kasan_poison_shadow(address, size, tag);
/* 如果size不是8的倍数,对最后一个影子内存的权限值设置为当前 siez & 7 的大小 */
/* 对于buddy,申请的都是整页,不会走下面,这个函数slab也会调用,会走到底下 */
if (size & KASAN_SHADOW_MASK) {
u8 *shadow = (u8 *)kasan_mem_to_shadow(address + size);
if (IS_ENABLED(CONFIG_KASAN_SW_TAGS))
*shadow = tag; /* 这里走不进来 */
else
*shadow = size & KASAN_SHADOW_MASK;
}
}
void kasan_alloc_pages(struct page *page, unsigned int order)
{
u8 tag;
unsigned long i;
if (unlikely(PageHighMem(page)))
return;
tag = random_tag();
for (i = 0; i < (1 << order); i++)
page_kasan_tag_set(page + i, tag);
/* 在这里设置对应影子内存的值 */
kasan_unpoison_shadow(page_address(page), PAGE_SIZE << order);
}
void kasan_free_pages(struct page *page, unsigned int order)
{
#define KASAN_FREE_PAGE 0xFF
if (likely(!PageHighMem(page)))
kasan_poison_shadow(page_address(page),
PAGE_SIZE << order,
KASAN_FREE_PAGE);
}
根据上述源码,分析,就能得知,为什么buddy能够识别use_after_free问题了,分析完代码看下图(注:图中的linear mapping是4.x的内核的映射关系,新版内核已经将其移动到kasan之前了,具体的内存分布清看Kasan映射章节,因此这里仅供示例与参考,后面图一样如此):
如图为buddy申请4个page对于kasan的映射(虚拟地址>>3+KASAN固定偏移)和数值设置情况。
对kasan的影子内存而言,申请4page(16K),则需要占用影子内存2KB(16/8),然后全部权限值设置为0
对于buddy的释放而言,同样的,要回收这4个page,对映射到的影子内存区域的值全部设置成0XFF即可
2.6.2 对于SLUB
ⅠSlub初始化填充
SLUB分配对象的内存的shadow memory值如何填充?对于SLUB,首先有初始化填充,其源码和调用关系如下:
函数调用关系:mm_init() -> kmem_cache_init() -> create_boot_cache() -> __kmem_cache_create() -> kmem_cache_open() -> calculate_sizes() -> kasan_cache_create()
static inline unsigned int optimal_redzone(unsigned int object_size)
{
if (IS_ENABLED(CONFIG_KASAN_SW_TAGS))
return 0;
/* 根据obj大小,调整readzone的大小 */
return
object_size <= 64 - 16 ? 16 :
object_size <= 128 - 32 ? 32 :
object_size <= 512 - 64 ? 64 :
object_size <= 4096 - 128 ? 128 :
object_size <= (1 << 14) - 256 ? 256 :
object_size <= (1 << 15) - 512 ? 512 :
object_size <= (1 << 16) - 1024 ? 1024 : 2048;
}
void kasan_cache_create(struct kmem_cache *cache, unsigned int *size,
slab_flags_t *flags)
{
unsigned int orig_size = *size;
unsigned int redzone_size;
int redzone_adjust;
/* 在SLUB Allocator管理的object layout中增加属于kasan的数据结构,包括以下3点: alloc meta,free meta,reazone */
/* Add alloc meta. */
cache->kasan_info.alloc_meta_offset = *size;
*size += sizeof(struct kasan_alloc_meta);
/* Add free meta. */
if (IS_ENABLED(CONFIG_KASAN_GENERIC) &&
(cache->flags & SLAB_TYPESAFE_BY_RCU || cache->ctor ||
cache->object_size < sizeof(struct kasan_free_meta))) {
cache->kasan_info.free_meta_offset = *size;
*size += sizeof(struct kasan_free_meta);
}
redzone_size = optimal_redzone(cache->object_size);
redzone_adjust = redzone_size - (*size - cache->object_size);
if (redzone_adjust > 0)
*size += redzone_adjust;
*size = min_t(unsigned int, KMALLOC_MAX_SIZE,
max(*size, cache->object_size + redzone_size));
/*
* If the metadata doesn't fit, don't enable KASAN at all.
*/
if (*size <= cache->kasan_info.alloc_meta_offset ||
*size <= cache->kasan_info.free_meta_offset) {
cache->kasan_info.alloc_meta_offset = 0;
cache->kasan_info.free_meta_offset = 0;
*size = orig_size;
return;
}
/* 必须保证kasan的元数据填充正确,才开起SLAB_KASAN */
*flags |= SLAB_KASAN;
}
如下图,为开启kasan,slub_debug之后的object layout
当我们第一次创建slab缓存池的时候,系统会调用kasan_poison_slab()函数初始化shadow memory为下图的模样。整个slab对应的shadow memory都填充0xFC。
其函数调用流程为:mm_init() -> kmem_cache_init() -> create_boot_cache() -> __kmem_cache_create() -> kmem_cache_open() -> init_kmem_cache_nodes() -> early_kmem_cache_node_alloc() -> new_slab() -> allocate_slab() -> kasan_poison_slab()
void kasan_poison_slab(struct page *page)
{
unsigned long i;
/* 没开启 CONFIG_KASAN_SW_TAGS 实际上什么也没干 */
for (i = 0; i < compound_nr(page); i++)
page_kasan_tag_reset(page + i);
/* 主要执行该函数,上面有分析,这里主要对对应页和大小,填充0XFC */
#define KASAN_KMALLOC_REDZONE 0xFC /* redzone inside slub object */
kasan_poison_shadow(page_address(page), page_size(page),
KASAN_KMALLOC_REDZONE);
}
图示如下:
ⅡSlub内存申请
当我们进行内存申请时,假设申请20字节数据,即kmalloc(20)的情况。我们知道kmalloc()就是基于SLUB Allocator实现的,所以会从kmalloc-32的kmem_cache中分配一个32 bytes object。Kmalloc最终会调用 __kasan_kmalloc
/**
* round_up - round up to next specified power of 2
* @x: the value to round
* @y: multiple to round up to (must be a power of 2)
* 这个宏的作用是将x取整到y的倍数
* Rounds @x up to next multiple of @y (which must be a power of 2).
* To perform arbitrary rounding up, use roundup() below.
*/
/* 如果y等于16,二进制是10000b。那么只要x的二进制的低4bit不是全0,
那么结果就是 (x + 10000b) & (~1111b) */
#define round_up(x, y) ((((x)-1) | __round_mask(x, y))+1)
static void *__kasan_kmalloc(struct kmem_cache *cache, const void *object, size_t size, gfp_t flags, bool keep_tag)
{
unsigned long redzone_start;
unsigned long redzone_end;
u8 tag = 0xff;
if (gfpflags_allow_blocking(flags))
quarantine_reduce();
if (unlikely(object == NULL))
return NULL;
/* 对于这里而言 假设size=20,不考虑slub申请后的object的虚拟地址是多少,
就假设成0, KASAN_SHADOW_SCALE_SIZE=8 那么redzone=24,
同理redzone=40(前面说过,申请20实际上使用的kmalloc_32) */
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);
if (IS_ENABLED(CONFIG_KASAN_SW_TAGS))
tag = assign_tag(cache, object, false, keep_tag);
/* 这里备注意思就是不用管set_tag,就是传入的object的虚拟地址,
该函数上面有说明,其实就是对object对应的影子内存及其对应长度的影子内存值设置0,
以及对不满8byte的最后一个字节的影子内存设置可访问个数。
比如20,则对应两个byte的影子内存值为0,第三个影子内存的值为4 */
/* Tag is ignored in set_tag without CONFIG_KASAN_SW_TAGS */
kasan_unpoison_shadow(set_tag(object, tag), size);
/* 对object对应影子内存偏移的start(24)和end(40)区间的红区,设置值为0xFC */
kasan_poison_shadow((void *)redzone_start, redzone_end - redzone_start, KASAN_KMALLOC_REDZONE);
if (cache->flags & SLAB_KASAN)
kasan_set_track(&get_alloc_info(cache, object)->alloc_track, flags);
return set_tag(object, tag);
}
图示如下:值得注意的是,kasan实际上只对obj的后面增加的redzone,也就是说如果没有slab_debug,理论上将只能检测数组向后越界,但实际上,由于slab在建立之初就已经对全部obj的对应的影子内存填充了0XFC,所以事实上是肯定能检测出问题的,即就算不开启slab_debug,单纯只开kasan,也是可以的。(注意:下图为slab_debug+kasan的布局)
ⅢSlub内存释放
此时对刚刚申请的kmalloc(20)进行释放,首先看源码
static inline bool shadow_invalid(u8 tag, s8 shadow_byte)
{
/* 如果 shadow_byte < 0 或者大于等于8 那肯定是有问题了 */
if (IS_ENABLED(CONFIG_KASAN_GENERIC))
return shadow_byte < 0 ||
shadow_byte >= KASAN_SHADOW_SCALE_SIZE;
/* 这下面都走不到的 */
/* else CONFIG_KASAN_SW_TAGS: */
if ((u8)shadow_byte == KASAN_TAG_INVALID)
return true;
if ((tag != KASAN_TAG_KERNEL) && (tag != (u8)shadow_byte))
return true;
return false;
}
void kasan_set_free_info(struct kmem_cache *cache,
void *object, u8 tag)
{
struct kasan_free_meta *free_meta;
/* 这里处理obj对应的kasan元数据 */
free_meta = get_free_info(cache, object);
kasan_set_track(&free_meta->free_track, GFP_NOWAIT);
#define KASAN_KMALLOC_FREETRACK 0xFA
/* 最后的最后,把对应obj映射的那个影子内存的首个byte标记为0xFA */
/*
* the object was freed and has free track set
*/
*(u8 *)kasan_mem_to_shadow(object) = KASAN_KMALLOC_FREETRACK;
}
static bool __kasan_slab_free(struct kmem_cache *cache, void *object,
unsigned long ip, bool quarantine)
{
s8 shadow_byte;
u8 tag;
void *tagged_object;
unsigned long rounded_up_size;
tag = get_tag(object); /* tag=0 */
tagged_object = object;
object = reset_tag(object); /* obj=obj不变 */
/* 这个函数的作用是在给定一个内存池(kmem_cache)和一个页面(page)以及一个地址(x),
找到位于该页面上最接近该地址的对象的地址。
如果通过该函数获取到的obj和要释放的obj不是同一个地址,
说明肯定是哪里出现问题了。这里面涉及到内存的红黑树修正,不深究 */
if (unlikely(nearest_obj(cache, virt_to_head_page(object), object) != object)) {
kasan_report_invalid_free(tagged_object, ip);
return true;
}
“RCU托管期内释放的RCU块可在RCU托管期内使用而不违反法律。”
这个语句是针对Linux内核中用于同步的“RCU(Read-Copy Update)机制”的。
这句话的意思是,当一个内存对象在RCU托管期内被释放时,在不进行修改的情况下,
其他部分仍可以合法地访问它。这是因为RCU提供了一种安全地管理共享系统资源并发访问的方法。
RCU的工作原理是,延迟释放对象的内存,直到使用它的所有线程完成操作为止。
在这个“RCU托管期”内,对象的内存仍是系统所有的,并且可以被其他部分在一定限制下合法地访问。
尽管这可能看起来有些不符合直觉,但这是RCU机制的一个关键特性,
能够有效地访问共享数据结构,特别是在高流量场景中。
然而,它需要系统设计人员仔细协调和使用,以确保RCU托管期得到正确管理,并避免潜在的竞态条件。
所以这里如果该块内存是处于RCU的类型的话,无论如何都认为没有问题
/* RCU slabs could be legally used after free within the RCU period */
if (unlikely(cache->flags & SLAB_TYPESAFE_BY_RCU))
return false;
/* 先直接判断一下obj指向的影子内存的第一个字节是否合法,在这之前,
这块内存肯定是被使用的,如果正常的话,第一个影子内存的byte必然大于等于0小于7 */
shadow_byte = READ_ONCE(*(s8 *)kasan_mem_to_shadow(object));
if (shadow_invalid(tag, shadow_byte)) {
kasan_report_invalid_free(tagged_object, ip);
return true;
}
#define KASAN_KMALLOC_FREE 0xFB
/* 这里 rounded_up_size 计算完是40,也就是说
函数 kasan_poison_shadow 将 从obj到红区的全部影子内存(即对应的5byte) 全部标记为0xFB*/
rounded_up_size = round_up(cache->object_size, KASAN_SHADOW_SCALE_SIZE);
kasan_poison_shadow(object, rounded_up_size, KASAN_KMALLOC_FREE);
if ((IS_ENABLED(CONFIG_KASAN_GENERIC) && !quarantine) ||
unlikely(!(cache->flags & SLAB_KASAN)))
return false;
/* 见上面分析 */
kasan_set_free_info(cache, object, tag);
quarantine_put(get_free_info(cache, object), cache);
return IS_ENABLED(CONFIG_KASAN_GENERIC);
}
2.6.3 对于全局变量
Ⅰ全局变量kasan实现
那对于全局变量,kasan是如何操作的呢?首先我们随便找一个带有全局变量的文件反汇编一下,比如下面这个
带kasan反汇编之后的结果为:
不带kasan反汇编的结果为:
也就是说,开启kasan后,编译器会自动识别全局变量,并进行初始化,并最终调用__asan_register_globals(),那么继续看源码实现:
/* The layout of struct dictated by compiler */
struct kasan_global {
const void *beg; /* Address of the beginning of the global variable. */
size_t size; /* Size of the global variable. */
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
};
static void register_global(struct kasan_global *global)
{
/* 按照全局变量的大小向8取整,假设size=4,那么对齐大小为8 */
size_t aligned_size = round_up(global->size, KASAN_SHADOW_SCALE_SIZE);
/* 全局量的起始地址+大小 设置初值,那么这里就是设置0x4 */
kasan_unpoison_shadow(global->beg, global->size);
#define KASAN_GLOBAL_REDZONE 0xF9
/* 对地址+对齐为起始地址 假设地址=0,那么这里起始地址就是8,到红区结束填充0Xf9 */
kasan_poison_shadow(global->beg + aligned_size,
global->size_with_redzone - aligned_size,
KASAN_GLOBAL_REDZONE);
}
void __asan_register_globals(struct kasan_global *globals, size_t size)
{
int i;
for (i = 0; i < size; i++)
register_global(&globals[i]);
}
EXPORT_SYMBOL(__asan_register_globals);
Ⅱ红区填充对齐
带kasan的.map 分布情况,init_mm占用大小0x440->1088
不带kasan的.map分布情况,init_mm占用大小0x388->904
如下图为官方对全局变量的描述,说是32对齐,为啥变成64了?带着这个疑问,我们尝试写几个变量看看
新增3个全局变量。可以看.map。gytest1,实际占用0x40(64字节),gytest2占用0x60(96字节—323),gytest3占用0xA0(160字节—325)也就是说确实是按照32字节对齐的,只不过这里可能存在一个算法
填充的原理是这样的。全局变量实际占用内存总数S(以byte为单位)按照每块32 bytes平均分成N块。假设最后一块内存距离目标32 bytes还差y bytes(if S%32 == 0,y = 0),那么redzone填充的大小就是(y + 32) bytes。画图示意如下(S%32 != 0)。因此总结的规律是:redzone = 63 – (S - 1) % 32。
同时我们可以注意的是:对init_mm反汇编之后发现向x1寄存器中传入的值变为了4,因为在该文件中新增了3个全局变量。size是4就是要初始化全局变量的个数,所以这里只需要一个构造函数即可。一次性将4个全局变量全部搞定。编译器已文件为单位创建一个构造函数,将本文件全局变量一次性全部打包初始化,而__asan_register_globals函数传入的size就是x1寄存器的值
以char a[4]为例,如下图所示。
2.6.4 对于局部变量
Ⅰ官方描述
局部变量官方的pdf是如下描述的:如下为一个foo函数使用局部变量 char a[328],那么kasan如何进行检测呢?
首先第一步,在该数组前增加一个32字节的一个数组,用于检测左越界
第二步,根据上面全局变量的做法,将数组a[328] 32字节对齐(补了24字节)在增加32字节红区,用于检测右越界
第三、四步分别为:计算影子内存地址(注意这里用的rz1作为起始),对红区标记为0xff。这里面比较好理解的是:shadow[0]和shadow[12]。如何理解shadow[11]:其实就是 328/8=41 shadow[1~10]40字节 最后一字节对应的影子内存的第一个字节也是0。如果这里面328换成329 那么shadow[11] = 0xffff0100
但是实际上,对于arm64而言,填入stack的redzone值还有做区分
Ⅱ反汇编
我们直接写一段代码,然后反汇编,查看
0000000000000000 <gytest>:
0: d503201f nop
4: d503201f nop
char gytest2[33];
char gytest3[111];
void gytest(void)
{
8: a9b87bfd stp x29, x30, [sp, #-128]!
c: 90000002 adrp x2, 0 <gytest>
10: 91000042 add x2, x2, #0x0
/* 这里构造出re1,长度32字节,保存在x0寄存器中 */
14: 910083e0 add x0, sp, #0x20
18: 910003fd mov x29, sp
1c: 90000001 adrp x1, 0 <gytest>
20: 91000021 add x1, x1, #0x0
24: a90153f3 stp x19, x20, [sp, #16]
/* 这里将x0寄存器指向的地址右移3,即rz1地址右移3 存到 x20中*/
28: d343fc14 lsr x20, x0, #3
2c: d2dffa13 mov x19, #0xffd000000000 // #281268818280448
30: d2915664 mov x4, #0x8ab3 // #35507
/* 将x4寄存器的第32-16位设置为0x41b5,也就是说x4=0x41b58ab3(这里没理解在干什么) */
34: f2a836a4 movk x4, #0x41b5, lsl #16
/* x19 = DFFFFFD000000000 也就是 KASAN_SHADOW_OFFSET */
38: f2fbfff3 movk x19, #0xdfff, lsl #48
/* x3= rz1地址 + KASAN_SHADOW_OFFSET */
3c: 8b130283 add x3, x20, x19
40: a9020be4 stp x4, x2, [sp, #32]
/* w2 也就是 x2寄存器的低32位 设置为0xf1f1f1f1 */
44: 3204d3e2 mov w2, #0xf1f1f1f1 // #-235802127
/* 将寄存器x1的值存储到以sp为地址偏移 48的内存地址中 */
48: f9001be1 str x1, [sp, #48]
4c: 52804000 mov w0, #0x200 // #512
/* x0 = F3F30200*/
50: 72be7e60 movk w0, #0xf3f3, lsl #16
/* 将w2的值(F1F1F1F1)存储到以x20为地址(rz1>>3)偏移 x19(kasan offset)的内存地址中,也就是将rz1对应的影子内存的权限值设置为0xf1f1f1f1 */
54: b8336a82 str w2, [x20, x19]
/* 将w0的值(F3F30200)存储到以x3为地址(rz1>>3 + kasan offset)偏移4字节的内存地址中,也就是将gy[10]+pad[22]对应的影子内存的权限值设置为0Xf3f30200 */
58: b9000460 str w0, [x3, #4]
5c: d5384101 mrs x1, sp_el0
char gy[10];
gy[0] = 1;
gy[9] = 10;
memset(gytest2, gy, 10);
60: d2800142 mov x2, #0xa // #10
64: 90000000 adrp x0, 0 <gytest>
{
68: f9423c23 ldr x3, [x1, #1144]
6c: f9003fe3 str x3, [sp, #120]
70: d2800003 mov x3, #0x0 // #0
memset(gytest2, gy, 10);
74: 91000000 add x0, x0, #0x0
78: 110103e1 add w1, wsp, #0x40
7c: 94000000 bl 0 <memset>
{
80: f8336a9f str xzr, [x20, x19]
}
84: d5384100 mrs x0, sp_el0
88: f9403fe1 ldr x1, [sp, #120]
8c: f9423c02 ldr x2, [x0, #1144]
90: eb020021 subs x1, x1, x2
94: d2800002 mov x2, #0x0 // #0
98: 54000081 b.ne a8 <gytest+0xa8> // b.any
9c: a94153f3 ldp x19, x20, [sp, #16]
a0: a8c87bfd ldp x29, x30, [sp], #128
a4: d65f03c0 ret
a8: 94000000 bl 0 <__stack_chk_fail>
通过反汇编得出的结果来看,好像没有rz3的存在,修改一下代码实际触发一下kasan,查看报错时,影子内存状态:F1 F1 F1 F1 00 02 F3 F3 确实没有rz3存在(是否和编译器版本有关系?这点不能确定)。但是却也证实了编译器对于kasan对局部变量的操作方式以及影子内存的标定
2.6.5 对于驱动(额外)
Ⅰ驱动的初始化
对于能够随时进行插入的驱动,kasan又是如何做到对驱动中的数据标定影子内存呢?
其源码和调用关系如下:函数调用关系:init_module() -> load_module () -> layout_and_allocat() -> module_alloc () -> move_module () -> module_alloc () -> kasan_module_alloc ()
其中,addr和size是module_alloc传过来的,我们在这里不去深究module区域的地址申请和大小,只研究kasan的实现
#ifndef CONFIG_KASAN_VMALLOC
int kasan_module_alloc(void *addr, size_t size)
{
void *ret;
size_t scaled_size;
size_t shadow_size;
unsigned long shadow_start;
/* 对要加载的驱动申请到的地址,映射对应的影子内存 */
shadow_start = (unsigned long)kasan_mem_to_shadow(addr);
scaled_size = (size + KASAN_SHADOW_MASK) >> KASAN_SHADOW_SCALE_SHIFT;
/* 影子内存按照整页的大小进行对齐 */
shadow_size = round_up(scaled_size, PAGE_SIZE);
if (WARN_ON(!PAGE_ALIGNED(shadow_start)))
return -EINVAL;
/* 这里申请方式和 module_alloc保持一致的 */
ret = __vmalloc_node_range(shadow_size, 1, shadow_start,
shadow_start + shadow_size,
GFP_KERNEL,
PAGE_KERNEL, VM_NO_GUARD, NUMA_NO_NODE,
__builtin_return_address(0));
#define KASAN_SHADOW_INIT 0
if (ret) {
/* 对申请到的影子内存置全0,和buddy类似 */
__memset(ret, KASAN_SHADOW_INIT, shadow_size);
find_vm_area(addr)->flags |= VM_KASAN;
kmemleak_ignore(ret);
return 0;
}
return -ENOMEM;
}
void kasan_free_shadow(const struct vm_struct *vm)
{
if (vm->flags & VM_KASAN)
vfree(kasan_mem_to_shadow(vm->addr));
}
#endif
Ⅱ驱动中的变量映射
我手中的板子使用的4KB的页表的大小,va_bit=39,因此能够计算的出module所在的虚拟内存地址为:FFFF FF80 0000 0000~FFFF FFA0 0000 0000
如下为我写的一个测试驱动
#include <linux/mman.h>
#include <linux/module.h>
#include <linux/printk.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
char gytest[17];
#define OOB_TAG_OFF (IS_ENABLED(CONFIG_KASAN_GENERIC) ? 0 : 8)
static bool multishot;
static int __init test_kasan_module_init(void)
{
char *gy = NULL,i;
char __user *usermem;
char ggyy[10];
int unused;
/* 驱动中开启kasan必须开这个,否则没有报告打印 */
bool multishot = kasan_save_enable_multi_shot();
gy = kmalloc(33, GFP_KERNEL);
printk("gytest stack\n");
for (i = 0; i < 11; i++){
ggyy[i] = i;
printk("ggyy[%d]=%d\n", i, ggyy[i]);
}
memcpy(gytest, ggyy, 17); /* 局部变量必须真的用起来 */
printk("gytest global\n");
for (i = 0; i < 18; i++){
gytest[i] = i;
printk("gytest[%d]=%d\n", i, gytest[i]);
}
printk("gytest slub\n");
for (i = 0; i < 34; i++){
gy[i] = i;
printk("gy[%d]=%d\n", i, gy[i]);
}
kfree(gy);
kasan_restore_multi_shot(multishot);
return -EAGAIN;
}
module_init(test_kasan_module_init);
MODULE_LICENSE("GPL");
加载后有如下报错
可以看出,驱动中的全局变量是在驱动的范围内的,那么映射到的影子内存也是基于驱动的初始化。
而局部变量依旧属于线性映射区,依旧在堆栈中,同样的slub也在,也是依旧调用的对应的SLUB分配器的obj
2.6.6总结
到这里,基本已经对kasan对影子内存的标记已经分析完了(除了vmalloc),总结如下:
3. Kasan映射
3.1 内存分布(arm64)
我们在来看下\Documentation\arm64\memory.rst关于arm64的虚拟内存分布,发现kasan的分区起始地址位于 -1 - (1<<(VA_BIT-1))处。
以我手中的板子VA_BITS=39, 内核虚拟地址空间总长度为:1 << 39 (512G),而影子内存长度是内核虚拟地址空间长度的1/8,因此有
KASAN区为:FFFF FFC0 0000 0000~FFFF FFD0 0000 0000 大小10_0000_0000(64G)
Module区为:FFFF FFD0 0800 0000 ~ FFFF FFD0 1000 0000 总大小128M
CONFIG_KASAN_SHADOW_OFFSET=0xdfffffd000000000
#define KASAN_SHADOW_OFFSET _AC(CONFIG_KASAN_SHADOW_OFFSET, UL)
#define KASAN_SHADOW_END ((UL(1) << (64 - KASAN_SHADOW_SCALE_SHIFT)) + KASAN_SHADOW_OFFSET) 即end为:FFFF FFD0 0000 0000
#define _KASAN_SHADOW_START(va) (KASAN_SHADOW_END - (1UL << ((va) - KASAN_SHADOW_SCALE_SHIFT))) 即start为:FFFFFFC000000000
#define KASAN_SHADOW_START _KASAN_SHADOW_START(vabits_actual)
#define BPF_JIT_REGION_START (KASAN_SHADOW_END) FFFF FFD0 0000 0000
#define BPF_JIT_REGION_SIZE (SZ_128M)
#define BPF_JIT_REGION_END (BPF_JIT_REGION_START + BPF_JIT_REGION_SIZE)
即FFFF FFD0 0800 0000
#define MODULES_END (MODULES_VADDR + MODULES_VSIZE)
即FFFF FFD0 1000 0000
#define MODULES_VADDR (BPF_JIT_REGION_END) 即FFFF FFD0 0800 0000
#define MODULES_VSIZE (SZ_128M)
#define KIMAGE_VADDR (MODULES_END) 即内核起始地址为FFFF FFD0 1000 0000
System.map可以看出,内核起始的虚拟地址确实是ffff_ffd0_1000_0000
可以发现内核的线性地址实际上已经提前了,而提前的原因正是因为kasan
该段话主要讲述:为了在系统启动时允许KASAN阴影更改大小,需要同时修复48位和52位虚拟地址的KASAN_SHADOW_END,并"增大"开始地址。另外,保持内核.text中的函数地址不变化是非常有用的。这两个要求促使我们将内核地址空间对换,使得直接线性映射占据较低的地址。
更具体地说,KASAN是Linux内核中的一种机制,用于检测和报告内存错误(例如越界访问、使用已经释放的内存等)。为了实现这一机制,内核在虚拟地址空间中预留了一个阴影地址空间,用于跟踪每个内存块的状态。该阴影地址空间通常需要在系统启动时进行指定。由于内核支持的虚拟地址空间可以是48位或52位,因此需要修复两个地址范围的KASAN_SHADOW_END。
另外,为了保持内核.text中的函数地址不变,我们需要将直接线性映射放在较低的地址空间上。这样可以避免在地址空间对换时需要修改内核地址重新定位的代码。这种交换还可以使内核代码在所有地址范围内映射的起始地址相同。
因此得出我手里板子(VA_BITS=39)的内存分布如下
#define VMEMMAP_SIZE ((_PAGE_END(VA_BITS_MIN) - PAGE_OFFSET) \
>> (PAGE_SHIFT - STRUCT_PAGE_MAX_SHIFT))
其中VMEMMAP_SIZE如上,不好计算就不算了
补充一个网上更直观的内存布局图(这个是VA_BITS=48的),实际物理内存=2G(在线性映射区),图里还补充了kernel的分布情况。但是下图是未开启kasan的,需要结合一下看
3.2 影子内存映射
首先上最基本的转换关系,如下:
即任意虚拟地址对应的影子地址为:地址 >> 8 +偏移。那么影子内存或者说kasan的内存又是如何管理的呢?这里需要对应看每个架构的不同实现了。这里我们对arm64进行分析
首先我们来看下官方的文档描述:
简单的翻译一下就是:
- 系统启动初期建立的映射,将所有的KASAN区域映射到kasan_zero_page物理页面
- 当页表初始化完成时,对实际物理内存(线性内存区),和内核区做实际映射
- 对驱动做动态的实际映射
3.2.1 kasan_early_init
调用在汇编中(vscode\arch\arm64\kernel\head.S),部分源码如下:
汇编中的调用逻辑大概如下:_head -> primary_entry -> __primary_switch(在这里有init_pg_dir,使能mmu,__relocate_kernel,__create_page_tables等一系列操作) –> __primary_switched(在这里调用kasan_early_init,并最终执行start_kernel)
源码如下:至于kasan_pgd_populate的流程如下:kasan_pgd_populate() -> kasan_p4d_populate() –> kasan_pud_populate() -> kasan_pmd_populate() -> kasan_pte_populate() 实际上就是虚拟地址找页表的过程。
/* The early shadow maps everything to a single page of zeroes */
asmlinkage void __init kasan_early_init(void)
{
/* 前面都是一系列的校验判断 */
BUILD_BUG_ON(KASAN_SHADOW_OFFSET !=
KASAN_SHADOW_END - (1UL << (64 - KASAN_SHADOW_SCALE_SHIFT)));
BUILD_BUG_ON(!IS_ALIGNED(_KASAN_SHADOW_START(VA_BITS), PGDIR_SIZE));
BUILD_BUG_ON(!IS_ALIGNED(_KASAN_SHADOW_START(VA_BITS_MIN), PGDIR_SIZE));
BUILD_BUG_ON(!IS_ALIGNED(KASAN_SHADOW_END, PGDIR_SIZE));
/* 这里做映射,需要注意的第4个参数 early传的是ture */
kasan_pgd_populate(KASAN_SHADOW_START, KASAN_SHADOW_END, NUMA_NO_NODE, true);
}
static void __init kasan_pte_populate(pmd_t *pmdp, unsigned long addr,
unsigned long end, int node, bool early)
{
unsigned long next;
pte_t *ptep = kasan_pte_offset(pmdp, addr, node, early);
do {
/* 对于early阶段,页表还没有完全建立完成。因此使用__pa_symbol
其作用是在内核物理内存的线性映射还没建立的时候,用来根据虚拟地址计算物理地址,具体的不在这里深入分析(可以看这个分析的很好 __pa_symbol 及str_l 解析_kimage_voffset_朝搴夕揽的博客-CSDN博客),总之在这一步,获取到了实际物理地址 */
phys_addr_t page_phys = early ?
__pa_symbol(kasan_early_shadow_page)
: kasan_alloc_raw_page(node);
/* 对于early 来说,虽然上for了那么多次,但实际上是用的一个地址,那就是 kasan_early_shadow_page 也就是说,对于kasan_early_init 来说,把所有虚拟地址映射到了一个页里这个页就是 kasan_early_shadow_page 所占的位置*/
if (!early)
memset(__va(page_phys), KASAN_SHADOW_INIT, PAGE_SIZE);
next = addr + PAGE_SIZE;
/* set_pte,set_pmd,set_pud和set_pgd向一个页表项中写入指定的值 */
/* PAGE_KERNEL是Linux内核中的一个宏定义,用于标识一部分内核数据和代码的页级描述符特征。具体来说,PAGE_KERNEL用于标识常规内核数据和代码的描述符,这些描述符是包含所有权限的标准描述符,例如读、写和执行权限。这意味着这些内核数据和代码可以在内核执行时(即运行在内核态)进行读、写和执行操作。
PAGE_KERNEL往往被用来定义一些常规的内核数据结构,例如内核堆栈空间和内核函数代码等。它与PAGE_KERNEL_EXEC标志的区别在于,后者标识的内核数据和代码区域是可执行的。 */
set_pte(ptep, pfn_pte(__phys_to_pfn(page_phys), PAGE_KERNEL));
} while (ptep++, addr = next, addr != end && pte_none(READ_ONCE(*ptep)));
}
也就是说,在kasan_early_init阶段就只做了一件事情,将内核区全部范围的地址映射到了一个指定的页中。来看下这个页的源码描述
/*
* This page serves two purposes:
* - It used as early shadow memory. The entire shadow region populated
* with this page, before we will be able to setup normal shadow memory.
* - Latter it reused it as zero shadow to cover large ranges of memory
* that allowed to access, but not handled by kasan (vmalloc/vmemmap ...).
*/
unsigned char kasan_early_shadow_page[PAGE_SIZE] __page_aligned_bss;
这段代码注释的意思是,这一页有两个作用:
它被用作早期阴影内存。在我们能够设置正常的阴影内存之前,整个阴影区域都会使用这个页面进行填充。
后来,它被重复使用为零阴影内存,用于覆盖许多内存范围,这些范围允许访问,但不受KASan的处理(例如vmalloc / vmemmap…)。
3.2.2 kasan_init
在页表创建完成后,执行kasan_init意味着kasan功能彻底开启,其调用流程为:
start_kernel
| -> setup_arch
| | - -> arm64_memblock_init
| | - -> page_init (在这里创建页表)
| | | - -> map_kernel
| | | | - -> kasan_copy_shadow (Copy the current shadow region into a new pgdir.相当于给kasan所在虚拟内存创建页表)
| | - -> bootmem_init
| | - -> kasan_init
源码如下:
void __init kasan_init(void)
{
u64 kimg_shadow_start, kimg_shadow_end;
u64 mod_shadow_start, mod_shadow_end;
phys_addr_t pa_start, pa_end;
u64 i;
/* 我手中板子的内存设备树如下 */
memory {
device_type = "memory";
reg = <0x0 0x22000000 0x0 0x20000000>;
};
/* kimg_shadow_start = 0xffff_ffca_0200_0000 */
kimg_shadow_start = (u64)kasan_mem_to_shadow(_text) & PAGE_MASK;
/* kimg_shadow_end = 0xffff_ffca_027a_2000 */
kimg_shadow_end = PAGE_ALIGN((u64)kasan_mem_to_shadow(_end));
/* mod_shadow_start = 0xffff_ffca_0100_0000 */
mod_shadow_start = (u64)kasan_mem_to_shadow((void *)MODULES_VADDR);
/* mod_shadow_end = 0xffff_ffca_0200_0000 */
mod_shadow_end = (u64)kasan_mem_to_shadow((void *)MODULES_END);
/*
* We are going to perform proper setup of shadow memory.
* At first we should unmap early shadow (clear_pgds() call below).
* However, instrumented code couldn't execute without shadow memory.
* tmp_pg_dir used to keep early shadow mapped until full shadow
* setup will be finished.
*/
/* 我们将对阴影内存进行正确的设置。首先,我们需要取消早期的阴影内存映射(如下方的clear_pgds()函数调用)。然而,没有了阴影内存,仪器化的代码无法执行。为了解决这个问题,我们使用tmp_pg_dir来保留早期的阴影内存映射,直到完整的阴影内存设置完成。 */
/* 如何理解这句话?在上面分析过kasan_early_init在内核启动初期,页表没有完全建立的时候,简单粗暴的将kasan的全部映射,都做到了一个固定的页中。而现在页表已经建立完毕,需要把实际物理内存和页表映射关系重新初始化一遍。但是在这个创建的过程中还是要用到之前的页表的,直到kasan的映射关系做完为止 */
memcpy(tmp_pg_dir, swapper_pg_dir, sizeof(tmp_pg_dir));
dsb(ishst);
cpu_replace_ttbr1(lm_alias(tmp_pg_dir));
clear_pgds(KASAN_SHADOW_START, KASAN_SHADOW_END);
/* 对kernel区进行实际映射(在memblock中实际分配内存了) */
kasan_map_populate(kimg_shadow_start, kimg_shadow_end,
early_pfn_to_nid(virt_to_pfn(lm_alias(_text))));
/* 对线性映射区到module区(包括KASAN+BPF+MODULE)做虚假映射(就是没有实际分配内存) */
kasan_populate_early_shadow(kasan_mem_to_shadow((void *)PAGE_END),
(void *)mod_shadow_start);
/* 对kernel到Kasan结束区做虚假映射,和kasan_early_init相同,最终也是映射到了kasan_early_shadow_page,只不过这回是创建的完整的页表但是最终指向这个页 */
kasan_populate_early_shadow((void *)kimg_shadow_end,
(void *)KASAN_SHADOW_END);
if (kimg_shadow_start > mod_shadow_end)
/* 对kernel到module中间的空洞做虚假映射 */
kasan_populate_early_shadow((void *)mod_shadow_end,
(void *)kimg_shadow_start);
for_each_mem_range(i, &pa_start, &pa_end) {
void *start = (void *)__phys_to_virt(pa_start);
void *end = (void *)__phys_to_virt(pa_end);
/* pa_start = 0x2200_0000 */
/* pa_end = 0x4200_0000 */
if (start >= end)
break;
/* 对实际物理内存对应的影子内存的大小做实际映射 */
kasan_map_populate((unsigned long)kasan_mem_to_shadow(start),
(unsigned long)kasan_mem_to_shadow(end),
early_pfn_to_nid(virt_to_pfn(start)));
}
/*
* KAsan may reuse the contents of kasan_early_shadow_pte directly,
* so we should make sure that it maps the zero page read-only.
*/
/* KAsan(Kernel Address Sanitizer)可能直接重用kasan_early_shadow_pte的内容,因此我们需要确保它将0页(即所有字节都为0的页)映射为只读(read-only) */
for (i = 0; i < PTRS_PER_PTE; i++)
set_pte(&kasan_early_shadow_pte[i],
pfn_pte(sym_to_pfn(kasan_early_shadow_page),
PAGE_KERNEL_RO));
memset(kasan_early_shadow_page, KASAN_SHADOW_INIT, PAGE_SIZE);
cpu_replace_ttbr1(lm_alias(swapper_pg_dir));
/* At this point kasan is fully initialized. Enable error messages */
init_task.kasan_depth = 0;
pr_info("KernelAddressSanitizer initialized\n");
}
加了些打印,如下
下面这个图是线性区还没改之前的版本,可以和上面的分析结合理解一下。
下面是我画的,手中设备的实际映射关系:
至此为止,KASAN就分析完了。
4.参考文献
- LinuxCon North America 2015 KernelAddressSanitizer.pdf
- https://www.kernel.org/doc/html/latest/translations/zh_CN/dev-tools/kasan.html(内核地址消毒剂(KASAN) — The Linux Kernel documentation)
- http://www.wowotech.net/memory_management/424.html (KASAN实现原理 (wowotech.net))
- https://blog.csdn.net/pwl999/article/details/116111186 (Linux mem 2.7 内存错误检测 (KASAN) 详解_内存值out of bounds -CSDN博客)
- https://patchwork.kernel.org/project/linux-arm-kernel/patch/20201001152232.274367-5-linus.walleij@linaro.org/
- kernel\Documentation\arm64\memory.rst
- https://www.cnblogs.com/aspirs/p/13909499.html(内存管理:虚拟地址空间布局(AArch64) - aspirs - 博客园 (cnblogs.com))
- https://www.cnblogs.com/zhangzhiwei122/p/16058173.html(arm64 内存相关-线性空间下移-VMEMMAP_SIZE VMEMMAP_START - 博客园 (cnblogs.com))
- https://blog.csdn.net/weixin_42978662/article/details/112613197 (AArch64架构内存布局及线性地址转换_2Jeff2的博客-CSDN博客)
- https://people.kernel.org/linusw/kasan-for-arm32-decompression-stop(KASan for ARM32 Decompression Stop — linusw (kernel.org))
- https://blog.csdn.net/weixin_43512663/article/details/127781953 (__pa_symbol 及str_l 解析_kimage_voffset_朝搴夕揽的博客-CSDN博客)