土法炼钢兴趣小组的算法知识备份

【操作系统百科】内核与用户态的边界:copy_from_user、pin、seccomp、capability

文章导航

分类入口
os
标签入口
#boundary#copy-from-user#get-user-pages#pin-user-pages#user-pointer#capability#seccomp#landlock#lsm

目录

【操作系统百科】内核与用户态的边界

内核与用户态的 “边界”不是一条线,是一组规则、指令和数据结构构成的带。过了这条带,语义要变:

本文把硬件边界(SMAP / PAN 等)接着 A-04 往上讲,聚焦 Linux 内核代码怎么写才算安全地处理用户态输入。

一、“指针”在内核看的维度

内核代码里一个 C 指针可能来自:

  1. 内核堆(kmalloc / vmalloc)——总是可读写,只要活着
  2. 内核代码段/数据段——只读或读写
  3. per-CPU 数据——只在当前 CPU 上用
  4. 用户态指针——语义全然不同

后一类用 __user 注解:

ssize_t sys_read(unsigned int fd, char __user *buf, size_t count);

__user 是个 sparse 注解(define __user __attribute__((noderef, address_space(__user)))),空间属性会让 sparse / smatch 报告非法 dereference。

规则:内核代码不能直接 *buf 读用户指针,必须经过 copy_from_userget_userstrncpy_from_user 等一组 helper。违反规则的代码在 SMAP 机器上会触发 GP fault(硬件帮忙抓);在无 SMAP 机器上会悄悄成功但被 sparse/smatch 抓。

二、access_ok + copy_from_user

2.1 access_ok

access_ok(addr, size) 检查 [addr, addr + size) 是否整段落在用户地址空间(低于 TASK_SIZE)。不检查有没有实际映射页面。

这个检查以前是逻辑独立的;现代内核(5.x+)把 access_ok 合并进 copy_from_user 家族。手动 access_ok 主要剩在 ioctl 框架等少数位置。

2.2 copy_from_user / copy_to_user

unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);

返回值是未能复制的字节数(成功是 0,部分失败返回剩余)。典型用法:

if (copy_from_user(&req, argp, sizeof(req)))
    return -EFAULT;

实现上,SMAP 机器会先 stac(允许 ring 0 访问用户页),复制完 clac。内部还有 exception table 处理页错误:如果用户页缺失,CPU 触发 #PF,处理程序查 exception table,跳到 fixup 代码返回 -EFAULT。这让内核能优雅处理 “用户传了野指针” 的情况而不 oops。

2.3 get_user / put_user

单值版本:

int val;
if (get_user(val, (int __user *)argp))
    return -EFAULT;

比 copy_from_user 快(一条指令),但只能处理原生宽度。

2.4 strncpy_from_user、strnlen_user

字符串处理要考虑 NUL 终止符和最大长度:

char path[PATH_MAX];
long len = strncpy_from_user(path, userpath, PATH_MAX);
if (len < 0) return len;      // EFAULT or too long
if (len == PATH_MAX) return -ENAMETOOLONG;

三、用户页生命周期:mmap、munmap、COW

copy_from_user 期间用户页可能经历:

内核访问用户页时必须假设页随时会变,不能依赖 “一次访问成功就永远成功”。

3.1 TOCTOU 陷阱

Time-Of-Check Time-Of-Use:内核 check 了一次后在 use 时不应再 deref 原指针。典型错误:

// 错误
if (user_ptr->field == SOMETHING)
    do_thing(user_ptr->field);   // 第二次读可能不一样!

另一线程可以在两次 dereference 之间改动 field。正确:先 copy_from_user 到内核局部变量,然后基于局部变量决策。

四、固定用户页:GUP 与 pin

有些场景(DMA、长时间占用)不能容忍页被换走。用 get_user_pages 家族:

long get_user_pages(unsigned long start, unsigned long nr_pages,
                    unsigned int gup_flags, struct page **pages);

返回一组 struct page *,这些页被 “pin”——refcount 增加。DMA 完成后必须 put_page() 释放。

