Linux Kernel Landlock LSM 深度实战:构建无特权安全沙箱
当我们在讨论 Linux 安全容器化时,脑海中浮现的通常是 Docker、Kubernetes 或 systemd-nspawn。这些方案有一个共同的前提:需要 root 权限或高权能(capabilities)来创建隔离环境。但如果你是一个普通用户,想要限制某个不可信程序对文件系统的访问能力,该怎么办?Landlock LSM 正是为此而生——它让无特权进程能够自我施加安全策略,在无需 root 的情况下构建精简的沙箱。
为什么需要 Landlock
传统 Linux 安全模块(LSM)如 AppArmor、SELinux 虽然强大,但存在一个根本限制:必须由特权用户或进程来配置安全策略。这意味着一个普通用户进程想要限制自身行为时,无法直接利用这些框架。
seccomp 解决了部分问题——它能限制系统调用,但粒度粗糙且无法感知语义(比如无法区分"读取 /etc/passwd"和"读取用户配置文件")。seccomp-bpf 虽然可以做过滤,但编写 syscall 白名单极其繁琐,且不同架构下 syscall 编号不同,可移植性差。
Landlock 的设计哲学与 seccomp 互补:seccomp 限制"能不能调用某个 syscall",Landlock 限制"能不能访问某个路径"。这让安全策略的表达更加自然,也更易于维护。
Landlock 自 Linux 5.13(2021年)合入主线以来,已被 Flatpak、ChromeOS 等实际产品采用。截至 2026 年,Landlock 已经发展到 ABI v5,支持文件系统控制、网络访问控制(预览)、以及细粒度的权限管理。
Landlock 架构全景
Landock 的架构分为三个层次:
┌──────────────────────────────────────────────┐
│ 用户态程序 │
│ landlock_create_ruleset() │
│ landlock_add_rule() │
│ landlock_restrict_self() │
└──────────────────┬───────────────────────────┘
│ syscall
┌──────────────────▼───────────────────────────┐
│ 内核态 LSM 钩子 │
│ bprm_committing_creds ← 策略激活点 │
│ file_open ← 文件访问检查 │
│ path_mkdir ← 路径操作检查 │
│ mmap_file ← 内存映射检查 │
│ │
│ 决策流程: │
│ 1. 检查进程是否有 Landlock 规则集 │
│ 2. 遍历所有已激活的 ruleset 层 │
│ 3. 任一层次拒绝 → 整体拒绝 │
│ 4. 所有层次都无规则 → 允许 │
└──────────────────────────────────────────────┘
核心数据结构
在内核中,Landlock 使用以下关键数据结构:
// include/uapi/linux/landlock.h
struct landlock_ruleset_attr {
__u64 handled_access; // 规则集能处理的访问权限集合
};
// 内核内部: 规则集层次结构
struct landlock_ruleset {
struct landlock_hierarchy *hierarchy;
struct rb_root root_inode; // 以 inode 为键的红黑树
access_mask_t fs_access; // 本层实际允许的权限
};
Landlock 的权限规则存储在按 inode 组织的红黑树中,每次文件访问都通过 inode 查找来确定访问策略。
LSM 钩子激活点
Landhook 注册在以下 LSM 钩子上:
| 钩子点 | 覆盖行为 |
|---|---|
file_open |
文件 open/read |
file_truncate |
truncate/ftruncate |
file_receive |
接收文件描述符(SCM_RIGHTS) |
mmap_file |
mmap 映射 |
path_mkdir |
mkdir |
path_mknod |
mknod |
path_symlink |
symlink |
path_unlink |
unlink |
path_rmdir |
rmdir |
path_rename |
rename |
path_link |
link/hardlink |
bprm_committing_creds |
execve 时继承规则集 |
Landlock ABI 权限详解
Landlock 定义了一组 access flags,代表规则集能处理的权限类型。这些权限随 ABI 版本递增:
ABI v1 权限(Linux 5.13 原始版本)
#define LANDLOCK_ACCESS_FS_EXECUTE (1ULL << 0) // 执行文件
#define LANDLOCK_ACCESS_FS_WRITE_FILE (1ULL << 1) // 写文件
#define LANDLOCK_ACCESS_FS_READ_FILE (1ULL << 2) // 读文件
#define LANDLOCK_ACCESS_FS_READ_DIR (1ULL << 3) // 读目录 (getdents)
#define LANDLOCK_ACCESS_FS_REMOVE_DIR (1ULL << 4) // rmdir
#define LANDLOCK_ACCESS_FS_REMOVE_FILE (1ULL << 5) // unlink
#define LANDLOCK_ACCESS_FS_MAKE_CHAR (1ULL << 6) // mknod S_IFCHR
#define LANDLOCK_ACCESS_FS_MAKE_DIR (1ULL << 7) // mkdir
#define LANDLOCK_ACCESS_FS_MAKE_REG (1ULL << 8) // mknod S_IFREG
#define LANDLOCK_ACCESS_FS_MAKE_SOCK (1ULL << 9) // mknod S_IFSOCK
#define LANDLOCK_ACCESS_FS_MAKE_FIFO (1ULL << 10) // mknod S_IFIFO
#define LANDLOCK_ACCESS_FS_MAKE_BLOCK (1ULL << 11) // mknod S_IFBLK
#define LANDLOCK_ACCESS_FS_MAKE_SYM (1ULL << 12) // symlink
ABI v3 新增(Kernel 6.2+)
#define LANDLOCK_ACCESS_FS_REFER (1ULL << 13) // link/rename 跨目录引用
ABI v4 新增(Kernel 6.7+)
新增对特殊文件(如 Unix domain socket)的控制能力。
编程实战:构建一个最小沙箱
下面演示如何用 C 从零构建一个限制文件系统访问的 Landlock 沙箱。
基础沙箱示例
#define _GNU_SOURCE
#include <linux/landlock.h>
#include <sys/syscall.h>
#include <sys/prctl.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#ifndef landlock_create_ruleset
static int landlock_create_ruleset(
const struct landlock_ruleset_attr *attr,
size_t size, __u32 flags)
{
return syscall(__NR_landlock_create_ruleset, attr, size, flags);
}
#endif
#ifndef landlock_add_rule
static int landlock_add_rule(int ruleset_fd, enum landlock_rule_type type,
const void *rule_attr, __u32 flags)
{
return syscall(__NR_landlock_add_rule, ruleset_fd, type, rule_attr, flags);
}
#endif
#ifndef landlock_restrict_self
static int landlock_restrict_self(int ruleset_fd, __u32 flags)
{
return syscall(__NR_landlock_restrict_self, ruleset_fd, flags);
}
#endif
/* 当前 ABI 版本探测 */
static int landlock_abi_version(void) {
int version = landlock_create_ruleset(
NULL, 0, LANDLOCK_CREATE_RULESET_VERSION);
if (version > 0) return version;
// ABI v1 不支持 VERSION flag,需要手动探测
struct landlock_ruleset_attr attr = { .handled_access = 0 };
int ruleset_fd = landlock_create_ruleset(&attr, sizeof(attr), 0);
if (ruleset_fd < 0) return 0;
close(ruleset_fd);
return 1;
}
int main(int argc, char *argv[]) {
/* Step 1: 创建规则集 */
__u64 handled_access =
LANDLOCK_ACCESS_FS_EXECUTE |
LANDLOCK_ACCESS_FS_WRITE_FILE |
LANDLOCK_ACCESS_FS_READ_FILE |
LANDLOCK_ACCESS_FS_READ_DIR |
LANDLOCK_ACCESS_FS_REMOVE_DIR |
LANDLOCK_ACCESS_FS_REMOVE_FILE |
LANDLOCK_ACCESS_FS_MAKE_DIR |
LANDLOCK_ACCESS_FS_MAKE_REG |
LANDLOCK_ACCESS_FS_MAKE_SYM;
struct landlock_ruleset_attr ruleset_attr = {
.handled_access = handled_access,
};
int ruleset_fd = landlock_create_ruleset(
&ruleset_attr, sizeof(ruleset_attr), 0);
if (ruleset_fd < 0) {
perror("landlock_create_ruleset");
return 1;
}
/* Step 2: 添加允许特定路径只读的规则 */
struct landlock_path_beneath_attr path_beneath = {
.allowed_access = LANDLOCK_ACCESS_FS_READ_FILE |
LANDLOCK_ACCESS_FS_READ_DIR,
.parent_fd = open("/usr", O_PATH | O_CLOEXEC),
};
if (path_beneath.parent_fd < 0) {
perror("open /usr");
return 1;
}
int ret = landlock_add_rule(
ruleset_fd,
LANDLOCK_RULE_PATH_BENEATH,
&path_beneath,
0
);
close(path_beneath.parent_fd);
if (ret) {
perror("landlock_add_rule");
return 1;
}
/* Step 3: 激活规则集(不可逆!) */
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) {
perror("prctl(NO_NEW_PRIVS)");
return 1;
}
if (landlock_restrict_self(ruleset_fd, 0)) {
perror("landlock_restrict_self");
return 1;
}
close(ruleset_fd);
/* Step 4: 在此之后,进程只能对 /usr 进行只读访问 */
printf("Landlock sandbox activated. Trying to write /tmp/test.txt...\n");
int fd = open("/tmp/test.txt", O_WRONLY | O_CREAT, 0644);
if (fd < 0 && errno == EACCES) {
printf("Blocked! Cannot write to /tmp — sandbox is working.\n");
}
printf("Trying to read /usr/bin/bash...\n");
fd = open("/usr/bin/bash", O_RDONLY);
if (fd >= 0) {
printf("Success: /usr/bin/bash readable (within allowed path).\n");
close(fd);
}
return 0;
}
编译并运行:
$ gcc -o landlock_demo landlock_demo.c
$ ./landlock_demo
Landlock sandbox activated. Trying to write /tmp/test.txt...
Blocked! Cannot write to /tmp — sandbox is working.
Trying to read /usr/bin/bash...
Success: /usr/bin/bash readable (within allowed path).
关键设计要点
PR_SET_NO_NEW_PRIVS 必须设置:防止子进程通过 execve 提权绕过 Landlock 限制。exec 后的程序需要主动重新应用 Landlock 规则。landlock_restrict_self 不可逆:一旦调用,当前进程及其所有后代进程都将永久受限于该规则集,无法移除。高级模式:网络访问控制(ABI v4+)
从 ABI v4(Linux 6.7)开始,Landhook 开始支持网络访问控制:
struct landlock_net_port_attr {
__u64 allowed_access; // LANDLOCK_ACCESS_NET_BIND_TCP / LANDLOCK_ACCESS_NET_CONNECT_TCP
__u16 port;
};
/* 仅允许绑定 HTTP 端口 8080 */
int add_net_rule(int ruleset_fd, __u16 port) {
struct landlock_net_port_attr net_attr = {
.allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP,
.port = port,
};
return landlock_add_rule(
ruleset_fd,
LANDLOCK_RULE_NET_PORT,
&net_attr,
0);
}
这在构建 "只允许监听特定端口的网络服务沙箱" 时极为有用——结合文件系统规则,可以精确控制一个进程的能力边界。
与 seccomp 的协同:纵深防御
单独使用 Landlock 限制了文件系统访问,但进程仍可能执行危险 syscall(如 ptrace、kexec)。实际生产中,Landlock 通常与 seccomp-bpf 协同工作:
/* 先应用 seccomp 限制 syscall */
static void setup_seccomp(void) {
scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ALLOW);
// 禁止 ptrace
seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(ptrace), 0);
// 禁止 kexec_load
seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(kexec_load), 0);
// 禁止 mount
seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(mount), 0);
seccomp_load(ctx);
}
/* 再应用 Landlock 限制文件系统 */
static void setup_landlock(void) {
/* ... 如上文的 landlock 代码 ... */
}
int main(void) {
setup_seccomp(); // Layer 1: syscall 限制
setup_landlock(); // Layer 2: 文件系统限制(叠加)
// exec 子进程
execvp("./untrusted_program", argv);
}
这种组合构成了典型的多层沙箱纵深防御:seccomp 框定"能做什么",Landlock 控制"能对什么做"。
内核实现深度分析
规则匹配流程
当程序调用 open() 访问 /usr/bin/bash 时,内核执行以下步骤:
security_file_open(file, cred)current->security->landlock_domain 是否有已激活的 ruleset规则继承机制
Landick 的规则集在 fork() 和 clone() 时自动继承(因为 task_struct->landlock_domain 会被复制)。在 execve() 时有一个关键行为:规则集会被保留,但 NO_NEW_PRIVS 确保新程序也无法提升权限来移除规则。
// kernel/fork.c: copy_creds()
if (p->landlock_domain) {
// 子进程继承 landlock 规则引用
atomic_inc(&p->landlock_domain->ruleset->ref);
new->landlock_domain = p->landlock_domain;
}
性能考量
Landlock 的规则查找是在 open 路径上进行的,每次文件访问都会触发。得益于以下设计,其性能开销极低:
if (!task_is_handled_by_landlock))实际测量中,Landlock 引入的额外延迟通常在 1-5 微秒 量级,对于绝大多数应用完全可以忽略。
实战案例:限制 unarchiver 工具
以下是一个实际场景——用户从网上下载了一个 tar.gz 不可信文件并解压。我们希望限制解压工具只写入当前目录:
/* sandbox_unarchiver.c */
#define _GNU_SOURCE
#include <linux/landlock.h>
#include <sys/syscall.h>
#include <sys/prctl.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
enum abi_version { ABI_V1 = 1, ABI_V2 = 2, ABI_V3 = 3 };
static int abi_version = ABI_V1;
/* (前述 landlock_* 封装函数略) */
static int setup_sandbox(void) {
/* 可根据 ABI 版本动态调整 handled_access */
__u64 handled_access =
LANDLOCK_ACCESS_FS_EXECUTE |
LANDLOCK_ACCESS_FS_WRITE_FILE |
LANDLOCK_ACCESS_FS_READ_FILE |
LANDLOCK_ACCESS_FS_READ_DIR |
LANDLOCK_ACCESS_FS_REMOVE_DIR |
LANDLOCK_ACCESS_FS_REMOVE_FILE |
LANDLOCK_ACCESS_FS_MAKE_DIR |
LANDLOCK_ACCESS_FS_MAKE_REG |
LANDLOCK_ACCESS_FS_MAKE_SYM;
if (abi_version >= ABI_V2)
handled_access |= LANDLOCK_ACCESS_FS_REFER;
struct landlock_ruleset_attr attr = {
.handled_access = handled_access,
};
int ruleset_fd = landlock_create_ruleset(&attr, sizeof(attr), 0);
if (ruleset_fd < 0) {
perror("landlock_create_ruleset");
return -1;
}
/* 规则1: 当前目录允许写入和工作 */
int cwd_fd = open(".", O_PATH | O_CLOEXEC);
struct landlock_path_beneath_attr cwd_rule = {
.allowed_access =
LANDLOCK_ACCESS_FS_READ_FILE |
LANDLOCK_ACCESS_FS_WRITE_FILE |
LANDLOCK_ACCESS_FS_READ_DIR |
LANDLOCK_ACCESS_FS_REMOVE_FILE |
LANDLOCK_ACCESS_FS_MAKE_DIR |
LANDLOCK_ACCESS_FS_MAKE_REG |
LANDLOCK_ACCESS_FS_MAKE_SYM,
.parent_fd = cwd_fd,
};
if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, &cwd_rule, 0)) {
perror("landlock_add_rule: cwd");
close(cwd_fd);
return -1;
}
close(cwd_fd);
/* 规则2: /tmp 允许读取(可能 tar 使用临时文件) */
int tmp_fd = open("/tmp", O_PATH | O_CLOEXEC);
struct landlock_path_beneath_attr tmp_rule = {
.allowed_access = LANDLOCK_ACCESS_FS_READ_FILE |
LANDLOCK_ACCESS_FS_READ_DIR,
.parent_fd = tmp_fd,
};
landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, &tmp_rule, 0);
close(tmp_fd);
/* 激活 */
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) ||
landlock_restrict_self(ruleset_fd, 0)) {
perror("activate landlock");
return -1;
}
close(ruleset_fd);
return 0;
}
int main(int argc, char *argv[]) {
if (argc < 2) {
fprintf(stderr, "Usage: %s <archive>\n", argv[0]);
return 1;
}
if (setup_sandbox()) return 1;
/* 现在被沙箱化的上下文内 exec tar 解压 */
char *tar_args[] = { "tar", "xzf", argv[1], NULL };
execvp("tar", tar_args);
perror("execvp tar");
return 1;
}
使用效果:即使 tar 包内包含 ../../../etc/rc.local 这样的路径遍历攻击,Landlock 也无法写入 etc 目录(因为只允许当前目录),路径遍历被自然化解。
Landlock 的局限与注意事项
strace 观察 EACCES 错误。/dev/shm 或 ramfs 中文件没有持久 inode,如果沙箱已经设置后创建的文件可能绕过规则——但实践中几乎不会构成威胁,因为通常没有其他进程会利用这一点。2026 年生态现状
截至 2026 年的 Landlock 生态系统:
Landlock= 单元选项中原生支持配置 Landlock 规则systemd 的 Service 单元配置示例:
[Service]
ExecStart=/usr/bin/my-app
LandlockPaths=/home/user/data:ro
LandlockPaths=/home/user/output:rw
LandlockPaths=/usr/bin:ro
这种声明式配置让 Landhook 的安全能力真正走进了运维日常。
总结
Landhook 代表了 Linux 安全模型的一个重要进化方向:让普通程序也能建立强安全边界,无需特权或复杂的系统级配置。它的设计精巧、性能开销几乎为零、与 seccomp 天然互补。
对于开发者而言,Landhook 是构建沙箱化工具的"最后一块拼图"——它终于让我们可以在不依赖容器运行时的情况下,以最小代价获得可组合、无特权限制的能力。
作为工程师,理解 Landhook 不仅是掌握一个内核 API,更是理解 Linux 安全架构设计哲学中"最小权限原则"在现代操作系统中的具体实践。
参考文档:Linux kernel Documentation/security/landlock.rst、include/uapi/linux/landlock.h

发表评论 取消回复