引言

内核态内存错误(越界访问、释放后重用、未初始化读取、双重释放等)是 Linux 内核中最难定位且危害最大的 bug 类别之一。这类错误可能导致静默数据损坏、安全漏洞(如提权)、或直接触发 Kernel Panic。传统调试方法(printk、KDB、KGDB)往往在错误发生很久后才观察到异常,因果关系模糊。KASAN(Kernel Address Sanitizer)通过在编译时插桩(Instrumentation)和运行时 Shadow Memory 检查,实现了对每次内存访问的即时检测,能在错误发生的第一时间精确定位出错的代码行堆栈、分配/释放路径和内存状态。自 Linux 3.19 引入以来,KASAN 已被 Google Android 内核、主流云厂商大规模部署,发现了数以千计的内核内存安全漏洞。

1. KASAN 核心原理:Shadow Memory(影子内存)

1.1 核心思想

KASAN 的关键创新是用额外的内存(Shadow Memory)来记录每字节主内存的可访问状态。其映射规则为:

#主内存与 Shadow Memory 的映射关系
#每 8 字节主内存 = 1 字节 Shadow Memory

shadow_addr = (main_addr >> 3) + offset

# offset 的典型值(x86_64):
#   KASAN: 0xdffffc0000000000 (位于直接映射区之上)
#
# Shadow 字节值含义:
#   0x00 → 对应的 8 字节全部可访问(Allocated + Initialized)
#   0x01-0x07 → 只有低 N 字节可访问(部分可用,Redzone 边界)
#   0x80-0xFF → 特殊标记:
#     0xF1 = KASAN_HEAP_LEFT_REDZONE   (左侧红区)
#     0xF2 = KASAN_HEAP_RIGHT_REDZONE  (右侧红区)
#     0xF3 = KASAN_FREED                (已释放)
#     0xF5 = KASAN_STACK_LEFT           (栈左红区)
#     0xF6 = KASAN_STACK_MID            (栈中段标记)
#     0xF7 = KASAN_STACK_RIGHT          (栈右红区)
#     0xF8 = KASAN_STACK_PARTIAL        (栈部分有效)
#     0xFA = KASAN_STACK_AFTER_END      (栈帧之后)
#     0xFD = KASAN_POISON_GENERIC       (通用中毒标记)

1.2 内存布局原理图

// KASAN 全局堆内存布局:
//
//    Shadow 区域                         对应的主内存
// ┌──────────┐                      ┌─────────────────┐
// │ 0x00(8B) │ ──────────────────→  │  可访问区域      │
// ├──────────┤                      ├─────────────────┤
// │ 0x01-07  │ ──────────────────→  │  部分有效(红区内) │
// ├──────────┤                      ├─────────────────┤
// │ 0xF1     │ ──────────────────→  │  堆左红区         │
// ├──────────┤                      ├─────────────────┤
// │ 0x00     │ ──────────────────→  │  实际分配的区域   │
// ├──────────┤                      ├─────────────────┤
// │ 0xF2     │ ──────────────────→  │  堆右红区         │
// ├──────────┤                      ├─────────────────┤
// │ 0xF3     │ ──────────────────→  │  已释放区域       │
// └──────────┘                      └─────────────────┘
//
// KASAN 栈内存布局:
// ┌───────────────┐
// │ 局部变量区域   │ ← 有效 (0x00-0x08)
// ├───────────────┤
// │ Redzone (128) │ ← 左红区 (0xF5)
// ├───────────────┤
// │ 帧指针/返回地址│
// ├───────────────┤
// │ Redzone (128) │ ← 右红区 (0xF7)
// └───────────────┘

1.3 编译时插桩过程

KASAN 通过 GCC/Clang 的 -fsanitize=kernel-address 选项,在编译时将每次内存访问替换为一个运行时检查函数调用:

// 原始 C 代码:
void example(void) {
    char *buf = kmalloc(16, GFP_KERNEL);
    buf[4] = 'A';       // 正常访问
    buf[20] = 'B';      // 越界!
    kfree(buf);
    buf[0] = 'C';       // 释放后使用!
}

