
SerenityOS 中 setresuid/setresgid 的实现解析真实、有效与保存的 UID/GID 系统调用【免费下载链接】serenityThe Serenity Operating System 项目地址: https://gitcode.com/GitHub_Trending/se/serenity本篇围绕 SerenityOS 的手册页 setresuid(2) 文档 展开完整覆盖setresuid与setresgid这两个系统调用的语义、参数规则与错误约定并结合 LibC 封装实现 与 内核系统调用处理代码 说明其在 SerenityOS 中的真实执行路径。读完后你将能够正确使用这对调用切换进程的多重身份 ID并理解 SerenityOS 内核在权限校验、凭据原子替换与 core dump 标记上的具体实现。一、手册页核心内容1.1 函数名与原型setresuid和setresgid的用途是一次性设置进程的真实real、有效effective和保存saved用户 ID / 组 ID。其 C 原型定义于 LibC 头文件为#include unistd.h int setresuid(uid_t ruid, uid_t euid, uid_t suid); int setresgid(gid_t rgid, gid_t egid, gid_t sgid);1.2 参数语义与-1保留值手册页的 Description 部分给出了两条关键规则设置行为将 real / effective / saved 三个 ID 全部设置为传入的值-1保持不变任一参数传入-1时对应的 ID 保持当前值不变非超级用户的限制对非 superuser 进程每个非-1的传入值必须等于当前 real、effective 或 saved ID 三者之一调用才能成功。内核源码与文档描述一一对应在 sys$setresuid 实现 中三个-1参数首先被替换为当前凭据中的对应值credentials.uid()/euid()/suid()随后通过统一的校验闭包执行权限判定。1.3 返回值与错误成功返回0失败返回-1并设置errno。手册页列出的错误只有EPERM至少一个传入 ID 不等于当前 real / effective / saved ID 之一且调用者不是 superuser。内核代码中该分支正是 setuid.cpp 第 199–200 行 的if ((!ok(new_ruid) || !ok(new_euid) || !ok(new_suid)) !credentials.is_superuser()) return EPERM;。二、背景为什么进程需要三个用户/组 ID理解setresuid/setresgid必须理解 SUID 机制这在 SerenityOS 的手册页 setuid_overview(7) 中有系统说明每个进程都持有三个用户 ID组 ID 同理real UID父进程留下的原始身份、effective UID用于权限检查的身份、saved UID上次有效身份的影子用于恢复提权执行普通可执行文件时三个 ID 都继承父进程执行带 Set User ID 位的可执行文件时effective 与 saved ID 取自文件属主real ID 仍来自父进程——例如/bin/ping以 root 为属主并带 SUID 位任何进程执行它都以 root 运行从而具备发送原始网络包的能力SUID 程序常需要临时降权以调用者的真实身份去读写用户文件再恢复提权saved ID 正是为此设计的手册页明确指出setresuid()、setresgid()、getresuid()、getresgid()是这组“取/设 ID”函数中最灵活、语义最容易理解的一组。由此可以推断setresuid/setresgid是 SUID 程序做权限管理时的首选接口这也是它出现在 geteuid(2)、setuid(2) 等相邻手册页交叉引用中的原因。三、LibC 封装层从 libc 调用到系统调用号用户态入口在 Userland/Libraries/LibC/unistd.cppint setresuid(uid_t ruid, uid_t euid, uid_t suid) { int rc syscall(SC_setresuid, ruid, euid, suid); __RETURN_WITH_ERRNO(rc, rc, -1); } int setregid(gid_t rgid, gid_t egid) { int rc syscall(SC_setresgid, rgid, egid); __RETURN_WITH_ERRNO(rc, rc, -1); } int setresgid(gid_t rgid, gid_t egid, gid_t sgid) { int rc syscall(SC_setresgid, rgid, egid, sgid); __RETURN_WITH_ERRNO(rc, rc, -1); }几个值得注意的事实__RETURN_WITH_ERRNO宏负责把内核返回的错误码写回errno并返回-1成功时返回0与手册页“Return value”一节完全一致系统调用在 Syscall 号表 中注册为setresuid/setresgid均标记为NeedsBigProcessLock::No即不需要持有进程大锁即可进入内部通过VERIFY_NO_PROCESS_BIG_LOCK断言这一点从源码看该仓库中setregid的用户态封装实际派发到了SC_setresgid系统调用号——setregid(rgid, egid)相当于以“saved ID 不变”语义调用setresgid(rgid, egid, 原 sgid)这解释了为什么内核侧 sys$setregid 与sys$setresgid的校验逻辑几乎相同。四、内核实现权限校验、凭据替换与 dumpable 标记4.1 sys$setresuid 的完整执行流程Process::sys$setresuid声明于 Process.h的处理流程为ErrorOrFlatPtr Process::sys$setresuid(UserID new_ruid, UserID new_euid, UserID new_suid) { VERIFY_NO_PROCESS_BIG_LOCK(this); TRY(require_promise(Pledge::id)); return with_mutable_protected_data( - ErrorOrFlatPtr { auto const credentials *protected_data.credentials; // 1) -1 表示“保持不变”替换为当前值 if (new_ruid (uid_t)-1) new_ruid credentials.uid(); if (new_euid (uid_t)-1) new_euid credentials.euid(); if (new_suid (uid_t)-1) new_suid credentials.suid(); // 2) 校验每个新值必须属于 {real, effective, saved}除非是超级用户 auto ok credentials { return id credentials.uid() || id credentials.euid() || id credentials.suid(); }; if ((!ok(new_ruid) || !ok(new_euid) || !ok(new_suid)) !credentials.is_superuser()) return EPERM; // 3) 用新值构造全新的 Credentials 对象并原子替换 auto new_credentials TRY(Credentials::create( new_ruid, credentials.gid(), new_euid, credentials.egid(), new_suid, credentials.sgid(), credentials.extra_gids(), credentials.sid(), credentials.pgid())); // 4) 有效 UID 变化时关闭 core dump 资格 if (credentials.euid() ! new_euid) protected_data.dumpable false; protected_data.credentials move(new_credentials); return 0; }); }sys$setresgid第 257–293 行结构完全对称只是操作gid/egid/sgid三个字段校验闭包与超级用户豁免逻辑一致。4.2 实现要点逐条对应文档约定手册页约定内核实现证据-1保持不变参数预处理阶段将-1回填为当前credentials.uid()/euid()/suid()L191-L196非 superuser 时每个值必须等于三个当前 ID 之一ok闭包对三个目标 ID 逐一判定任一不满足且非超级用户即返回EPERML198-L200超级用户不受限制credentials.is_superuser()短路整个校验Credentials::is_superuser() 定义为euid() 0成功返回 0失败设置 errno统一返回ErrorOrFlatPtr错误码经系统调用机制转换为errno补充两点源码级细节Pledge 沙箱入口处TRY(require_promise(Pledge::id))表明调用方必须先承诺id权限类别这是 SerenityOS 对身份类系统调用的访问控制手段dumpable 语义只要 effective ID 发生变化无论提权还是降权进程都会失去可转储资格protected_data.dumpable false这与 Linux 对“身份变化后禁止 core dump”的安全模型一致。4.3 Credentials凭据的不可变快照Kernel/Security/Credentials.h 中的Credentials是AtomicRefCounted引用计数对象封装了uid/gid/euid/egid/suid/sgid、附加组列表extra_gids、会话 ID 与进程组 ID。setresuid/setresgid并不修改旧凭据而是调用Credentials::create构造全新的凭据对象后整体替换protected_data.credentials——这种“不可变 原子替换”方式保证了其他读取凭据的代码路径如权限检查、进程列表展示看到的始终是完整一致的一组 ID不存在改了一半的中间状态。五、典型用法临时降权与永久弃权setuid_overview(7) 手册页给出了三个可直接套用的 SUID 程序权限管理模式均基于setresuid1临时放弃提权——把 effective UID 降为低权限用户同时把当前 effective ID 存入 saved ID以便后续恢复if (setresuid(-1, new_uid, geteuid()) 0) return OH_NO;由于 SUID 二进制的 saved ID 初始就是文件属主 ID实践中这与seteuid(getuid())等价但用 saved ID 显式保存更易于推理。2恢复提权——先读出当前三元组再把 saved ID 装回 effective 位uid_t ruid, euid, suid; if (getresuid(ruid, euid, suid) 0) return OH_NO; if (setresuid(-1, suid, -1) 0) return OH_NO;手册页同时警告SUID 程序通常属主为 rootUID 0此时恢复提权常等价于seteuid(0)——若 SUID 程序在临时降权后不安全地处理用户输入攻击者就可能借恶意输入触发seteuid(0)重新提权。3永久弃权——把 real ID 复制进 effective 和 saved 两个位置之后在非 superuser 前提下便无法再把 effective ID 设回高权限值if (setresuid(new_uid, new_uid, new_uid) 0) return OH_NO;在 SerenityOS 上这通常与setuid(new_uid)等效但setresuid的写法把“三个 ID 全部落到低权限值”这一意图表达得更明确。组 ID 的操作完全对称用setresgid替换即可。手册页还给出了一条实践建议先降组权限、再降用户权限若只是临时降权恢复用户权限后再恢复组权限。六、与相邻 ID 设置调用的对照SerenityOS 内核在同一个 setuid.cpp 文件中实现了整套身份修改调用均可作为setresuid/setresgid的对照系统调用修改范围-1语义手册页setuid/setgid同时修改 real、effective、saved 三个 ID非法返回EINVAL见 sys$setuid L83-L84setuid(2)seteuid/setegid仅 effective ID非法返回EINVALseteuid(2)setreuid/setregidreal 与 effective 两个 ID保持不变与本页同组setresuid/setresgid全部三个 ID保持不变本页setgroups附加组列表—非 superuser 直接EPERM见 sys$setgroups从源码结构看所有调用都要求Pledge::id、都遵循“非 superuser 只能在自己的三个 ID 之间挑选”这一不变量、都通过Credentials::create原子换凭据、都在 effective ID 变化时清除dumpable标记——setresuid/setresgid之所以被文档称为“最灵活、语义最容易理解”正是因为它是唯一能显式触碰 saved ID 的接口。七、小结与适用前提setresuid(ruid, euid, suid)/setresgid(rgid, egid, sgid)以三个独立参数覆盖真实、有效与保存 ID-1表示保留当前值SerenityOS 内核的实现位于 Kernel/Syscalls/setuid.cpp用户态封装位于 Userland/Libraries/LibC/unistd.cpp凭据模型位于 Kernel/Security/Credentials.h权限模型superusereuid 0可设置任意值普通进程只能在自己现有三个 ID 的集合内取值否则得到EPERM前提与限制调用方需要持有Pledge::id承诺effective ID 一旦变化进程失去 core dump 资格SUID 程序应按“临时降权—恢复—或永久弃权”的模式使用本接口并注意降权窗口内的输入安全问题进一步阅读setuid_overview(7)、getresuid(2)、getuid(2)。【免费下载链接】serenityThe Serenity Operating System 项目地址: https://gitcode.com/GitHub_Trending/se/serenity创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考