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 不可逆:一旦调用,当前进程及其所有后代进程都将永久受限于该规则集,无法移除。
  • 规则集可叠加(Layered Mode):可以创建多个 ruleset 并通过多次调用叠加——每个新层只能进一步收紧权限,不会放宽已有约束。这实现了类似 Unix umask 的累进式限制模型。
  • 路径规则基于 inode 而非字符串:内核将路径转换为 inode 进行匹配,因此符号链接无法绕过规则——这与使用真实路径字符串匹配的其他安全工具有本质不同。

  • 高级模式:网络访问控制(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 时,内核执行以下步骤:

  • VFS 层解析路径 → 获取目标 inode
  • 进入 LSM 钩子 security_file_open(file, cred)
  • hook 检查 current->security->landlock_domain 是否有已激活的 ruleset
  • 遍历 ruleset → 在 inode 红黑树中查找匹配项
  • 检查 requested access ⊆ allowed_access
  • 若全部层次通过 → 允许访问;任一层次拒绝 → 返回 EACCES
  • 规则继承机制

    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 路径上进行的,每次文件访问都会触发。得益于以下设计,其性能开销极低:

  • 使用 inode 红黑树而非路径字符串比较,查找复杂度 O(log n)
  • 仅对已启用 Landlock 的进程进行检查(快速路径:if (!task_is_handled_by_landlock))
  • 规则集使用前编译为 BTF-friendly 的扁平结构,无运行时解析开销
  • 实际测量中,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 的局限与注意事项

  • 仅限文件系统:Landhook 不限制网络、IPC、信号处理等。对于需要全面隔离的场景,仍需配合 seccomp、namespace、cgroup。
  • 不追踪移动/重命名(ABI < v3):只要 ABI 版本低于 v3,攻击者可能利用 rename 在已授权区域内移动恶意文件到未授权位置。建议在生产使用 ABI v3+。
  • 不支持审计日志:默认情况下,Landlock 拒绝访问既不打日志也不通知用户态。调试时需要结合 strace 观察 EACCES 错误。
  • dnotify/fanotify 不受影响:Landlock 只限制文件修改操作,不影响文件事件通知 API。
  • OverlayFS 场景下的 inode 变化:在使用 OverlayFS 或某些 FUSE 文件系统时,inode 可能动态生成,需注意规则匹配的有效性。
  • tmpfs 和 ramfs 中的文件:在 /dev/shm 或 ramfs 中文件没有持久 inode,如果沙箱已经设置后创建的文件可能绕过规则——但实践中几乎不会构成威胁,因为通常没有其他进程会利用这一点。

  • 2026 年生态现状

    截至 2026 年的 Landlock 生态系统:

  • Flatpak 默认启用 Landlock,限制应用对 XDG 目录之外的访问
  • ChromeOS 将 Landlock 作为其多层安全架构的一环
  • Android (Linux 6.1+) 正在评估 Landlock 用于应用沙箱强化
  • systemd 254+ 在 Landlock= 单元选项中原生支持配置 Landlock 规则
  • rust-landlock 和 golang 实验绑定 使得在高级语言中使用 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

    点赞(0) 打赏

    评论列表 共有 0 条评论

    暂无评论
    立即
    投稿

    微信公众账号

    微信扫一扫加关注

    发表
    评论
    返回
    顶部