// 编译后生成的伪代码:
void example(void) {
    char *buf = kmalloc(16, GFP_KERNEL);
    
    // 编译时自动插入的检查:
    __asan_store1(buf + 4);   // 检查: shadow[buf+4] < 8 → 通过
    buf[4] = 'A';
    
    __asan_store1(buf + 20);  // 检查: shadow[buf+20] = 0xF2 → 红区!报越界
    buf[20] = 'B';
    
    kfree(buf);
    // kfree() 时将整个 buf 区域的 shadow 设为 0xF3
    
    __asan_store1(buf + 0);   // 检查: shadow[buf] = 0xF3 → 已释放!报 UAF
    buf[0] = 'C';
}

1.4 检查函数的实现

// KASAN 编译时插桩调用的运行时检查函数(简化版)
// 文件: mm/kasan/kasan.c

void __asan_load1(unsigned long addr) {
    check_access(addr, 1, false);   // false = 读
}

void __asan_store1(unsigned long addr) {
    check_access(addr, 1, true);    // true = 写
}

// 对于多字节访问 (loadN/storeN, N = 2,4,8,16)
void __asan_loadN(unsigned long addr, size_t size) {
    check_access(addr, size, false);
}

// 核心检查逻辑
static __always_inline void check_access(unsigned long addr, size_t size, bool is_write) {
    if (unlikely(!addr)) return;  // NULL 特殊处理
    
    // 快速路径:先检查对齐性
    // 如果 addr + size 没有跨过 8 字节边界(单 shadow 字节即可判断)
    if (likely(IS_ALIGNED(addr, 8) || 
               (addr & 7) + size <= 8)) {
        // 单字节检查(Fast Path)
        u8 *shadow = shadow_pointer(addr);
        if (unlikely(*shadow != 0)) {
            // 部分红区(0x01-0x07),需检查偏移
            if ((addr & 7) + size > *shadow) {
                kasan_report(addr, size, is_write, _RET_IP_);
            }
        }
    } else {
        // 跨8字节边界(Slow Path)
        check_memory_region_inline(addr, size, is_write, _RET_IP_);
    }
}

// Shadow Memory 地址计算
static __always_inline u8 *shadow_pointer(unsigned long addr) {
    return (u8 *)((addr >> 3) + KASAN_SHADOW_OFFSET);
}

2. KASAN 三种工作模式

2.1 Generic KASAN(通用模式)

即前面描述的标准实现,支持堆、栈、全局变量的越界检测和 UAF 检测。该模式下需要硬件支持(ARM64 的 TCR_EL1.TBI 或 x86_64 的虚拟地址高位),或使用软件回退。适用于开发/调试环境。

// Generic KASAN 配置要求
CONFIG_KASAN=y
CONFIG_KASAN_GENERIC=y          # 通用模式
CONFIG_KASAN_OUTLINE=y          # 大纲模式(代码体积小,推荐)
# CONFIG_KASAN_INLINE is not set # 内联模式(体积小,速度慢)

// 检测能力:
// ✅ 堆越界 (kmalloc/vmalloc)
// ✅ 堆 Use-After-Free
// ✅ 栈越界(局部数组越界、Stack Overflow)
// ✅ 全局变量越界
// ✅ 栈 Return Address 篡改检测

2.2 KASAN_SW_TAGS(软件标签模式)

利用 ARM64 的 Top Byte Ignore(TBI)特性,将标签存储在指针的最高字节。无需 Shadow Memory 映射,对硬件要求低,特别适合生产环境部署:

// KASAN_SW_TAGS 核心原理
// 指针格式(ARM64 TBI 模式下):
// | 高8位标签 | 56位实际地址 |
// | tag      |   address    |
//
// Shadow Memory = 每个线程的#define标签
//
// 指针解引用时比较 tag:
// - 指针高8位 tag == 当前线程 slot tag → 正常
// - 否则 → 报错(UAF 或越界)

CONFIG_KASAN_SW_TAGS=y

// 优势:
// - 无 Shadow Memory 映射开销(可热插拔)
// - 对 RISC-V、ARMv7 等无 TBI 架构也可通过软件回退实现
// - 适合内存受限的嵌入式设备

// Google Android 部署数据:
// 2019 年起 Pixel 手机内核启用 KASAN_SW_TAGS
// 性能损失:<3%(vs. Generic KASAN 的 2-3x)

2.3 KASAN_HW_TAGS(硬件标签模式 - MTE)

基于 ARM v8.5-A 的 Memory Tagging Extension(MTE),硬件实现指针标签和内存标签的同步检查,零性能开销:

