引言
内核态内存错误(越界访问、释放后重用、未初始化读取、双重释放等)是 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-22600 | Netfilter 堆越界 | 权限提升 | KASAN slab-out-of-bounds |
| CVE-2022-0185 | Filecontext 堆溢出 | 容器逃逸 | KASAN global-out-of-bounds |
| CVE-2022-25636 | Netfilter 堆越界 | 远程代码执行 | KASAN slab-out-of-bounds |
| CVE-2022-23222 | eBPF 栈越界 | 权限提升 | KASAN stack-out-of-bounds |
| CVE-2023-1829 | TC 堆 UAF | 权限提升 | KASAN use-after-free |
7. KASAN 性能影响与生产部署
7.1 实测性能开销
| 指标 | 无 KASAN | KASAN_OUTLINE | KASAN_INLINE | KASAN_SW_TAGS | KFENCE |
|---|---|---|---|---|---|
| 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

发表评论 取消回复