登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  linux

Linux openat2 RESOLVE_BENEATH怎样限制路径不能逃逸根目录

来源:17golang原创

时间:2026-11-05 02:34:31 208浏览 收藏

可以。关键是不要先把用户路径和根目录拼成字符串再自行判断,而是先打开一个可信的目录文件描述符,把它作为 dirfd,再调用 openat2(),并在 struct open_how.resolve 中设置 RESOLVE_BENEATH。这样内核会把路径解析限制在这个 dirfd 的后代目录内;一旦发现路径从边界逃逸,调用会失败,常见错误是 EXDEV。

下面的示例还显式加入 RESOLVE_NO_MAGICLINKS,避免把当前对魔链的隐含行为当成长期契约。示例只讨论“打开受限目录中的已有文件”,不负责创建目录、不把用户输入当作根目录,也不把字符串规范化当成安全边界。

把可信根目录固定为 root_fd,让用户输入保持相对路径,再交给 openat2() 做带约束的解析,是限制路径逃逸的核心组合。RESOLVE_BENEATH 负责“不能走出 dirfd”,其他 RESOLVE_* 标志负责补充符号链接、魔链和挂载点策略。

先把受限目录变成稳定的 dirfd

RESOLVE_BENEATH 不是“检查一个字符串是否以某个前缀开头”。它约束的是内核进行路径查找时的结果:所有组件都必须落在传入的 dirfd 后代范围内。因此根目录应由服务配置决定,并用 O_PATH | O_DIRECTORY | O_CLOEXEC 打开;用户只能提供相对于这个目录的路径。

例如,服务根目录是 /srv/app-data,用户路径是 reports/2026/summary.txt。服务传给内核的是 root_fd 和这个相对路径,而不是把两段字符串拼成一个新的绝对路径。这样即使输入包含 ../,边界也仍然由路径解析过程本身负责。

root_fd、相对路径和 openat2 解析约束的静态关系图
图1:结构图。root_fd 提供可信边界,相对路径进入 openat2,resolve 标志约束解析结果;这是静态说明图,不是终端截图或运行证据。

用 RESOLVE_BENEATH 绑定解析边界

调用接口是一个系统调用:当前 glibc 没有稳定的 openat2() 包装函数,C 程序通常通过 syscall(SYS_openat2, ...) 调用。open_how 必须清零后再设置字段,大小参数使用 sizeof(how),这样未来结构体增加字段时不会带入未初始化值。

#define _GNU_SOURCE
#include 
#include 
#include 
#include 
#include 

static int open_beneath(int root_fd, const char *relative_path) {
    struct open_how how = {0};

    // 只读打开并自动关闭,避免把权限或生命周期交给用户路径。
    how.flags = O_RDONLY | O_CLOEXEC;
    // BENEATH 限制不能离开 root_fd;魔链策略显式写出,避免依赖隐含行为。
    how.resolve = RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS;

    for (int attempt = 0; attempt = 0) {
            return fd;
        }
        // 内核无法确认 .. 没有在竞态中越界时允许一次有限重试。
        if (errno != EAGAIN) {
            break;
        }
    }

    return -1;
}

这段函数没有把 relative_path 改写成“看起来安全”的新字符串,而是把原始相对路径交给内核。成功返回的是文件描述符;失败时保留 errno,由调用方决定是把输入标记为非法、记录诊断信息,还是在允许的场景下重试。

给魔链和符号链接设置独立策略

RESOLVE_BENEATH 的重点是“不能离开 dirfd”,并不等于“路径中绝不允许任何符号链接”。普通符号链接只要解析结果仍在边界内,可能继续工作;如果业务要求路径树完全不经过符号链接,应再加 RESOLVE_NO_SYMLINKS。它会连同魔链一起禁止,兼容性代价也更大。

RESOLVE_NO_MAGICLINKS 是较窄的策略,专门拒绝例如 /proc 下的魔链。虽然当前文档说明 RESOLVE_BENEATH 也会阻止魔链解析,但调用方不应把当前的附带行为当作未来保证,因此示例显式写出该标志。若还要阻止 bind mount 或普通挂载点穿越,可以评估 RESOLVE_NO_XDEV;这会改变可访问目录树,不能默认打开。