// MTE 工作原理(硬件实现):
// 1. 16 位物理地址空间被划分为 16 字节的 granule
// 2. 每个 granule 有 4 位物理标签存储在独立内存区域
// 3. 分配内存时硬件随机生成 granule 标签
// 4. 存储标签到指针的 47:44 位
// 5. 解引用时硬件自动对比指针标签与 granule 标签

CONFIG_KASAN_HW_TAGS=y

// 检测场景:
// - 堆越界 +/- 16 字节(granule 精度)
// - UAF(标签在释放后重新分配时变化)
// - 标签不匹配触发同步异常

// 典型硬件平台:
// - ARM Cortex-X2/A710/A510 及之后
// - Google Tensor G2/G3
// - 高通骁龙 8 Gen 1+

3. KASAN 堆内存检测实现细节

3.1 SLUB Allocator 集成

KASAN 通过在 SLUB 分配器的底层函数(__kmem_cache_alloc)进行后处理,实现分配红区和释放中毒:

// mm/kasan/kasan.c 中 kmalloc 的 hook 实现

void *__kasan_kmalloc(struct kmem_cache *cache, const void *object,
                      size_t size, gfp_t flags) {
    if (unlikely(object == NULL))
        return object;
    
    // 关键步骤 1: 下毒 kfree 对象的 shadow
    // 注意:object(实际指针)可能不等于原始请求的地址
    // 因为 SLAB 可能返回了含 redzone 的大小
    return kasan_set_shadow(object, size);
}

static void *kasan_set_shadow(const void *object, size_t size) {
    void *shadow_begin = shadow_pointer((unsigned long)object);
    void *shadow_end   = shadow_pointer((unsigned long)object + size - 1);
    
    // 将整个对象区域设为 0 (全部可访问)
    memset(shadow_begin, 0, shadow_end - shadow_begin + 1);
    
    // 处理部分有效(最后一个 8 字节中不满 8 的部分标记为 0)
    u8 *tail = (u8 *)shadow_end;
    int remainder = ((unsigned long)object + size) & 7;
    if (remainder) {
        *tail = remainder;  // 标记尾部的无效字节数
    }
    
    return (void *)object;
}

// kfree/__kasan_kfree 的实现
void __kasan_kfree(void *object) {
    struct page *page = virt_to_head_page(object);
    struct kmem_cache *cache = page->slab_cache;
    
    // 获取对象在该 SLAB 中的实际大小
    size_t object_size = cache->size;
    
    // 将整个对象区域的 shadow 设为 0xF3 (KASAN_FREED)
    void *shadow_begin = shadow_pointer((unsigned long)object);
    void *shadow_end   = shadow_pointer((unsigned long)object + object_size - 1);
    memset(shadow_begin, 0xF3, shadow_end - shadow_begin + 1);
    
    // 标记尾部
    u8 *tail = (u8 *)shadow_end;
    int remainder = (object_size) & 7;
    if (remainder) {
        *tail = 0xF3;
    }
}

3.2 Redzone(红区)机制

在分配的内存前后增加保护区,用于捕获越界读写:

// mm/kasan/kasan.c:
// kasan_cache_create() 初始化时设置红区大小
static int kasan_cache_create(struct kmem_cache *cache, unsigned int size) {
    // 右侧红区:在对象后追加 16 字节(或更大)
    cache->kasan_info.alloc_meta = sizeof(struct kasan_alloc_meta);
    cache->kasan_info.free_meta  = sizeof(struct kasan_free_meta);
    
    // 实际对象大小 = 请求大小 + 左红区 + 右红区
    // 左红区存储 alloc meta (分配信息)
    // 右红区全是 0xF2
}

// Redzone 大小
// 彩色区域为红区:
// ┌──────────┬───────────────────┬──────────┐
// │ 32 bytes │    实际分配区域     │ 32 bytes │
// │ 左红区    │  (N bytes)         │ 右红区    │
// │ 0xF1     │  0x00               │ 0xF2     │
// └──────────┴───────────────────┴──────────┘
//       ↑ kmalloc返回指针指向这里

// 全局变量也使用相同的红区机制
// 例如:char global[12] → KASAN 扩充为 12 + 64 = 76 字节
// 访问 global[12] → shadow 0xF2 → 越界报告

3.3 Stack 检测实现

KASAN 在函数入口处通过编译器插入的代码毒化栈变量的红区:

