内核模块跑起来之后,发现 SMP 下偶发崩溃。追查下去,发现是一类隐藏很深的 bug:static 局部缓冲区。
static 局部变量在 SMP 下是定时炸弹
// ext2.c 里随处可见这样的代码
int ext2_read(uint32_t inum, ...) {
static uint8_t blk[4096]; // 危险!
// ...
}
static 局部变量存在 .bss 段里,整个内核只有一份。单核下没问题,但 SMP 下:
- CPU0 和 CPU1 同时读不同的文件
- 都调用
ext2_read,都在用同一个blk[] - 互相覆盖对方的数据 → 文件读出来是乱的
ext2.c 里有十几处这样的 static 缓冲区,vfs.c 里也有。全部改成 kmalloc:
// 修改后
uint8_t *blk = (uint8_t *)kmalloc(block_size);
if (!blk) return -1;
// ... 使用 ...
kfree(blk);
同时还有一个更隐蔽的问题:间接块的 LBA 缓存:
// 修改前:有状态缓存,SMP 下竞争
static uint32_t ind1[1024];
static uint32_t ind1_bno = 0; // 上次读的 LBA,用来判断是否要重读
if (ind1_bno != inode.i_block[12]) {
ind1_bno = inode.i_block[12];
read_block(ind1_bno, ind1);
}
两个核心同时修改 ind1_bno,都以为自己读的数据有效——去掉缓存,每次都读:
uint32_t *ind1 = (uint32_t *)kmalloc(block_size);
read_block(inode.i_block[12], ind1);
kfree(ind1);
mmap_next:全局变量变 per-process
同样的问题出现在 mmap_next——匿名 mmap 的地址分配器:
// 修改前:全局,所有进程共享
static uint64_t mmap_next = MMAP_ANON_BASE;
两个进程同时调 mmap(NULL, ...),会拿到同一个地址,互相覆盖。
移入 process_t:
// 修改后:每进程独立
current->mmap_next = MMAP_ANON_BASE; // 懒初始化
uint64_t base = addr ? addr : current->mmap_next;
if (!addr) current->mmap_next = base + pages * 4096;
kfree 的双重释放检测
顺手修了 kfree 的两个问题:
1. 指针越界:如果传入的指针不在堆范围内(比如 NULL、栈指针),静默忽略而不是崩溃。
2. 向前合并时的 UAF:
// 修改前(有 bug)
blk->prev->size += BLOCK_HDR_SIZE + blk->size;
blk->prev->next = blk->next;
if (blk->next) blk->next->prev = blk->prev; // 此时 blk->prev 已被修改?
// 修改后:提前保存
block_t *prev = blk->prev;
prev->size += BLOCK_HDR_SIZE + blk->size;
prev->next = blk->next;
if (blk->next) blk->next->prev = prev;
小结
这章的修改看起来琐碎,但每一个都是真实的 bug。static 局部变量是单核内核里的常见写法,移植到 SMP 后需要全面清查。好在改法很统一:一律换成 kmalloc/kfree,栈上小缓冲区能用就用,4KB 以上的必须堆分配。