【操作系统百科】内核与用户态的边界
内核与用户态的 “边界”不是一条线,是一组规则、指令和数据结构构成的带。过了这条带,语义要变:
- 指针语义不同:用户指针可能指向无效地址,可能随时被 munmap
- 权限模型不同:用户 uid/gid/capabilities 决定能做什么
- 错误后果不同:用户态崩溃是进程问题,内核越界访问用户页是 oops / panic
- 并发模型不同:用户页可能被其它线程同时访问,内核必须考虑 TOCTOU
本文把硬件边界(SMAP / PAN 等)接着 A-04 往上讲,聚焦 Linux 内核代码怎么写才算安全地处理用户态输入。
一、“指针”在内核看的维度
内核代码里一个 C 指针可能来自:
- 内核堆(kmalloc / vmalloc)——总是可读写,只要活着
- 内核代码段/数据段——只读或读写
- per-CPU 数据——只在当前 CPU 上用
- 用户态指针——语义全然不同
后一类用 __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_user、get_user、strncpy_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 期间用户页可能经历:
- 被 munmap:另一线程 munmap 同一段地址
- 被 madvise(DONTNEED):页被丢弃
- COW 触发:父进程 fork 后,子进程写入时触发 copy-on-write
- 迁移:NUMA balancing 把页挪到别的节点
- swap out:换出到 swap
- THP split:透明大页拆分
内核访问用户页时必须假设页随时会变,不能依赖 “一次访问成功就永远成功”。
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 区分。
用于:
- RDMA:
ib_umem - O_DIRECT DMA
- io_uring fixed buffers
- vfio
- GPU driver pinning
正确用法:pin_user_pages(...) 后使用
unpin_user_pages(...) 成对释放。
4.3 LRU 与 pinned 页
pin 会让页不能被回收/迁移,这会带来内存压力。大量长时间 pin(如 RDMA 大 memory region)会把 anonymous LRU 打爆。治理手段:
- 资源限制(cgroup memory + locked memory limit)
RLIMIT_MEMLOCKCAP_IPC_LOCKcapability 控制
五、iov_iter 抽象
很多 syscall 接受
iovec(scatter/gather):readv、writev、preadv2、sendmsg。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 个小块:
CAP_NET_ADMIN:改路由、防火墙、网卡状态CAP_SYS_ADMIN:mount、umount、swap 等 “杂七杂八的 root 特权”(臭名昭著的”垃圾桶” cap)CAP_SYS_PTRACE:ptrace 任意进程CAP_IPC_LOCK:mlock / pin 内存CAP_NET_RAW:SOCK_RAWCAP_DAC_OVERRIDE:跨越 DAC(文件权限位)
能力集四种:
- Permitted:有哪些 cap 可被激活
- Effective:当前生效的 cap
- Inheritable:exec 后能传递
- Ambient:exec 后自动进入 effective(2016 引入,方便容器)
现实问题:大量工具仍按 “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 时决策:
SECCOMP_RET_ALLOW:通过SECCOMP_RET_ERRNO:返回指定 errnoSECCOMP_RET_KILL_PROCESS:杀进程SECCOMP_RET_TRAP:投 SIGSYSSECCOMP_RET_USER_NOTIF:把 syscall 转给监管进程处理(4.x 引入)SECCOMP_RET_TRACE:让 ptracer 处理
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 等安全模块在内核关键决策点插入检查:
- 文件打开
- 进程 exec
- 能力检查
- 网络发送
- IPC
- …
多个 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 必须检查:
- 所有用户指针都带
__user - 只用 copy_from_user / get_user / strncpy_from_user 读入
- 用
kmalloc/kvmalloc分配足够大小的内核缓冲再读 - 读取 size / offset / length 后立刻做上下界检查
- 检查整数溢出(
size_add_overflow) - 避免 TOCTOU:读入一次,别回头二次 deref
- 如果要 DMA:用
pin_user_pages,成对释放 - 对容量做 capability
检查(
capable()或ns_capable()) - 考虑
RLIMIT_*配额 - 错误路径释放所有已分配资源
违反任何一条都是潜在漏洞来源。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 问题:提交的指针到执行时可能已变。
教训:边界是一个持续演进的话题。每次引入新机制都可能拓宽攻击面。
十、总结
- 硬件边界(SMAP、PAN、KPTI)帮忙兜底,但不能替代代码中的正确性
- 用户指针生命周期短、会失效、会换走——永远 copy 进核内再用
- pin_user_pages 是长期引用的正确方式,和 page 回收/迁移协作
- capability、namespace、seccomp、LSM 层层提供软件约束,每层保护的是不同层面
- 任何 syscall / ioctl 的实现都要走十步 checklist
- 历史性漏洞(Dirty COW、DirtyPipe)都是 “看似小细节” 的边界 bug
掌握这条边界是写 Linux 内核代码、分析内核 CVE、做安全评审时的基础工作。下一篇 A-07 跳出 Linux 看 POSIX / BSD / Windows 的选择差异。
参考文献
- Linux Documentation/core-api/pin_user_pages.rst
- Linux Documentation/core-api/symbol-namespaces.rst
- Linux Documentation/userspace-api/seccomp_filter.rst、landlock.rst
- Linux Documentation/security/credentials.rst、LSM.rst
- Corbet, J. “User-space access and the speculation problem.” LWN.net 2018
- Corbet, J. “The pin_user_pages() long story.” LWN.net 2020
- Edge, J. “Landlock: unprivileged access control.” LWN.net 2021
- Stevens, W. R., Rago, S. A. Advanced Programming in the UNIX Environment, 3rd ed.
- Bovet, D., Cesati, M. Understanding the Linux Kernel, 3rd ed. Chapter 10 “System calls”
- Linux source:
mm/gup.c,lib/usercopy.c,kernel/capability.c,kernel/seccomp.c,security/
工具
sparse——make C=2,检查__user注解smatch—— 进一步语义检查- Syzkaller —— 模糊测试,自动生成 syscall 序列
coccinelle—— 语义 patch,常用于大规模 API 改造trace-cmd record -e syscalls—— 实际 syscall 追踪
上一篇:系统调用 ABI 下一篇:POSIX 与 Linux/BSD/Windows 的偏离
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【操作系统百科】seccomp-bpf 与 Landlock
进程自限如何实现最小权限?seccomp strict mode、filter mode(BPF 过滤器)、user notify、Landlock 文件访问控制、syscall user dispatch——本文讲 Linux 的系统调用过滤。
【操作系统百科】SELinux 与 AppArmor
SELinux 与 AppArmor 的模型差异如何影响运维?LSM hooks、类型强制策略、AppArmor profile、permissive/enforce 模式、容器标签——本文讲 Linux 强制访问控制。
【操作系统百科】xattr/ACL/capabilities
扩展属性(xattr)是 inode 的元数据扩展点——POSIX ACL、capability set、SELinux label 都存在 xattr 里。本文讲 xattr 四命名空间、POSIX ACL、文件 capability、备份工具与容器镜像的 xattr 问题。
【操作系统百科】内存回收
Linux 内存回收是 VM 最复杂的子系统之一。本文讲 active/inactive LRU、kswapd 与 direct reclaim、watermark 三线、swappiness 的真实含义、MGLRU 改造、memcg 回收与 PSI。