// 编译器插入的代码示例(伪汇编):
//
// function_prologue:
//   // 正常栈帧操作
//   push rbp
//   mov rbp, rsp
//   sub rsp, 64       ; 为局部变量分配栈空间
//
//   // KASAN 插入的插桩代码:
//   mov rdi, rbp
//   sub rdi, 64       ; 栈变量起始地址
//   mov esi, 32       ; 栈变量大小
//   call __kasan_unpoison_stack  ; 标记栈变量为 0x00
//
//   ; 标记左红区 (128字节)
//   lea rdi, [rbp - 64 - 128]
//   mov esi, 128
//   mov edx, 0xF5     ; KASAN_STACK_LEFT
//   call __kasan_poison_stack
//
//   ; 标记右红区 (128字节)
//   lea rdi, [rbp]
//   mov esi, 128
//   mov edx, 0xF7     ; KASAN_STACK_RIGHT
//   call __kasan_poison_stack

// 退出时清除(避免泄漏):
// function_epilogue:
//   mov rdi, rbp
//   sub rdi, 32
//   clear_stack_shadow

4. KASAN 报告解析与定位技巧

4.1 标准报告格式

当 KASAN 检测到内存错误时,会输出详细报告。以下是一次典型越界报告的分析:

==================================================================
BUG: KASAN: slab-out-of-bounds in my_oob_write+0x6c/0x98 [my_module]
Write of size 1 at addr ffff88800637a030 by task insmod/1234
-----------------------------------------------------------------------------

CPU: 1 PID: 1234 Comm: insmod Tainted: G           O      5.15.0 #1
Hardware name: QEMU Standard PC (i440FX + PIIX, 1996)
Call Trace:
 dump_stack_lvl+0x46/0x5b
 print_description+0x1f/0x50
 kasan_report+0x115/0x130
 __asan_store1+0x68/0x90
 my_oob_write+0x6c/0x98 [my_module]
 init_module+0x1a/0x1000 [my_module]
 do_one_initcall+0x56/0x1c0
 ...

Allocated by task 1234:
 ...
 kmalloc_trace+0x24/0x90
 kmalloc include/linux/slab.h:581
 my_oob_write+0x38/0x98 [my_module]
 init_module+0x1a/0x1000 [my_module]
 ...

Freed by task 1234:
 ...
 kfree+0x11c/0x2e0
 buggy_use_after_free+0x50/0xa8 [my_module]
 ...

The buggy address belongs to the object at ffff88800637a000
 which belongs to the cache kmalloc-32 of size 32             ← 分配大小只有 32 字节
The buggy address is located 48 bytes to the right of         ← 越界偏移 +48
 the buggy object (偏移量 = 报告地址 - 分配地址)

Memory state around the buggy address:                        ← Shadow Memory 十六进制输出
 ffff88800637a000: 00 00 00 00 00 00 00 00 00 00 00 00 02 f8 f8 f8
 ffff88800637a010: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
 ...

==================================================================

4.2 报告关键字段解读

  • slab-out-of-bounds: 在 SLAB 分配的内存区域外读写
  • use-after-free: 访问已释放的内存
  • stack-out-of-bounds: 栈越界
  • global-out-of-bounds: 全局变量越界
  • Write/Read of size N: 访问操作类型和大小
  • Allocated by: 分配时的完整调用栈(只有 KASAN 开启了 CONFIG_KASAN_STACK 时才有)
  • Freed by: 释放时的调用栈
  • N bytes to the right of: 越界偏移量(+红区则 +32/+64 等)

4.3 常见错误场景报告模式

// 场景 1: 堆越界 (Heap OOB)
BUG: KASAN: slab-out-of-bounds in func+0x.../0x... [module]
Write of size 4 at addr ffff88800a1b23c0
Allocated by task 100:
  kmalloc include/linux/slab.h:560
The buggy address is located 20 bytes to the right of the buggy object

// 场景 2: 释放后使用 (Use-After-Free)
BUG: KASAN: use-after-free in func+0x.../0x... [module]
Read of size 8 at addr ffff88800a1b23a0
Freed by task 100:
  kfree+0x11c/0x2e0
The buggy address belongs to the cache kmalloc-64

// 场景 3: 双重释放 (Double Free)
BUG: KASAN: double-free or invalid-free in func+0x.../0x... [module]
// KASAN 在 free 时检查 shadow 区域,如果发现已经是 FREED (0xF3)
// 则报 double-free