// 仅在业务要求“任何符号链接都不接受”时使用更严格的组合。
how.resolve = RESOLVE_BENEATH
            | RESOLVE_NO_MAGICLINKS
            | RESOLVE_NO_SYMLINKS;

// 需要同时禁止挂载点穿越时再加入该标志,先确认目录布局允许它。
// how.resolve |= RESOLVE_NO_XDEV;

按 EXDEV、EAGAIN 和 ENOSYS 处理异常

路径限制代码最容易出错的地方不是标志名,而是把所有失败都当成“文件不存在”。至少要区分下面几类结果:

错误通常含义处理建议
EXDEV解析过程发现越出 dirfd;如果同时使用 NO_XDEV,也可能是跨越挂载点。拒绝本次输入并记录相对路径,不要改成字符串拼接重试。
EAGAIN内核在处理 .. 时遇到竞态,无法确认边界未被绕过。可按服务策略做有限重试;连续失败就返回错误。
ELOOP启用 NO_SYMLINKS 或 NO_MAGICLINKS 后遇到被禁止的链接。检查业务是否真的需要链接;不要无条件放宽安全标志。
ENOSYS运行内核不认识 openat2 系统调用。安全场景下失败关闭;不要把不等价的 openat 加路径判断当作透明替代。

编译时只需要系统头文件和 C 编译器。下面命令只是构建入口,示例没有把它当作文章发布前的本地运行验证:

# 使用 Linux 系统调用头文件编译示例,不依赖 glibc 的 openat2 包装函数
cc -Wall -Wextra -O2 -o open-beneath open-beneath.c
RESOLVE_BENEATH 与 EXDEV、EAGAIN、ELOOP、ENOSYS 的静态策略关系图
图2:结构图。不同 resolve 策略与错误结果属于独立的边界关系;图中不表示执行时序,也不是运行日志或截图。

选择 BENEATH 还是 IN_ROOT

两者都能把一次路径解析限制到 dirfd 附近,但语义不同:

  • RESOLVE_BENEATH 适合“输入必须是根目录下的相对路径”。绝对路径会被拒绝,解析结果不能成为 dirfd 之外的后代。
  • RESOLVE_IN_ROOT 更像把 dirfd 临时当作这一次解析的根,绝对路径和符号链接中的绝对目标会按这个根解释。它适合已经采用“虚拟根”语义的容器或资源服务。
  • 如果业务不允许任何符号链接,单独补充 RESOLVE_NO_SYMLINKS;如果只是不希望触碰 proc 等魔链,使用 RESOLVE_NO_MAGICLINKS 更精确。

无论选择哪组标志,root_fd 都应来自可信配置,不能由请求参数动态决定。文件打开成功后及时关闭返回的文件描述符,根目录 fd 则跟随服务生命周期管理;不要把“打开成功”误解为后续对文件内容的访问也自动获得权限。

int root_fd = open("/srv/app-data", O_PATH | O_DIRECTORY | O_CLOEXEC);
if (root_fd 

小结与排查清单

要限制 Linux 文件路径不能逃逸根目录,可以记住五个动作:一是由可信配置打开根目录 fd;二是只接受相对路径;三是用 RESOLVE_BENEATH 把解析限制在 dirfd 后代;四是根据业务显式选择魔链、符号链接和挂载点策略;五是对 EXDEV、EAGAIN、ELOOP 和 ENOSYS 分开处理。

如果你看到 EXDEV,先检查用户路径是否包含绝对路径、.. 或指向边界外的链接;如果看到 EAGAIN,不要把它改写成“路径不存在”,而应执行有限重试或返回失败。真正需要绝对路径按沙箱根解释时,再考虑 RESOLVE_IN_ROOT,不要为了兼容而无条件放宽 RESOLVE_BENEATH。

相关问题

  • RESOLVE_BENEATH 能否替代 chroot?不能简单等同。它是一次 openat2 调用的路径解析约束,不会永久改变进程根目录。
  • 为什么还要显式写 RESOLVE_NO_MAGICLINKS?因为当前 BENEATH 的附带行为不应被当成未来接口契约,显式配置更容易审查。
  • 普通符号链接也必须禁止吗?只有业务要求路径树无链接时才加 NO_SYMLINKS,否则可能误伤合法目录布局。
声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>