一次构建成功、一次无告警运行、一个用例 SKIP,分别代表不同结果。内核验证需要把输入、执行路径、环境前提与结果留在同一份记录里。
| 查阅目标 | 阅读位置 |
| 定位执行路径 | 日志与 Dynamic Debug |
| 验证不同边界 | KUnit、kselftest、sanitizer、模糊测试 |
| 失败分支与交付证据 | 故障注入、复现记录 |
printk 与控制台等级
pr_err、pr_info、pr_debug 等提供日志入口,dev_* 形式附带设备上下文。日志记录到内核缓冲与最终输出到控制台是不同阶段,console_loglevel 过滤控制台输出,不等于删除所有较低等级记录。
日志等级数值越小越严重。console_loglevel=4 通常输出数值小于 4 的消息,即 error 及更严重级别;不应把这项设置描述为系统只有这些日志存在。高频路径日志还会改变时序与性能,需要限速或改用合适跟踪点。
Dynamic Debug 控制已有调用点
dynamic debug 在支持并编译了相应调用点的前提下,按模块、函数、文件、行号或其他条件开启 pr_debug/dev_dbg 等信息。它不能凭空给任意没有调试语句的函数添加入口日志。
当前文档提供 /proc/dynamic_debug/control;启用并挂载 debugfs 时也可能有 /sys/kernel/debug/dynamic_debug/control。先读取实际存在的控制文件,确认条目和原始 flags,再做有限范围修改。Dynamic debug
例如命令字符串 module my_driver +p 表示启用对应模块条目;-p 关闭打印,f/l/t 等标志添加函数、行号和线程信息。恢复时应按先前状态恢复,不要无条件清掉其他人已经启用的调试点。
KUnit 与 kselftest 检查不同边界
KUnit 在内核环境中验证内部函数或子系统逻辑,可通过 UML、QEMU 或测试内核执行。kselftest 通常从用户空间检查内核接口与行为,有些套件会配合测试模块;两者互补。KUnit 入门、kselftest
KUnit 不只有 KUNIT_CASE 数组,还需要注册 suite,并在 Kconfig/Kbuild 中接入。下面用缓冲区区间检查作为例子:容量为 capacity,访问 [offset, offset+length);允许长度为零且 offset 恰好等于容量,但禁止越界。若直接判断 offset + length <= capacity,无符号加法回绕可能把非法区间误认为合法。
保存为 kb_range_test.c:
#include <kunit/test.h>
#include <linux/module.h>
#include <linux/limits.h>
static bool range_fits(size_t capacity, size_t offset, size_t length)
{
return offset <= capacity && length <= capacity - offset;
}
static void range_boundary_test(struct kunit *test)
{
KUNIT_EXPECT_TRUE(test, range_fits(4096, 4092, 4));
KUNIT_EXPECT_FALSE(test, range_fits(4096, 4092, 5));
KUNIT_EXPECT_TRUE(test, range_fits(4096, 4096, 0));
KUNIT_EXPECT_FALSE(test, range_fits(4096, 4097, 0));
KUNIT_EXPECT_TRUE(test, range_fits(0, 0, 0));
}
static void range_overflow_test(struct kunit *test)
{
KUNIT_EXPECT_FALSE(test, range_fits(4096, SIZE_MAX - 3, 4));
KUNIT_EXPECT_TRUE(test, range_fits(SIZE_MAX, SIZE_MAX, 0));
KUNIT_EXPECT_FALSE(test, range_fits(SIZE_MAX, SIZE_MAX, 1));
}
static struct kunit_case range_cases[] = {
KUNIT_CASE(range_boundary_test),
KUNIT_CASE(range_overflow_test),
{}
};
static struct kunit_suite range_suite = {
.name = "kb-range",
.test_cases = range_cases,
};
kunit_test_suite(range_suite);
MODULE_LICENSE("GPL");
第一组检查排他上界和空区间契约,第二组检查最大整数附近的加法风险。offset <= capacity 还保证了后续减法不会下溢;C 的 && 短路求值使第二项只在此前提成立时计算。这个 helper 只验证数值范围,不保证指针有效、页面可访问或设备有权执行 DMA。
接入测试源码目录时,增加对应 Kconfig 条目和 Kbuild 规则,并让上层配置与构建包含该目录:
config KB_RANGE_KUNIT_TEST
tristate "Range boundary tests" if !KUNIT_ALL_TESTS
depends on KUNIT
default KUNIT_ALL_TESTS
obj-$(CONFIG_KB_RANGE_KUNIT_TEST) += kb_range_test.o
对接入了该测试的干净源码树,可在独立构建目录使用 .kunitconfig 启用 CONFIG_KUNIT=y 与 CONFIG_KB_RANGE_KUNIT_TEST=y,再按 suite 名称筛选。过滤器只选择已编入的测试,不能代替配置启用。
./tools/testing/kunit/kunit.py run --build_dir=.kunit-range --kunitconfig=.kunitconfig kb-range
默认 UML 路径会构建并运行一个用户态 Linux 测试内核,不是在当前宿主加载测试模块。源码树若已有不兼容的构建产物,应使用单独准备的干净源码副本;不能为了教程命令直接对正在维护的内核树执行 mrproper。KUnit 配置、构建与过滤
内建测试与模块测试的执行时机不同。kunit.py 能帮助配置、构建和执行测试,首次运行可能需要完整的测试内核构建;仅编译该 C 文件并不算测试已经执行。
结果解释:SKIP 与 PASS 分开记录
selftests 的 TARGETS 可筛选套件,但网络、BPF 等测试需要对应内核配置、权限和依赖,并可能改变测试环境。优先运行与修改相关的套件,记录 SKIP 原因;“全部跳过”不是“全部通过”。
sanitizer、锁检查与断点各自提供证据
KASAN 关注适用的越界和释放后使用,KCSAN 帮助发现数据竞争,lockdep 检查锁依赖等约束。它们受配置、执行路径与采样机制影响,不能把未报告问题当作无错误证明。
KGDB 允许在适当配置和调试链路下使用 GDB 调试内核。SysRq-g 可能停住目标内核等待调试器,适合有恢复手段的测试环境;不应作为日常只读诊断命令直接执行。
断点和日志都会改变调度时序。难复现竞争可结合 tracepoint、最小输入与重复触发,保存发生先后关系,而不是只保留最后的栈。
syzkaller 与 LTP
syzkaller 使用系统调用描述、资源关系与覆盖率反馈生成并变异程序,在配置好的目标环境中寻找崩溃、挂起等异常,再尝试最小化和复现。QEMU 是常用执行后端,不是唯一后端;自动 bisect 也需要相应基础设施,不能把任意运行都说成包含它。syzkaller
LTP 提供系统接口等回归测试集合,适合补充内核和 libc 交互行为验证。用例失败可能来自内核、环境、权限或测试前提,应保留具体结果与版本,而不是只报告一个总失败数。LTP
故障注入:把不可控失败变成可验证输入
内存分配、I/O 和函数返回等注入点帮助检查清理、重试和回滚路径。框架的 probability、interval、times、task-filter 及过滤条件共同影响触发;单独把 failslab/interval 设为 100 不保证每 100 次任意分配必定失败一次。故障注入
| 阶段 | 应确认的事实 |
| 注入前 | 目标任务/路径已限定,基线行为明确 |
| 注入中 | 预期失败确实发生,清理与回滚可验证 |
| 注入后 | 资源已回收,相关参数恢复 |
理论示例无需在当前宿主开启全局注入;测试 VM 更便于复现和恢复。
按获取顺序逐个失败,检查是否只释放已拥有的资源
假设初始化依次获取 context、buffer、registration,全部成功后才把对象发布给调用者。应分别测试每一步失败,而不是只让第一次分配失败一次就称为“错误路径已覆盖”。
| 失败位置 | 失败前已拥有 | 应回滚的顺序 |
| context | 无 | 不释放未获得的资源 |
| buffer | context | context |
| registration | context、buffer | buffer → context |
| 全部成功后退出 | 三项资源,且对象已发布 | 先停止对外发布,再按资源依赖清理 |
下面是确定性的本地生命周期模型。第 fail_at 次获取在建立资源前失败,成功对象的所有权转交给调用者;退出时先撤销发布,再释放资源。它不会开启宿主 failslab,也不模拟真正设备的异步回调。
class Resources:
def __init__(self, fail_at=None):
self.fail_at = fail_at
self.attempts = 0
self.live = set()
self.freed = []
self.published = None
def acquire(self, name):
self.attempts += 1
if self.attempts == self.fail_at:
raise MemoryError(name)
assert name not in self.live
self.live.add(name)
return name
def release(self, name):
assert self.published is None and name in self.live
self.live.remove(name)
self.freed.append(name)
def create(self):
acquired = []
try:
for name in ("context", "buffer", "registration"):
acquired.append(self.acquire(name))
except MemoryError:
for name in reversed(acquired):
self.release(name)
raise
self.published = tuple(acquired)
return self.published
def destroy(self, owned):
assert self.published is owned
self.published = None
for name in reversed(owned):
self.release(name)
for failure, expected in [(1, []), (2, ["context"]), (3, ["buffer", "context"])]:
resources = Resources(failure)
try:
resources.create()
except MemoryError:
pass
else:
raise AssertionError("目标失败未发生")
assert resources.attempts == failure
assert not resources.live and resources.published is None
assert resources.freed == expected
resources = Resources()
owned = resources.create()
assert resources.live == set(owned) and not resources.freed
resources.destroy(owned)
assert not resources.live and resources.published is None
assert resources.freed == ["registration", "buffer", "context"]
这里不仅检查函数返回失败,还检查目标注入点确实到达、后续获取没有继续、资源没有泄漏或重复释放,以及未完成对象没有对外可见。这些才是错误路径的可验证不变量。
真实驱动的注册操作可能在返回前启动工作,或者外部对象仍持有引用;撤销发布后还需取消新请求、等待在途操作和回调退出,再释放其使用的 buffer/context。机械地反转几条 free 语句并不足够。注入 API 的概率与过滤参数用于触发路径,资源与可见性检查用于判断该路径是否正确,两者不能互相替代。
把失败点放在不同资源取得之后,再切换清理方式。除了剩余资源数量,还检查是否在依赖者仍活着时提前释放了它所依赖的对象。
正在呈现知识画面
失败落在不同位置,清理责任怎样变化?失败路径只能释放已经取得的资源,并按使用依赖撤销。最后资源数为零也不足以证明中途没有先释放仍被使用的缓冲区。
{"id":"failure-injection-rollback","title":"失败落在不同位置,清理责任怎样变化?","summary":"失败路径只能释放已经取得的资源,并按使用依赖撤销。最后资源数为零也不足以证明中途没有先释放仍被使用的缓冲区。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><label for=\"failAt\" data-i18n=\"failAt\"></label><select id=\"failAt\"><option value=\"0\" data-i18n=\"buffer\"></option><option value=\"1\" data-i18n=\"irq\"></option><option value=\"2\" selected data-i18n=\"interface\"></option><option value=\"3\" data-i18n=\"success\"></option></select><label for=\"policy\" class=\"caption\" data-i18n=\"policy\"></label><select id=\"policy\"><option value=\"correct\" data-i18n=\"correct\"></option><option value=\"leak\" data-i18n=\"leak\"></option><option value=\"forward\" data-i18n=\"forward\"></option></select><div class=\"actions\"><button id=\"run\" data-i18n=\"run\"></button><button id=\"remove\" data-i18n=\"remove\"></button></div><div class=\"track-row\"><span data-i18n=\"resources\"></span><div id=\"resources\" class=\"track\"></div></div><div class=\"metrics\"><div><span data-i18n=\"remaining\"></span><strong id=\"remaining\"></strong></div><div><span data-i18n=\"hazards\"></span><strong id=\"hazards\"></strong></div></div><div class=\"track-row\"><span data-i18n=\"trace\"></span><div id=\"trace\" class=\"track\"></div></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"note\"></p></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n.caption{display:block;margin:14px 0 5px}#trace{display:grid;grid-template-columns:1fr}#trace .cell{min-height:28px;font:11px var(--viz-font-ui)}.track#resources .cell{min-height:60px}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}let live=[false,false,false],hazards=0,trace=[],success=false;const names=['buffer','IRQ','interface'];function cleanup(){const policy=$('#policy').value,order=policy==='forward'?[0,1,2]:[2,1,0];for(const i of order){if(!live[i])continue;if(policy==='leak'&&i===0){trace.push('KEEP buffer');continue;}if((i===0&&live[1])||(i===1&&live[2])){hazards++;trace.push('FREE '+names[i]+' !');}else trace.push('FREE '+names[i]);live[i]=false;}}function draw(){cells('resources',live.map((v,i)=>({text:names[i]+' '+(v?'●':'×'),cls:v?'gold':'empty'})));cells('trace',Array.from({length:7},(_,i)=>({text:trace[i]??'—',cls:trace[i]?.includes('!')?'gold':trace[i]?'on':'empty'})));set('remaining',live.filter(Boolean).length);set('hazards',hazards);$('#remove').disabled=!success;set('explanation',t('note'));}$('#run').onclick=()=>{live=[false,false,false];hazards=0;trace=[];const fail=+$('#failAt').value;for(let i=0;i<3;i++){if(i===fail){trace.push('FAIL '+names[i]);break;}live[i]=true;trace.push('GET '+names[i]);}success=fail===3;if(!success)cleanup();draw();};$('#remove').onclick=()=>{cleanup();success=false;draw();};draw();","audio":false,"strings":{"buffer":"申请 buffer 之前","correct":"逆依赖清理","failAt":"失败发生在哪里","forward":"按申请顺序释放","hazards":"释放次序风险次数","heading":"失败落在不同位置,清理责任怎样变化?","interface":"注册 interface 之前","intro":"选择 probe 的失败位置和回滚策略,再运行一次模型。比较残留资源与释放顺序风险;成功路径还可以显式拆除。","irq":"申请 IRQ 之前","leak":"漏掉 buffer 释放","note":"试“全部成功”再拆除,或在 interface 之前失败。逆序清理、漏释放与错误顺序的结果不同,不能只验证正常 probe 返回成功。","policy":"回滚策略","remaining":"残留资源数量","remove":"拆除成功的设备","reset":"重置","resources":"仍有效的资源及依赖","run":"运行选定情景","scope":"玩具 probe:buffer → IRQ 使用 buffer → 对外 interface 依赖 IRQ。失败发生在选中步骤取得资源之前。错误策略仅在模型内演示,不触发真实内核故障。省略并发细节;“次序风险”表示依赖仍有效时释放了其资源。","scopeLabel":"模型范围与约定","success":"全部成功","trace":"本次取得与清理记录"}}
可交付的复现记录
记录准确的内核版本或提交、.config、启动参数、模块和 taint 状态,以及最小输入、实际行为、预期行为和原始日志。若修复仅在某个配置下验证,应明确该边界。
把这些证据连接到 构建与模块 的产物,才能确认测试运行的确实是目标代码,而不是另一套同名内核。