// 场景 4: 栈越界 (Stack OOB)
BUG: KASAN: stack-out-of-bounds in func+0x.../0x...
Write of size 1 at addr ffffc90000123abc
The buggy address belongs to the variable [stack_var]
 which is in the [stack_task] of task 500

// 场景 5: 全局变量越界
BUG: KASAN: global-out-of-bounds in func+0x.../0x...
Read of size 2 at addr ffffffe123456789
The buggy address belongs to the variable
 my_global_array [arr+90] which belongs to the .data section

5. 进阶功能:KASAN 补充工具与优化

5.1 KASAN_EXTRA(KASAN 双倍检测)

开启 CONFIG_KASAN_EXTRA 后,KASAN 会分配双倍大小的红区,能检测更大范围的越界(但内存消耗更高):

CONFIG_KASAN=y
CONFIG_KASAN_EXTRA=y     # 双倍红区 (128 字节 → 256 字节)
// 红区加倍后:
// - 正常 OOB 大小为 32 字节偏移时仍可检测
// - 适用于 fuzz 测试和更大的 buffer 溢出场景
// - 内存开销增加约 1.5x

// 红区大小对照表:
// KASAN 默认: Alloc meta = 0, Redzone = 32 bytes, Trailer = 8 bytes
// KASAN_EXTRA: Alloc meta = 16, Redzone = 64 bytes, Trailer = 8 bytes

5.2 KUnit(内核单元测试)与 KASAN 集成

Linux 内核的 KUnit 测试框架内置了对 KASAN 的支持。创建 KASAN 测试用例非常简单:

// 示例: KASAN 自测试用例 (lib/test_kasan.c)
#include <kunit.h>

static void kasan_test_oob_left_redzone(struct kunit *test)
{
    char *ptr = kmalloc(16, GFP_KERNEL);
    
    // KUnit 期望 KASAN 报告触发
    KUNIT_EXPECT_KASAN(test, ptr[-1] = 'x',
        "accessing left redzone should trigger KASAN");
    
    kfree(ptr);
}

static void kasan_test_oob_right_redzone(struct kunit *test)
{
    char *ptr = kmalloc(16, GFP_KERNEL);
    int i = 16;
    
    KUNIT_EXPECT_KASAN(test, ptr[i] = 0,
        "accessing right redzone should trigger KASAN");
    
    kfree(ptr);
}

static void kasan_test_uaf(struct kunit *test)
{
    char *ptr = kmalloc(16, GFP_KERNEL);
    kfree(ptr);
    
    KUNIT_EXPECT_KASAN(test, ptr[0] = 0,
        "use-after-free should trigger KASAN");
}

static struct kunit_case kasan_test_cases[] = {
    KUNIT_CASE(kasan_test_oob_left_redzone),
    KUNIT_CASE(kasan_test_oob_right_redzone),
    KUNIT_CASE(kasan_test_uaf),
    {}
};

static struct kunit_suite kasan_test_suite = {
    .name = "kasan",
    .test_cases = kasan_test_cases,
};
kunit_test_suite(kasan_test_suite);

// 运行方式:
// $ ./tools/testing/kunit/kunit.py run --kunitconfig=lib/Kconfig.kasan
// [KASAN] Start testing KASAN
// [KASAN] KASAN: slab-out-of-bounds in kasan_test_oob_right_redzone+0x...
// [KASAN] Passed: 3/3 tests

5.3 KFENCE:KASAN 的轻量级补充

对于性能敏感的生产环境,Google 开发了 KFENCE(Kernel Electric Fence),它在采样基础上提供特定场景的内存安全检测:

// KFENCE vs KASAN 对比:
//
// KFENCE:
// - 采样检测 (~1% 的堆分配)
// - 无编译插桩,运行时几乎零开销
// - 基于 Guard Page(缺页异常检测)
// - 检测范围: 堆越界 + UAF
// - 适用于生产环境
// - Linux 5.12+
//
// KASAN:
// - 全量检测 (100% 的内存访问)
// - 编译时插桩,2-10x 性能损失
// - 检测范围: 堆/栈/全局 越界 + UAF + Double Free
// - 适用于开发/测试环境

