导引:点亮什么
053 把「调度迁移写花 ctx」这个最显眼的雷扫了,可 SMP 上会写花状态的地方远不止 runqueue。
ext2把读过的 inode 缓在一张跨核共享的表里,入口函数get_cached_inode全程裸奔——两核同时 miss 同一个 ino,一头在evict把某 slot 删了,另一头手里还攥着指向那个 slot 的Inode*在用,slot 一被释放或被重填成另一个文件,指针就成了别名 UAF,读到的字节不属于你那个文件。这种错连 ASAN 都看不见(对象还活着,只是内容被换了),是比 panic 难抓十倍的静默别名。这一章咱们要做的事,不是「把这一处修了就收工」,而是把 SMP 上「抓竞态」这件事拧成一条红线——它贯穿四个阶段:一是先造一台跨核交错报警器(
race-detect),盯着「这块共享状态压根没锁」这类设计缺陷;二是拿ext2 inode_cache当靶子,看报警器真能抓;三是给病灶上真锁、把报警器换成lockdep_assert_held当回归护栏,顺手清三笔旧债;四是在纵深阶段推导出「同步 TLB shootdown 跑在缺页中断的 IF=0 里会确定性互锁」——正是这个结论逼出了 deferred CoW 设计范式,把跨核 free 从会互锁的中断上下文挪到可阻塞的 drain 内核线程,把 044 章那两本账(pte_count/refcount)的no_free变体兑现掉。诚实边界先摆前面:三个
ext2盘元数据的 race(block_buf_共享 buffer 互踩、位图分配 RMW 无锁、与之绑定的样式整理)依赖KmBufRAII 和ext2_dirops拆分这块地基,划给 080/081;deferred CoW 的基建与接线(handle_cow_fault走_no_free+enqueue_pending_shootdown、drain 线程已起、CINUX_TLB_DRAINoption 默认 ON、0xE1 shootdown IPI 已注册进 IDT)在本机工作树均已落地。WSL2 上-smp 2靠 KVM 能跑,但 host 的 SMAP/CPUID 透传限制对 race-detect 本身无影响(race-detect 不依赖 SMAP),真机上的 heisenbug 会单独说。
这章咱们要点亮什么
- race-detect 这台报警器:它盯的不是时间窗口或数据撕裂,而是「某个共享可变状态本该被自旋锁串行化、实际却没加锁」这一类具体缺陷;机制极简——一个
RaceWatchpoint只记「上一个碰它的 CPU 是谁」,跨 CPU 交错就报警。 - opt-in 门控是一条链:option 开了 ≠ 编译宏传了 ≠ 机制测试真跑了 ≠ 测试真 PASS。断任何一环,日志照样绿、哑火不报错。这条链在本机工作树三环已通(
cmake/options.cmake声明 option、kernel/CMakeLists.txtforeach 自动传编译宏、§14 文件门按 option 链真实现),lab 会带你亲手开 option 并验证 PASS 亮起。 - 同一缓存的两层加固:上一轮结构加固先把 inode cache 从「固定值数组」改成「堆分配 + 分离链 + refcount」治结构性别名 UAF(地址即身份,refcount>0 绝不移动);这一章再叠一把
inode_cache_lock_治并发(两个核同时改这张表)。一个治「谁能在何时被释放」,一个治「谁能同时进来改」,缺一不可。 - deferred CoW 的死锁推导:为什么同步 shootdown 挂在 CoW fault 里会互锁——
Spinlock::guard不动 IF,等锁时 IF 保持 0,hole 成立;破局不是「加锁」而是「换执行上下文」,把 shootdown 从 IF=0 的 ISR 搬到 IF=1 的 kthread。