前言:从“全能root”到“最小权限”的安全范式跃迁

        在前面的章节中,我们让OS能够运行标准C程序、支持动态链接和网络通信。但此时的系统仍遵循着危险的“全有或全无”权限模型:进程要么是拥有无限权力的root,要么是一无所有的普通用户。一个没有细粒度安全隔离的操作系统,就像一座没有防火墙和门禁的大厦——任何一个小漏洞都可能导致整个系统的沦陷。

        现代系统安全不是“补丁集合”,而是“默认不信任的架构原则”。它通过三重正交机制将特权拆解为可组合的最小单元:Capability将root权限分解为40+个独立能力位;Namespace为每个进程提供独立的资源视图(PID/挂载/网络等);seccomp-bpf则在内核入口处以可编程方式过滤系统调用。本章将实现这三者的最小可用版本,让你的OS首次支持容器化隔离运行环境,为云原生安全奠定基石。

本章里程碑:

  • ✅ Capability模型:cap_t结构、capget/capset系统调用、execve权限继承规则
  • ✅ PID Namespace:独立PID编号空间、/proc虚拟化、reaper进程语义
  • ✅ Mount Namespace:独立挂载树、bind mount、chroot增强
  • ✅ seccomp-bpf框架:BPF解释器、SECCOMP_SET_MODE_FILTER、syscall白名单
  • ✅ unshare/clone标志:CLONE_NEWPID/CLONE_NEWNS/CLONE_NEWNET集成
  • ✅ 验证:容器内ps仅见自身进程、mount隔离生效、非法syscall被EPERM拒绝

核心概念:安全不是“访问控制列表”,而是“执行上下文的结构性约束”
为什么Capability优于传统UID检查?

        因为root是一个过度聚合的危险抽象。传统Unix中,绑定端口、修改时钟、加载模块、追踪进程全部依赖uid==0判断。这意味着一个仅需绑定端口的服务必须以完整root身份运行,一旦其代码存在漏洞,攻击者即获得全部特权。Capability将这一单体权限拆分为CAP_NET_BIND_SERVICE、CAP_SYS_TIME、CAP_SYS_MODULE、CAP_SYS_PTRACE等独立位,使进程仅持有完成任务所需的最小能力集。

⚠️ 关键洞察:Capability是“特权的类型系统”。就像强类型语言防止整数当指针用一样,Capability防止网络服务意外获得内核模块加载权。若你的实现仍在内核各处写if (!uid) return -EPERM,你就永远无法实现真正的最小权限。教学实现中必须将所有uid==0检查替换为ns_capable(ns, CAP_XXX),并在task_struct中维护effective/permitted/inheritable三组cap_t。这个替换过程本身就是安全重构的最佳实践。

Namespace的本质:全局资源的局部投影

        Namespace不是“复制资源”,而是“改变资源的寻址方式”。PID Namespace并不创建新的进程调度实体,而是为同一组进程提供独立的编号映射;Mount Namespace不复制文件系统,而是维护独立的vfsmount树根节点。这种“视图隔离”而非“实体隔离”的设计,使隔离开销趋近于零,同时保持了资源共享的效率。

        关键约束:①Namespace必须可嵌套(子ns是父ns的子集);②跨ns操作必须显式授权(如ptrace需同ns或CAP_SYS_PTRACE);③ns生命周期绑定到最后一个引用它的进程或文件描述符。教学实现先聚焦PID和Mount两种最直观的ns,验证隔离语义正确性后再扩展Network/User ns。

seccomp-bpf:内核入口处的可编程防火墙

        传统的syscall过滤基于静态黑名单,无法表达“允许open但禁止O_WRONLY”这类上下文敏感策略。seccomp-bpf将BPF虚拟机嵌入内核syscall入口,允许用户态提交一段安全的字节码程序,对每次syscall的参数进行运行时决策(ALLOW/DENY/LOG/TRAP)。这使得安全策略可以精确到参数级别,且无需修改内核代码。

        关键约束:①BPF程序必须有界终止(无循环、指令数上限);②只能访问syscall_nr和参数,不能读写内核内存;③DENY动作必须返回明确的errno而非SIGKILL(便于调试)。教学实现先支持SECCOMP_RET_ALLOW/ERRNO两种动作和基本的ALU/JMP指令,验证过滤逻辑后再扩展TRACE/KILL。


实战代码
Capability检查与继承
// kernel/security/capability.c
#include <linux/capability.h>

// ★ 替代所有uid==0检查的核心函数
bool ns_capable(struct user_namespace *ns, int cap) {
    // 检查当前进程在目标ns中是否持有该capability
    return has_ns_capability(current, ns, cap);
}

static bool has_ns_capability(struct task_struct *t, 
                              struct user_namespace *ns, int cap) {
    // effective cap set包含该位 → 允许
    if (cap_raised(t->cap_effective, cap))
        return true;
    
    // 否则拒绝(不再fallback到uid检查!)
    return false;
}