CONFIG_KFENCE=y
CONFIG_KFENCE_SAMPLE_INTERVAL=1000   # 每 1000 次分配采样 1 次
CONFIG_KFENCE_NUM_OBJECTS=256        # 同时监控的对象数
CONFIG_KFENCE_DEFERRABLE=y           # 允许运行时调整间隔

// KFENCE 工作原理:
// 1. 从 SLUB 分配一个页面 (4KB)
// 2. 将被分配对象放在页面左端或右端(随机)
// 3. 另一端设为 Guard Page (不映射)
// 4. 越界访问即触发 Page Fault → 报告
// 5. 释放时再次设为 Guard Page → UAF 触发 Page Fault

6. KASAN 在漏洞挖掘中的应用

6.1 Syzkaller + KASAN 自动化漏洞挖掘

Syzkaller 是 Google 开发的内核 Fuzzing 工具,与 KASAN 结合已成为内核漏洞挖掘的黄金标准:

// syzkaller 典型工作流
// 1. 编译开启 CONFIG_KASAN 的内核
// 2. 在 QEMU 中启动
// 3. syzkaller 自动生成随机 syscall 序列
// 4. 通过 Hipercall 注入syscall
// 5. KASAN 监控每次内存访问
// 6. 发现崩溃后保存 PoC 和报告

// Google 项目 syzbot 的发现数据 (截至 2023):
// - 日活内核: android-4.14/4.19/5.4/5.10/5.15
// - 累计发现 KASAN 报告: 13,000+
// - 已修复 KASAN 漏洞: 9,000+
// - 日均新增报告: ~50 (有效 ~20)
// - 平均修复周期: 7-14 天

// 关键配置:
CONFIG_KASAN=y
CONFIG_KASAN_OUTLINE=y
CONFIG_KASAN_STACK=y
CONFIG_KASAN_KUNIT_TEST=y
CONFIG_DEVMEM=y
CONFIG_USER_NS=y

6.2 KASAN 发现的真实 CVE 举例

CVE类型影响检测方式
CVE-2021-22600Netfilter 堆越界权限提升KASAN slab-out-of-bounds
CVE-2022-0185Filecontext 堆溢出容器逃逸KASAN global-out-of-bounds
CVE-2022-25636Netfilter 堆越界远程代码执行KASAN slab-out-of-bounds
CVE-2022-23222eBPF 栈越界权限提升KASAN stack-out-of-bounds
CVE-2023-1829TC 堆 UAF权限提升KASAN use-after-free

7. KASAN 性能影响与生产部署

7.1 实测性能开销

指标无 KASANKASAN_OUTLINEKASAN_INLINEKASAN_SW_TAGSKFENCE
SpecCPU2017 (整数)基线 100%65-75% (-35%)35-50% (-60%)92-97%98-100%
内存延迟 (sysbench)基线+2-3x+5-10x+5-15%+0-2%
内存占用增加0%+30-50%+80-120%+5-10%+1-3%
代码体积增长0%+15-25%+50-80%+2-5%~0%
适用场景生产开发/测试深度调试Android 生产服务器生产

7.2 生产环境部署策略

// Google Android 部署模型(分层部署):

// 层级 1: 开发环 (Dogfood devices)
//   → 全量 KASAN + KASAN_EXTRA
//   → 开启 KASAN_ALL (所有子系统)
//   → Syzkaller 7x24 小时 Fuzz

// 层级 2: Beta 环 (Beta/Canary builds)
//   → KASAN_SW_TAGS(低开销)
//   → 开启 KFENCE (采样检测)
//   → syzkaller 持续监控

// 层级 3: 稳定环 (Stable/Public release)
//   → KFENCE (生产级实时检测)
//   → KASAN_SW_TAGS (部分设备可选)
//   → CTS/VTS 测试覆盖 KASAN 用例

// 云厂商 (AWS/GCP/Azure) 策略:
// - 构建时: KASAN 内核跑 CI 测试
// - 运行时: KFENCE 全量开启
// - 关键客户: 可选 KASAN_SW_TAGS 内核

8. 实战:编写自定义 KASAN 测试模块

8.1 演示模块代码

// my_kasan_demo.c - 演示 KASAN 各种内存错误的模块

#include <linux/module.h>
#include <linux/slab.h>
#include <linux/vmalloc.h>

