SerenityOS access() 系统调用解析:文件访问权限检查的实现与使用指南
本指南围绕 SerenityOS 用户手册中 access 手册页(Base/usr/share/man/man2/access.md)展开,系统讲解 access() 的语义、四种模式标志、返回值与错误处理,并结合内核源码(Kernel/Syscalls/faccessat.cpp、Kernel/FileSystem/VirtualFileSystem.cpp)揭示其底层实现原理。读完本文,你将掌握如何在 SerenityOS 用户态程序中使用 access() 检查文件可访问性,并理解从 LibC 封装到 VFS 权限校验的完整调用链。
函数概览
access() 用于检查指定路径上的文件是否存在,以及当前用户是否可按给定模式访问该文件。
头文件与原型
#include <unistd.h>
int access(const char* path, int mode);
- path:要检查的文件路径。
- mode:要检查的访问模式,可取下列标志之一(或按位组合,见下节)。
- 返回值:文件可按指定模式访问时返回
0;否则返回-1并设置errno描述错误原因。
在 SerenityOS 中,该函数声明位于 Userland/Libraries/LibC/unistd.h,POSIX 兼容实现位于 Userland/Libraries/LibC/unistd.cpp。
mode 参数:四种访问模式标志
mode 参数支持以下四个标志,定义于 Kernel/API/POSIX/unistd.h:
| 标志 | 数值 | 含义 |
|---|---|---|
F_OK |
0 | 检查文件本身是否可访问(即是否存在) |
R_OK |
4 | 检查文件是否可读 |
W_OK |
2 | 检查文件是否可写 |
X_OK |
1 | 检查文件是否可作为程序执行 |
从源码可见这些标志按位定义,R_OK、W_OK、X_OK 之间可以按位或组合使用(例如 access(path, R_OK | W_OK) 同时检查读写权限),而 F_OK 数值为 0,仅用于单独判断文件是否存在。
if (access("/etc/passwd", R_OK) == 0) {
// 当前用户可以读取该文件
}
if (access("/bin/sh", X_OK) == 0) {
// 当前用户可以执行该程序
}
返回值与错误处理
- 文件确实可按指定
mode访问时,access()返回0。 - 否则返回
-1,并通过errno说明具体错误。
常见 errno 含义包括:
EACCES:权限不足,无法按请求的模式访问文件。ENOENT:路径不存在。EROFS:文件所在文件系统只读,无法写入(仅在使用W_OK时可能出现,见下文内核实现)。
源码级实现:从 LibC 到内核
access() 并非独立系统调用,而是建立在 faccessat() 之上的封装,最终经由 VFS(虚拟文件系统)完成权限判定。下面按调用链自顶向下展开。
第一步:LibC 层封装
Userland/Libraries/LibC/unistd.cpp 中,access() 直接转发给 faccessat():
int access(char const* pathname, int mode)
{
return faccessat(AT_FDCWD, pathname, mode, 0);
}
int faccessat(int dirfd, char const* pathname, int mode, int flags)
{
if (!pathname) {
errno = EFAULT;
return -1;
}
Syscall::SC_faccessat_params params { dirfd, { pathname, strlen(pathname) }, mode, flags };
int rc = syscall(SC_faccessat, ¶ms);
__RETURN_WITH_ERRNO(rc, rc, -1);
}
关键点:
access()以AT_FDCWD作为目录描述符,即以进程当前工作目录为基准解析相对路径。- 传入的空指针会被提前拦截并返回
EFAULT,避免进入内核。 - 最终通过
SC_faccessat系统调用号进入内核。
第二步:内核系统调用入口
内核侧对应实现为 sys$faccessat(),位于 Kernel/Syscalls/faccessat.cpp:
ErrorOr<FlatPtr> Process::sys$faccessat(Userspace<Syscall::SC_faccessat_params const*> user_params)
{
VERIFY_NO_PROCESS_BIG_LOCK(this);
TRY(require_promise(Pledge::rpath));
auto params = TRY(copy_typed_from_user(user_params));
auto pathname = TRY(get_syscall_path_argument(params.pathname));
if ((params.flags & ~(AT_SYMLINK_NOFOLLOW | AT_EACCESS)) != 0)
return EINVAL;
auto flags = AccessFlags::None;
if (params.flags & AT_SYMLINK_NOFOLLOW)
flags |= AccessFlags::DoNotFollowSymlinks;
if (params.flags & AT_EACCESS)
flags |= AccessFlags::EffectiveAccess;
CustodyBase base(params.dirfd, pathname->view());
TRY(VirtualFileSystem::access(vfs_root_context(), credentials(), pathname->view(), params.mode, base, flags));
return 0;
}
该入口值得注意的实现细节:
- pledge 承诺:调用需要进程已承诺
rpath(读取路径解析)权限,这体现了 SerenityOS 的 pledge 安全模型——即使拿到系统调用入口,未被承诺的进程也会被拒绝。 - flags 校验:除
AT_SYMLINK_NOFOLLOW(不跟随符号链接)与AT_EACCESS(使用有效 ID 而非实际 ID 检查)外,其余 flags 一律返回EINVAL。 - 路径参数通过
get_syscall_path_argument()安全地从用户态拷贝,防止内核直接解引用用户指针。
第三步:VFS 权限判定
真正的权限检查落在 Kernel/FileSystem/VirtualFileSystem.cpp 的 VirtualFileSystem::access():
ErrorOr<void> VirtualFileSystem::access(VFSRootContext const& vfs_root_context, Credentials const& credentials, StringView path, int mode, CustodyBase const& base, AccessFlags access_flags)
{
auto should_follow_symlinks = !has_flag(access_flags, AccessFlags::DoNotFollowSymlinks);
auto custody = TRY(resolve_path(vfs_root_context, credentials, path, base, nullptr, should_follow_symlinks ? 0 : O_NOFOLLOW_NOERROR));
auto& inode = custody->inode();
auto metadata = inode.metadata();
auto use_effective_ids = has_flag(access_flags, AccessFlags::EffectiveAccess) ? UseEffectiveIDs::Yes : UseEffectiveIDs::No;
if (mode & R_OK) {
if (!metadata.may_read(credentials, use_effective_ids))
return EACCES;
}
if (mode & W_OK) {
if (!metadata.may_write(credentials, use_effective_ids))
return EACCES;
if (custody->is_readonly())
return EROFS;
}
if (mode & X_OK) {
if (!metadata.may_execute(credentials, use_effective_ids))
return EACCES;
}
return {};
}
该实现揭示了 access() 的完整判定逻辑:
- 路径解析:先通过
resolve_path()将路径解析为 inode。默认跟随符号链接;若指定AT_SYMLINK_NOFOLLOW,则以O_NOFOLLOW_NOERROR语义解析。 - 按位检查:对
mode中设置的每一位分别调用 inode 元数据上的may_read/may_write/may_execute方法(这些方法内部综合权限位、属主、属组与补充组进行判定)。 - 写权限特判:即使文件权限允许写入,只要所在挂载点或 custody 为只读(
is_readonly()),就返回EROFS——这正是只读文件系统上access(path, W_OK)失败的来源。 - 有效 ID 与真实 ID:默认按进程真实 UID/GID 判定;指定
AT_EACCESS时改为按有效 ID 判定,与 POSIX 语义一致。
由于 mode 采用位标志,F_OK(数值 0)不会命中上述任一分支,因此该函数天然将"纯存在性检查"实现为仅做路径解析与元数据读取,权限位检查被跳过。
使用场景与注意事项
典型用法
access() 常见于以下场景:
- 程序启动时探测配置文件、数据目录是否存在且可读写;
- 检查外部命令是否在可执行路径上(
X_OK); - 在尝试打开文件前预判是否具备读取权限。
注意:检查与使用之间存在 TOCTOU 竞态
access() 基于调用时的路径与凭据做检查,检查通过与实际 open()/exec() 之间文件状态可能发生变化(即 time-of-check to time-of-use 竞态)。对于安全敏感场景,更稳妥的做法是直接尝试打开文件并根据 errno 处理错误,而不是先 access() 再操作。
与 pledge 的配合
如前所述,内核入口要求进程持有 Pledge::rpath 承诺。在 SerenityOS 中编写使用 access() 的程序时,需确保进程 pledge 了 rpath,否则系统调用会被拒绝。这一设计将"检查文件可访问性"归入路径读取类能力,与其他只读路径操作保持一致。
小结
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00