操作系统实验中的防御性编程

操作系统实验中的防御性编程

2024 南京大学《操作系统:设计与实现》
操作系统实验中的防御性编程

同学们面对的困境

理论很美好,现实很残酷

  • 我们的框架直接运行在虚拟机上
    • 根本没法带这些 sanitizers
    • 试图移植 = 失败
  • 我们根本不可能 “观测每一次共享内存访问”

《操作系统》实验有 deadline

  • 直接摆烂
  • 困难是摆烂的第一原因
2024 南京大学《操作系统:设计与实现》
操作系统实验中的防御性编程

相信你的编程能力!

Full Sanitizer 很难实现

  • 不如换一种思路
  • 我们可以 “编程”!

Best-effort is better than no-effort!

  • 不实现 “完整” 的检查 (允许存在误报/漏报)
  • 实现简单、非常有用——而且有惊喜
    • 我们不是一直都在写 assertions 吗?
      • Peterson 算法:assert(nest == 1);
      • 链表:assert(u->prev->next == u);
      • spinlock: if (holding(&lk)) panic();
2024 南京大学《操作系统:设计与实现》
操作系统实验中的防御性编程

防御性编程:Buffer Overrun 检查

Canary (金丝雀) 对一氧化碳非常敏感

  • 它们用生命预警矿井下的瓦斯泄露 (since 1911)

center

Canary: “牺牲” 内存单元,预警 memory error

  • (程序运行时没有动物受到实质的伤害)
2024 南京大学《操作系统:设计与实现》
操作系统实验中的防御性编程

Canary 的例子:Stack Guard

栈溢出:如果你遇到过,一定调了半天

#define MAGIC 0x55555555
#define BOTTOM (STK_SZ / sizeof(u32) - 1)
struct stack { char data[STK_SZ]; };

void canary_init(struct stack *s) {
    u32 *ptr = (u32 *)s;
    for (int i = 0; i < CANARY_SZ; i++)
        ptr[BOTTOM - i] = ptr[i] = MAGIC;
}

void canary_check(struct stack *s) {
    u32 *ptr = (u32 *)s;
    for (int i = 0; i < CANARY_SZ; i++) {
        panic_on(ptr[BOTTOM - i] != MAGIC, "underflow");
        panic_on(ptr[i] != MAGIC, "overflow");
    }
}
2024 南京大学《操作系统:设计与实现》
操作系统实验中的防御性编程

另一种 Canary

检测 “缓冲区溢出”

int foo() {
    // 一段连续内存;位于局部变量和返回地址之前
    u32 canary = SOME_VALUE;

    ... // 实际函数

    canary ^= SOME_VALUE; // 如果程序被攻击或出错
                          // canary 就不会归零了
    assert(canary == 0);
    return ret;
}
2024 南京大学《操作系统:设计与实现》
操作系统实验中的防御性编程

烫烫烫、屯屯屯和葺葺葺

MSVC 中 Debug Mode 的 guard/fence/canary

  • 未初始化栈: 0xcccccccc
  • 未初始化堆: 0xcdcdcdcd
  • 对象头尾: 0xfdfdfdfd
  • 已回收内存: 0xdddddddd
    • 手持两把锟斤拷,口中疾呼烫烫烫
    • 脚踏千朵屯屯屯,笑看万物锘锘锘
    • (它们一直在无形中保护你)
for i in [0xcc, 0xcd, 0xdd, 0xfd]:
    print((bytes([i]) * 80).decode('gbk'))
2024 南京大学《操作系统:设计与实现》
操作系统实验中的防御性编程
2024 南京大学《操作系统:设计与实现》
操作系统实验中的防御性编程

防御性编程:低配版 Lockdep

高配版 lockdep 太复杂?

  • 统计当前的 spin count
  • 如果超过某个明显不正常的数值 (100,000,000) 就报告
    • 你感觉到 “hang” 了
int spin_cnt = 0;
while (xchg(&lk, ❌) == ❌) {
    if (spin_cnt++ > SPIN_LIMIT) {
        panic("Spin limit exceeded @ %s:%d\n",
            __FILE__, __LINE__);
    }
}
  • 配合调试器和线程 backtrace 一秒诊断死锁
2024 南京大学《操作系统:设计与实现》
操作系统实验中的防御性编程

防御性编程:低配版 AddressSanitizer

L1 内存分配器的 specification

  • 已分配内存 S=[0,r0)[1,r1)S = [\ell_0, r_0) \cup [\ell_1, r_1) \cup \ldots
  • kalloc(ss) 返回的 [,r)[\ell, r) 必须满足 [,r)S=[\ell, r) \cap S = \varnothing
// allocation
for (int i = 0; (i + 1) * sizeof(u32) <= size; i++) {
    panic_on(((u32 *)ptr)[i] == MAGIC, "double-allocation");
    arr[i] = MAGIC;
}

// free
for (int i = 0; (i + 1) * sizeof(u32) <= alloc_size(ptr); i++) {
    panic_on(((u32 *)ptr)[i] == 0, "double-free");
    arr[i] = 0;
}
2024 南京大学《操作系统:设计与实现》
操作系统实验中的防御性编程

防御性编程:低配版 ThreadSanitizer

回顾:数据竞争的表现

  • 🏃 的结果会影响状态
  • 我们观测状态影响就可以了!
// Suppose x is lock-protected

...
int observe1 = x;
delay();
int observe2 = x;

assert(observe1 == observe2);
...
2024 南京大学《操作系统:设计与实现》
操作系统实验中的防御性编程

防御性编程:SemanticSanitizer

两个看似平常的检查

  • 检查整数是否在某个范围
#define CHECK_INT(x, cond) \
    ({ panic_on(!((x) cond), \
       "int check fail: " \
       #x " " #cond); \
    })
  • 检查指针是否位于堆区
#define CHECK_HEAP(ptr) \
    ({ panic_on(!IN_RANGE((ptr), heap)); })

应该怎么用

2024 南京大学《操作系统:设计与实现》
操作系统实验中的防御性编程

防御性编程:SemanticSanitizer (cont'd)

检查内部数据一致性

  • CHECK_INT(waitlist->count, >= 0);
  • CHECK_INT(pid, < MAX_PROCS);
  • CHECK_HEAP(ctx->rip); CHECK_HEAP(ctx->cr3);

代入 “变量语义”

  • CHECK_INT(count, >= 0);
  • CHECK_INT(count, <= 10000);
  • 预防多种错误,甚至部分承担了 AddressSanitizer 的功能
    • Overflow, use-after-free 都可能导致 memory corruption
2024 南京大学《操作系统:设计与实现》
操作系统实验中的防御性编程

这才是真正的 “编程”

短短几行代码,我们实现了

  • Stack guard
  • Lockdep (simple)
  • AddressSanitizer (simple)
  • ThreadSanitizer (simple)
  • SemanticSanitizer

它们是一个种子

  • 指向 “engineering” 里无限的空间
  • 也指引我们反思编程语言的机制设计
2024 南京大学《操作系统:设计与实现》