// ★ execve时的capability继承规则
void compute_new_caps(struct linux_binprm *bprm) {
    const struct cred *old = current_cred();
    struct cred *new = bprm->cred;
    
    // Permitted = (Inheritable & File_Inheritable) | (File_Permitted & Bounding_Set)
    cap_intersect(new->cap_inheritable, bprm->file_caps.inheritable, 
                  &new->cap_permitted);
    cap_union(&new->cap_permitted, &bprm->file_caps.permitted);
    cap_intersect(&new->cap_permitted, &current->cap_bset, &new->cap_permitted);
    
    // Effective = Permitted (若文件设置了SECBIT_NOROOT则清空)
    if (!(bprm->secureexec))
        new->cap_effective = new->cap_permitted;
    else
        cap_clear(new->cap_effective);
}
PID Namespace实现
// kernel/pid_namespace.c

struct pid_namespace {
    unsigned int level;           // 嵌套深度
    struct pid_namespace *parent;
    struct idr pid_idr;           // ★ 本ns内的PID分配器
    struct task_struct *child_reaper; // ns内init进程
    struct kref kref;
};

// ★ fork时根据CLONE_NEWPID创建新ns
struct pid_namespace *create_pid_namespace(struct pid_namespace *parent) {
    struct pid_namespace *ns = kzalloc(sizeof(*ns), GFP_KERNEL);
    ns->level = parent->level + 1;
    ns->parent = get_pid_ns(parent);
    idr_init(&ns->pid_idr);
    kref_init(&ns->kref);
    return ns;
}

// ★ 在指定ns中分配PID
pid_t alloc_pid_number(struct pid_namespace *ns) {
    int nr;
    // ★ 从本ns的idr分配,而非全局PID
    if (idr_alloc(&ns->pid_idr, NULL, 1, PID_MAX_LIMIT, GFP_ATOMIC, &nr))
        return -EAGAIN;
    return nr;
}

// ★ /proc/<pid>/status中显示ns内PID
int proc_pid_status(struct seq_file *m, struct pid_namespace *ns,
                    struct task_struct *task) {
    // ★ 将全局PID转换为当前ns视角的PID
    pid_t nspid = task_pid_nr_ns(task, ns);
    seq_printf(m, "Pid:\t%d\n", nspid);
    seq_printf(m, "NSpid:\t%d\n", nspid); // 显示ns层级链
    return 0;
}
seccomp-bpf过滤器安装与执行
// kernel/seccomp.c

// ★ 安装BPF过滤器
long seccomp_set_mode_filter(unsigned int flags, 
                             const char __user *uargs) {
    struct sock_fprog fprog;
    if (copy_from_user(&fprog, uargs, sizeof(fprog)))
        return -EFAULT;
    
    // ★ 验证BPF程序安全性
    struct bpf_prog *prog = bpf_prog_alloc(fprog.len);
    if (IS_ERR(prog)) return PTR_ERR(prog);
    
    if (bpf_check_classic(prog->insns, prog->len)) { // ★ 静态验证
        bpf_prog_free(prog);
        return -EINVAL;
    }
    
    // ★ 附加到当前进程
    current->seccomp.mode = SECCOMP_MODE_FILTER;
    current->seccomp.filter = prog;
    return 0;
}

// ★ syscall入口处调用
int __secure_computing(void) {
    if (current->seccomp.mode != SECCOMP_MODE_FILTER)
        return 0;
    
    struct seccomp_data sd = {
        .nr = syscall_get_nr(current, task_pt_regs(current)),
        .arch = AUDIT_ARCH_X86_64,
    };
    syscall_get_arguments(current, task_pt_regs(current), sd.args);
    
    // ★ 执行BPF程序
    u32 ret = BPF_PROG_RUN(current->seccomp.filter, &sd);
    
    switch (ret & SECCOMP_RET_ACTION_MASK) {
    case SECCOMP_RET_ALLOW: return 0;
    case SECCOMP_RET_ERRNO: return -(ret & SECCOMP_RET_DATA);
    default: do_exit(SIGSYS);
    }
}

关键细节解析

1. 为什么Capability检查必须绑定User Namespace?

        因为Capability的意义依赖于所在的User Namespace。在父ns中拥有CAP_SYS_ADMIN不代表在子ns中也拥有——反之,子ns中的root可能在父ns中毫无特权。若Capability检查脱离ns上下文,就无法实现真正的层级隔离。教学实现中ns_capable()的第一个参数必须是目标ns,而非隐式使用current_user_ns()。在mount/pid等操作中,目标ns通常是操作对象所属的ns,而非调用者所在ns。这个区分是容器安全模型的数学基础。

2. 为什么PID Namespace的child_reaper必须是ns内第一个进程?

        因为孤儿进程的回收责任必须限定在ns边界内。若ns内的init进程退出后,其子进程被父ns的init回收,就会泄露跨ns的进程引用,破坏隔离性。Linux规定:当ns的child_reaper退出时,该ns内所有剩余进程收到SIGKILL并被立即清理。教学实现中必须在do_exit()中检查current是否为某ns的child_reaper,若是则触发zap_pid_ns_processes()。这个“ns自毁”语义是防止僵尸ns泄漏的关键安全阀。