4.1 GUP 的历史包袱

传统 get_user_pages 只加 refcount,不额外标记。对某些 workflow(例如 long-term DMA、RDMA)这会让内核的 page migration / swap 做出错误决定:它以为可以移动,但其实有 DMA 正在进行。

4.2 pin_user_pages(2019+)

Linux 5.x 引入 pin_user_pages——明确声明 “这些页被外部引用,不要移动/换出”。内部用 FOLL_PIN flag 实现,refcount 增加 GUP_PIN_COUNTING_BIAS(1024)以便与普通 refcount 区分。

用于:

正确用法:pin_user_pages(...) 后使用 unpin_user_pages(...) 成对释放。

4.3 LRU 与 pinned 页

pin 会让页不能被回收/迁移,这会带来内存压力。大量长时间 pin(如 RDMA 大 memory region)会把 anonymous LRU 打爆。治理手段:

五、iov_iter 抽象

很多 syscall 接受 iovec(scatter/gather):readvwritevpreadv2sendmsg。Linux 5.x 引入 iov_iter 统一抽象用户侧缓冲区:

struct iov_iter {
    u8 iter_type;           // UBUF / IOVEC / BVEC / KVEC / XARRAY / DISCARD
    ...
    size_t count;           // 剩余字节
    union {
        const struct iovec *iov;
        const struct bio_vec *bvec;
        ...
    };
};

内核文件系统的 read/write 路径统一拿 iov_iter,既能处理用户 iovec,也能处理 splice 从 pipe 来的 bio_vec,或 io_uring 固定缓冲区的 xarray。这种统一降低了复杂度,也让新 I/O 路径(io_uring、ebpf read/write)容易接入。

六、软件层的边界:capability、namespace、seccomp

硬件的隔离是”能否访问内存/指令”;软件层的隔离是”能否做动作”。Linux 的两大流派:

6.1 capabilities(CAP_*)

POSIX capabilities 把 root 特权切成 ~40 个小块:

能力集四种:

现实问题:大量工具仍按 “uid == 0” 判断,不识别单独 cap;CAP_SYS_ADMIN 被滥用到几乎 == root。L-95 会深入。

6.2 namespace

namespace 把某一全局资源视图私有化:pid、mnt、net、user、uts、ipc、cgroup、time(8 种)。容器就是 namespace + cgroup 的组合。B-17 专题。

6.3 seccomp-bpf

seccomp(secure computing mode)允许进程自己限制自己能调的 syscall。seccomp-bpf 扩展允许加载 BPF 程序在每次 syscall 时决策:

sandbox、浏览器(Chrome/Firefox 都用)、容器 runtime 广泛使用。性能开销视过滤器复杂度,微基准 50–300 ns per syscall。

6.4 Landlock

2021 年引入的非特权沙箱:进程可以自己限制自己对文件系统路径的访问(不需要 root),LSM 钩子级别执行,替代传统需要 CAP_SYS_ADMIN 的 chroot/mount namespace 方案。

6.5 LSM 框架

Linux Security Modules 提供一组 hook,允许 SELinux、AppArmor、SMACK、TOMOYO、Yama、Lockdown 等安全模块在内核关键决策点插入检查:

多个 LSM 可堆叠(stacking LSM,5.x+)。L-97 专题。

七、“这个接口如何进来”:一张地图

把上面所有机制画到一条 syscall 路径上:

flowchart TB
    U[用户 syscall 指令]
    U --> H1[切特权级 / SWAPGS / CR3]
    H1 --> E[内核 entry]
    E --> S1{seccomp-bpf}
    S1 -->|allow| P[ptrace 通知]
    S1 -->|deny/errno/kill| RET[直接返回]
    P --> AU[audit 入口]
    AU --> L1[LSM 预钩子<br/>security_*_check]
    L1 --> D[分发 sys_foo]
    D --> C1[capable / ns_capable 检查]
    C1 --> C2[copy_from_user 拉入核内]
    C2 --> BL[业务逻辑<br/>VFS / net / mm]
    BL --> C3[copy_to_user 写回]
    C3 --> L2[LSM 后钩子 + audit 出口]
    L2 --> X[signal / resched 检查]
    X --> H2[CR3 还原 / SWAPGS / sysret]
    H2 --> U
    classDef sec fill:#f0883e,color:#2d333b,stroke:#f0883e;
    classDef hw fill:#388bfd,color:#cdd9e5,stroke:#388bfd;
    class S1,L1,C1,L2 sec
    class H1,H2 hw

