linux之kasan原理及解析

linux之kasan原理及解析linux 之 kasan 原理分析 kasan

大家好,欢迎来到IT知识分享网。

kasan原理及解析

1. 前言

Kernel Address SANitizer(KASAN)是一种动态内存安全错误检测工具,主要功能是检查内存越界访问和使用已释放内存的问题。

KASAN有三种模式:1.通用KASAN;2.基于软件标签的KASAN;3.基于硬件标签的KASAN

  1. 用CONFIG_KASAN_GENERIC启用的通用KASAN,是用于调试的模式,类似于用户空 间的ASan。这种模式在许多CPU架构上都被支持,但它有明显的性能和内存开销
  2. 基于软件标签的KASAN或SW_TAGS KASAN,通过CONFIG_KASAN_SW_TAGS启用, 可以用于调试和自我测试,类似于用户空间HWASan。这种模式只支持arm64,但其适度的内存开销允许在内存受限的设备上用真实的工作负载进行测试。
  3. 基于硬件标签的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模式使用编译时工具在每个内存访问之前插入有效性检查,因此需要一个 提供支持的编译器版本。基于硬件标签的模式依靠硬件来执行这些检查,但仍然需要一个支持内存标签指令的编译器版本。

1.3 可检查的内存类型

1.4 开启kasan

在这里插入图片描述
在这里插入图片描述

另外CONFIG_KASAN_VMALLOC也需要看架构是否支持,若不支持则无法检测vmalloc要将受影响的slab对象的alloc和free堆栈跟踪包含到报告中,请启用 CONFIG_STACKTRACE。要包括受影响物理页面的分配和释放堆栈跟踪的话, 请启用 CONFIG_PAGE_OWNER 并使用 page_owner=on 进行引导

但是用设备实测,使用outline的方式并没有触发程序中已知的一个数组越界,而inline的方式是可以的 (使用inline插桩,编译器不进行函数调用,而是直接插入代码来检查影子内存),因此这里怀疑可能是因为使用outline的方式插桩的函数没有找到对应的函数调用。所以后续使用皆以inline为准

2. Kasan工作原理

2.1 触发kasan

2.2 Report

结合测试demo和报告输出,先对kasan的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)); } 

翻译过来为:如果阴影字节值在 [0, KASAN_SHADOW_SCALE_SIZE) 范围内,我们可以查看下一个阴影字节来确定无效访问的类型。这意味着访问的内存区域在分配的缓冲区范围之内,但访问可能不合法。在这种情况下,我们可以查看下一个阴影字节来确定坏访问的类型。下一个阴影字节将包含值 0 表示有效访问,非零值表示无效访问。通过检查这个阴影字节,我们可以确定无效访问的类型,例如是否为使用已释放的内存或越界访问。这些信息有助于识别和修复代码中的错误。KASAN_SHADOW_SCALE_SIZE 是一个编译时常量,设置每个字节内存的阴影字节数。

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 影子内存

2.5 Kasan如何检测

2.5.1 如何理解各项检查项:

  1. Size不太可能为0,很好理解,因为既然有内存操作,操作的大小正常也不应该是0。
  2. 地址+大小不太可能小于地址,也很好理解,毕竟不太可能出现size为”负数”的情况
  3. 如何理解地址不太可能小于影子内存?这个我们需要看下影子内存的内存分布。Arm64的内存布局如下(VA_BITS=48,实际我的设备是39,这里使用的内核官方文档的内存布局),可以看到kasan的影子内存基本属于内核区的最顶端,也就是说,内核地址不太可能小于影子内存,如果小于了,那么基本说明该地址可能已经是用户态内存了,那确信就是存在问题。
    在这里插入图片描述
  4. 可能的memory_is_poisoned,也就是说这点是kasan最关注的一个检查项

2.5.2 Kasan检测基本原理:

而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; } 

对于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; } 

2.6 Kasan对影子内存的标定

其实原理也很简单,就是内存申请,释放的时候调用kasan的相关函数。

2.6.1 对于buddy:

/* * 该函数会为从“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); } 

2.6.2 对于SLUB

ⅠSlub初始化填充
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; } 
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); } 
Ⅲ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后,编译器会自动识别全局变量,并进行初始化,并最终调用__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); 
Ⅱ红区填充对齐

新增3个全局变量。可以看.map。gytest1,实际占用0x40(64字节),gytest2占用0x60(96字节—323),gytest3占用0xA0(160字节—325)也就是说确实是按照32字节对齐的,只不过这里可能存在一个算法
在这里插入图片描述
在这里插入图片描述

2.6.4 对于局部变量

Ⅰ官方描述
Ⅱ反汇编
0000000000000000 <gytest>: 0: df nop 4: df nop char gytest2[33]; char gytest3[111]; void gytest(void) { 
    8: a9b87bfd stp x29, x30, [sp, #-128]! c:  adrp x2, 0 <gytest> 10:  add x2, x2, #0x0 /* 这里构造出re1,长度32字节,保存在x0寄存器中 */ 14: e0 add x0, sp, #0x20 18: fd mov x29, sp 1c:  adrp x1, 0 <gytest> 20:  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 // #0448 30: d 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: 8b add x3, x20, x19 40: a9020be4 stp x4, x2, [sp, #32] /* w2 也就是 x2寄存器的低32位 设置为0xf1f1f1f1 */ 44: 3204d3e2 mov w2, #0xf1f1f1f1 // #- /* 将寄存器x1的值存储到以sp为地址偏移 48的内存地址中 */ 48: f9001be1 str x1, [sp, #48] 4c:  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: b str w0, [x3, #4] 5c: d mrs x1, sp_el0 char gy[10]; gy[0] = 1; gy[9] = 10; memset(gytest2, gy, 10); 60: d mov x2, #0xa // #10 64:  adrp x0, 0 <gytest> { 
    68: f9423c23 ldr x3, [x1, #1144] 6c: f9003fe3 str x3, [sp, #120] 70: d mov x3, #0x0 // #0 memset(gytest2, gy, 10); 74:  add x0, x0, #0x0 78: e1 add w1, wsp, #0x40 7c:  bl 0 <memset> { 
    80: f8336a9f str xzr, [x20, x19] } 84: d mrs x0, sp_el0 88: f9403fe1 ldr x1, [sp, #120] 8c: f9423c02 ldr x2, [x0, #1144] 90: eb020021 subs x1, x1, x2 94: d mov x2, #0x0 // #0 98:  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:  bl 0 <__stack_chk_fail> 

2.6.5 对于驱动(额外)

Ⅰ驱动的初始化
对于能够随时进行插入的驱动,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 
Ⅱ驱动中的变量映射

如下为我写的一个测试驱动

#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"); 

2.6.6总结

3. Kasan映射

3.1 内存分布(arm64)

在这里插入图片描述

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 

该段话主要讲述:为了在系统启动时允许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)) 

3.2 影子内存映射

简单的翻译一下就是: 
  1. 系统启动初期建立的映射,将所有的KASAN区域映射到kasan_zero_page物理页面
  2. 当页表初始化完成时,对实际物理内存(线性内存区),和内核区做实际映射
  3. 对驱动做动态的实际映射

3.2.1 kasan_early_init

源码如下:至于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; 

3.2.2 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 0x 0x0 0x>; }; /* 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.参考文献

  1. LinuxCon North America 2015 KernelAddressSanitizer.pdf
  2. https://www.kernel.org/doc/html/latest/translations/zh_CN/dev-tools/kasan.html(内核地址消毒剂(KASAN) — The Linux Kernel documentation)
  3. http://www.wowotech.net/memory_management/424.html (KASAN实现原理 (wowotech.net))
  4. https://blog.csdn.net/pwl999/article/details/ (Linux mem 2.7 内存错误检测 (KASAN) 详解_内存值out of bounds -CSDN博客)
  5. https://patchwork.kernel.org/project/linux-arm-kernel/patch//
  6. kernel\Documentation\arm64\memory.rst
  7. https://www.cnblogs.com/aspirs/p/13909499.html(内存管理:虚拟地址空间布局(AArch64) – aspirs – 博客园 (cnblogs.com))
  8. https://www.cnblogs.com/zhangzhiwei122/p/16058173.html(arm64 内存相关-线性空间下移-VMEMMAP_SIZE VMEMMAP_START – 博客园 (cnblogs.com))
  9. https://blog.csdn.net/weixin_/article/details/ (AArch64架构内存布局及线性地址转换_2Jeff2的博客-CSDN博客)
  10. https://people.kernel.org/linusw/kasan-for-arm32-decompression-stop(KASan for ARM32 Decompression Stop — linusw (kernel.org))
  11. https://blog.csdn.net/weixin_/article/details/ (__pa_symbol 及str_l 解析_kimage_voffset_朝搴夕揽的博客-CSDN博客)

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/120413.html

(0)
上一篇 2025-10-29 21:20
下一篇 2025-10-29 21:26

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注微信