3. 为什么seccomp BPF验证必须在安装时而非运行时?

        因为BPF程序的安全性是不可运行时验证的静态属性。若允许安装后再检查,恶意程序可在检查窗口期执行危险操作。更重要的是,BPF验证本身可能耗时较长(O(n²)复杂度),放在syscall热路径上会成为DoS向量。教学实现中bpf_check_classic()必须在seccomp_set_mode_filter内同步完成,且验证失败时绝不安装程序。这个“验证前置”原则是所有内核可扩展性的安全基石。


调试Checklist:系统安全排查
症状可能原因排查方法
容器内看到宿主进程PID ns未正确传递/proc未重新挂载/task_pid_nr_ns回退全局cat /proc/self/status NSpid字段;验证unshare(CLONE_NEWPID)返回值;dump task->nsproxy->pid_ns_for_children
capset失败EPERMpermitted set不包含目标cap/bounding set限制/user ns无权capsh --print验证三组cap;测试CAP_SETFCAP是否持有;检查/proc/sys/kernel/cap_last_cap
seccomp过滤无效BPF程序未安装/syscall nr获取错/arch字段不匹配prctl(PR_GET_SECCOMP)确认模式;strace验证syscall nr;测试简单ALLOW ALL程序排除安装问题
mount隔离泄露CLONE_NEWNS未设置/shared subtree传播/bind mount源错误findmnt -o TARGET,PROPAGATION验证传播类型;测试mount --make-private;dump mnt_namespace根节点
容器内su root失败user ns未映射/CAP_SETUID不在effective/setgroups被禁cat /proc/self/uid_map验证映射;grep CapEff /proc/self/status;测试echo deny > /proc/self/setgroups
BPF程序被拒指令数超限/非法操作数/缺少ALLOW返回/除零风险bpf_verify_program单独测试;启用CONFIG_BPF_SYSCALL_DEBUG;简化程序逐步添加指令定位拒绝点

🔧 黄金法则:安全子系统调试的终极武器是权限状态快照器+隔离边界可视化器。实现debug_dump_security_context(task)显示三组cap、所属各层ns ID、seccomp模式和过滤器哈希,同时实现debug_trace_security_check(type, target, result)记录每次ns_capable/pid_ns_access/seccomp_eval的决策依据。安全bug几乎总是“隐含假设与显式约束的不一致”:你以为检查了cap但传错了ns、你以为创建了ns但forget了reaper、你以为BPF允许了但arch字段填了32位值。只有将抽象的安全策略与具体的运行时状态叠加验证,才能发现这些隐藏在信任边界中的致命裂缝。不要在安全路径上加条件编译的debug printk——你看到的只是单次检查的投影,真正的正确性存在于cap/ns/seccomp三维状态空间的全局一致性中。


本章小结与下一步

        今天我们让操作系统拥有了现代安全架构的三大支柱:

  • ✅ Capability模型将root拆解为40+个可组合的最小权限位
  • ✅ PID/Mount Namespace提供零开销的资源视图隔离
  • ✅ seccomp-bpf实现内核入口处的可编程syscall过滤
  • ✅ unshare/clone标志无缝集成现有进程创建路径
  • ✅ 验证:容器隔离、权限最小化、syscall沙箱全部工作

        从此,你的操作系统拥有了真正的安全隔离能力。当你第一次看到容器内ps仅显示自身进程、非特权用户安全地绑定低端口、恶意程序的非法syscall被静默拒绝时,你见证的是OS从“功能平台”到“可信计算基”的信任跃升。

下一章预告:《性能剖析与调优:perf子系统、调度统计与瓶颈定位方法论》

        当前的系统能跑但不知为何慢。下一章将实现硬件PMU驱动、perf_event_open接口、调度延迟追踪、缓存命中率采样,让你的OS具备生产级性能诊断能力,从“能用”迈向“好用”。


参考资料
  • Linux Kernel: kernel/capability.c, kernel/pid_namespace.c, kernel/seccomp.c, Documentation/admin-guide/namespaces/
  • Michael Kerrisk, The Linux Programming Interface Ch.39-40 (Capabilities & Namespaces)
  • Will Drewry, "seccomp and seccomp-bpf" (Google Security Blog)
  • Serge Hallyn, "Linux Capabilities: Making Them Work" (LWN.net)
  • 本系列完整代码:[你的GitHub仓库链接](Commit: s1e2c3a)

📝 作者注:这是《从零手写操作系统》系列的第51篇。系统安全是整个教程中思维范式转变最大、规范细节最繁琐、错误后果最严重的章节。它要求你从“如何让功能工作”转向“如何证明功能不被滥用”。强烈建议先用单元测试验证cap_t位运算正确性,再集成到execve路径;先实现单级PID ns,再添加嵌套;先写最简单的BPF ALLOW程序,再逐步增加过滤规则。把“Capability数据结构”、“cap检查替换”、“PID ns”、“Mount ns”、“seccomp BPF”分成五个独立里程碑,并用安全状态快照+决策追踪双重验证每一步的信任边界完整性,是避免在安全迷宫中迷失的关键纪律。 下一章,我们让系统从“默默运行”走向“透明可观测”!

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