蓝色是硬件切换层;橙色是软件安全层。每一站都有自己的性能开销与错误处理——Linux 内核源码里某个看起来简单的 syscall 其实走过十几个 checkpoint。做性能优化时要盯橙色(可以被规避/合并),做安全加固时要补橙色(多加一层就是多一层防线)。

八、内核写用户态代码时的 checklist

开发内核模块/驱动/syscall 必须检查:

  1. 所有用户指针都带 __user
  2. 只用 copy_from_user / get_user / strncpy_from_user 读入
  3. kmalloc / kvmalloc 分配足够大小的内核缓冲再读
  4. 读取 size / offset / length 后立刻做上下界检查
  5. 检查整数溢出(size_add_overflow
  6. 避免 TOCTOU:读入一次,别回头二次 deref
  7. 如果要 DMA:用 pin_user_pages,成对释放
  8. 对容量做 capability 检查capable()ns_capable()
  9. 考虑 RLIMIT_* 配额
  10. 错误路径释放所有已分配资源

违反任何一条都是潜在漏洞来源。KCSAN、KASAN、sparse、smatch、Syzkaller 都是为了自动化这些检查。

九、生产故障案例小集

9.1 CVE-2016-5195 “Dirty COW”

mm 子系统竞态:get_user_pages + COW 中的 race 允许普通用户写到只读文件映射的页。利用简单,影响几乎所有 Linux 版本,2016 年轰动。

9.2 CVE-2022-0847 “DirtyPipe”

splice 到 pipe 时 PIPE_BUF_FLAG_CAN_MERGE 未清除,让普通用户写入只读文件。修复只改了几行。

9.3 io_uring 相关

io_uring 重新定义了”syscall 边界”——命令在共享环里,不是每次 syscall 进入。这带来新的 TOCTOU 问题:提交的指针到执行时可能已变。

教训:边界是一个持续演进的话题。每次引入新机制都可能拓宽攻击面。

十、总结

掌握这条边界是写 Linux 内核代码、分析内核 CVE、做安全评审时的基础工作。下一篇 A-07 跳出 Linux 看 POSIX / BSD / Windows 的选择差异。


参考文献

工具


上一篇系统调用 ABI 下一篇POSIX 与 Linux/BSD/Windows 的偏离

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-06-26 · os

【操作系统百科】seccomp-bpf 与 Landlock

进程自限如何实现最小权限?seccomp strict mode、filter mode(BPF 过滤器)、user notify、Landlock 文件访问控制、syscall user dispatch——本文讲 Linux 的系统调用过滤。

2026-06-27 · os

【操作系统百科】SELinux 与 AppArmor

SELinux 与 AppArmor 的模型差异如何影响运维?LSM hooks、类型强制策略、AppArmor profile、permissive/enforce 模式、容器标签——本文讲 Linux 强制访问控制。

2026-05-12 · os

【操作系统百科】xattr/ACL/capabilities

扩展属性(xattr)是 inode 的元数据扩展点——POSIX ACL、capability set、SELinux label 都存在 xattr 里。本文讲 xattr 四命名空间、POSIX ACL、文件 capability、备份工具与容器镜像的 xattr 问题。

2026-04-27 · os

【操作系统百科】内存回收

Linux 内存回收是 VM 最复杂的子系统之一。本文讲 active/inactive LRU、kswapd 与 direct reclaim、watermark 三线、swappiness 的真实含义、MGLRU 改造、memcg 回收与 PSI。


By .