// 测试 1: 堆越界 (Heap OOB - 向右)
static int test_heap_oob_right(void)
{
    char *buf = kmalloc(16, GFP_KERNEL);
    if (!buf) return -ENOMEM;
    
    pr_info("test_heap_oob_right: buf = %p\n", buf);
    buf[24] = 'O';  // 越界 +8 字节(越过右红区前的有效区域)
    // KASAN 会在此触发 slab-out-of-bounds
    
    kfree(buf);
    return 0;
}

// 测试 2: 释放后使用 (Use-After-Free)
static int test_use_after_free(void)
{
    u64 *data = kmalloc(sizeof(u64), GFP_KERNEL);
    if (!data) return -ENOMEM;
    
    *data = 0xDEADBEEF;
    kfree(data);
    
    // 释放后访问 (且 KFENCE 或 KASAN 检测到)
    if (*data == 0xDEADBEEF)  // KASAN: use-after-free
        pr_info("UAF detected!\n");
    
    return 0;
}

// 测试 3: 双重释放
static int test_double_free(void)
{
    char *buf = kmalloc(32, GFP_KERNEL);
    if (!buf) return -ENOMEM;
    
    kfree(buf);
    kfree(buf);  // KASAN: double-free
    
    return 0;
}

// 测试 4: 栈越界
static int test_stack_oob(void)
{
    char buf[8];
    
    memset(buf, 0, sizeof(buf));
    buf[10] = 'S';  // KASAN: stack-out-of-bounds
    
    return 0;
}

// 测试 5: 全局越界
static char my_global[8];
static int test_global_oob(void)
{
    my_global[12] = 'G';  // KASAN: global-out-of-bounds
    return 0;
}

static int __init my_kasan_demo_init(void)
{
    pr_info("KASAN Demo module loaded (expect crashes)\n");
    // 取消下面任意一项注释来测试相应错误
    // test_heap_oob_right();
    // test_use_after_free();
    // test_double_free();
    // test_stack_oob();
    // test_global_oob();
    return 0;
}

static void __exit my_kasan_demo_exit(void) {
    pr_info("KASAN Demo module unloaded\n");
}

module_init(my_kasan_demo_init);
module_exit(my_kasan_demo_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("KASAN Detection Demo Module");

// Makefile:
// obj-m += my_kasan_demo.o
// all:
//     make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

8.2 编译与运行

# 步骤 1: 编译测试模块
$ make KCONFIG_CONFIG=/path/to/kasan.config

# 步骤 2: 启动 QEMU KASAN 内核
$ qemu-system-x86_64 \
    -kernel arch/x86/boot/bzImage \
    -append "console=ttyS0 kasan_multi_shot" \
    ... (其他 QEMU 参数)

# 步骤 3: 加载模块
$ insmod my_kasan_demo.ko

# 步骤 4: 查看 KASAN 报告
$ dmesg | grep -A 50 "BUG: KASAN"

# 输出示例:
# BUG: KASAN: slab-out-of-bounds in test_heap_oob_right+0x58/0x80
# Write of size 1 at addr ffff88800a123456
# Allocated by task 500:
#  kfree+0x24/0x90
#  test_heap_oob_right+0x20/0x80 [my_kasan_demo]

9. 总结

KASAN 是 Linux 内核开发中不可或缺的内存安全工具。本文系统阐述了 KASAN 的三大核心机制:基于 Shadow Memory 的状态映射、编译时即时插桩、以及三级工作模式的选择。在 Google 和全球安全团队的共同努力下,KASAN + Syzkaller 的组合已成为内核漏洞挖掘的事实标准,持续为 Linux 内核的安全性保驾护航。

对于内核开发者,建议将 KASAN 集成到 CI/CD 流水线的必跑环境中,同时在性能允许的设备上始终保持 KASAN_SW_TAGS 或 KFENCE 开启。内存问题越早发现,修复成本越低——KASAN 的核心价值在于将"崩溃后数周才发现的 bug 模式"转变为"开发时即时反馈的开发体验提升"。

关键配置速查:

  • 开发环境: CONFIG_KASAN=y + CONFIG_KASAN_OUTLINE=y
  • 深度调试: CONFIG_KASAN=y + CONFIG_KASAN_INLINE=y + CONFIG_KASAN_EXTRA=y
  • Android: CONFIG_KASAN=y + CONFIG_KASAN_SW_TAGS=y + CONFIG_KFENCE=y
  • 生产服务器: CONFIG_KFENCE=y
  • ARM MTE 硬件: CONFIG_KASAN_HW_TAGS=y
点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部