SerenityOS 的 su 命令完全指南:切换用户与身份认证的实现原理

📅 发布时间:2026/9/10 21:45:13
SerenityOS 的 su 命令完全指南:切换用户与身份认证的实现原理
SerenityOS 的 su 命令完全指南切换用户与身份认证的实现原理【免费下载链接】serenityThe Serenity Operating System 项目地址: https://gitcode.com/GitHub_Trending/se/serenity导读suswitch user是 SerenityOS 中用于切换当前用户身份的核心系统命令无参数时默认切换到 rootUID 0也可以指定用户名切换到任意账户甚至能以目标用户身份直接执行单条命令而无需进入交互式 shell。本文将基于系统手册 Base/usr/share/man/man1/su.md 完整讲解其语法、选项、认证流程并结合 Userland/Utilities/su.cpp 与 Userland/Libraries/LibCore/Account.cpp 的源码深入剖析 su 从参数解析、密码验证到最终 exec 切换身份的全链路实现。读完本文你将掌握 su 的每一种用法并理解其与pls、/etc/shadow等机制的关系。命令概览su允许当前会话切换到另一个用户。系统手册给出的语法为$ su [-] [user] $ su [-] [user] [-c command]当不指定用户时su默认切换到root即 UID 0 的用户。若目标用户设置了密码则需要输入该用户的密码才能完成切换。手册中的 See also 部分指向pls(8)其说明文档位于 Base/usr/share/man/man8/pls.md二者是 SerenityOS 中两种不同的提权途径su需要目标账户密码而pls仅允许wheel组的成员执行并以 UID/GID 0 运行命令。选项与参数详解选项Options选项等价形式含义--l、--login以真实登录login的方式启动 shell-c--command使用/bin/sh执行指定命令而非启动交互式 shell在源码 Userland/Utilities/su.cpp 中这两个选项分别被解析为args_parser.add_option(command, Command to execute, command, c, command); args_parser.add_option(simulate_login, Simulate login, login, l);值得注意的一个实现细节-单个短横线并不是独立注册的选项而是通过第一个位置参数捕获add_positional_argument(..., -, ...)随后在参数解析完成后进行特殊处理if (first_positional -) { simulate_login true; user second_positional; }也就是说su -与su -l、su --login在效果上完全等价都会将simulate_login置为真。参数Arguments参数含义user要切换到的目标用户默认是 UID 0 的用户root源码中用户名的解析逻辑为auto account TRY(user.is_empty() ? Core::Account::from_uid(0) : Core::Account::from_name(user));即未指定用户名时通过from_uid(0)直接按 UID 0 解析 root 账户指定用户名时通过from_name(user)按名称解析。这两种工厂方法均由Core::Account提供定义于 Userland/Libraries/LibCore/Account.h。运行前提为什么 su 必须以 root 身份执行与常见的 Unixsu实现不同SerenityOS 的 su 在启动后会立即做一次特权校验Userland/Utilities/su.cppif (geteuid() ! 0) return Error::from_string_literal(Not running as root :();只有有效用户 IDeffective UID为 0 的进程才能运行 su否则会直接报错退出。这意味着在 SerenityOS 中普通用户若要使用 su需要先借助其他提权途径例如pls以 root 执行su——这与许多桌面 Linux 发行版上“任意用户都可运行 su 并输入目标用户密码”的行为存在明显差异。这正是手册文档与源码相互印证的关键实现事实。进程在启动时还通过pledge声明了所需权限集Userland/Utilities/su.cppTRY(Core::System::pledge(stdio rpath tty exec id));stdio标准输入输出rpath读取/etc/passwd、/etc/shadow等账户文件tty在终端上读取密码exec最终执行目标用户的 shellid读取/设置进程的 UID、GID 与附加组。随着流程推进pledge 集合会被逐步收缩先去掉tty最终只保留stdio exec体现 SerenityOS 最小权限原则的工程实践。密码认证流程什么时候需要密码认证判断位于 Userland/Utilities/su.cppif (getuid() ! 0 account.has_password()) { if (!TRY(Core::System::isatty(STDIN_FILENO))) return Error::from_string_literal(Standard input is not a terminal); auto password TRY(Core::get_password()); if (!account.authenticate(password)) return Error::from_string_literal(Incorrect or disabled password.); }认证逻辑要点只有当实际 UIDgetuid非 0且目标账户设置了密码时才要求输入密码——如果当前已是 root则直接跳过认证密码必须从终端TTY读取若标准输入不是终端例如通过管道或脚本调用会报错Standard input is not a terminal并退出密码读取通过Core::get_password()完成不回显输入认证失败统一报错Incorrect or disabled password.不会区分密码错误与账户密码被禁用避免泄露账户状态。底层认证实现Account::authenticate实现在 Userland/Libraries/LibCore/Account.cppbool Account::authenticate(SecretString const password) const { // If there was no shadow entry for this account, authentication always fails. if (!m_password_hash.has_value()) return false; // An empty passwd field indicates that no password is required to log in. if (m_password_hash-is_empty()) return true; char* hash crypt(password.characters(), bytes); return hash ! nullptr AK::timing_safe_compare(hash, bytes, m_password_hash-length()); }账户信息来自/etc/passwd与/etc/shadow若该账户在 shadow 文件中没有条目认证一律失败若密码字段为空表示无需密码即可登录对应has_password()返回 false否则使用crypt()对输入密码加盐计算哈希并通过AK::timing_safe_compare做常量时间比较降低时序侧信道风险。has_password()的语义在 Userland/Libraries/LibCore/Account.h 中有明确注释不存在的密码即账户缺失于 /etc/shadow被视为有密码认证将永远失败——这是刻意为之的安全设计避免暴露账户是否存在。身份切换login() 与 shell 启动建立目标身份认证通过后account.login()负责完成身份切换Userland/Libraries/LibCore/Account.cppErrorOrvoid Account::login() const { TRY(Core::System::setgroups(m_extra_gids)); TRY(Core::System::setgid(m_gid)); TRY(Core::System::setuid(m_uid)); TRY(Core::Environment::set(HOMEsv, m_home_directory, Core::Environment::Overwrite::Yes)); return {}; }调用顺序至关重要先设置附加组setgroups再设置 GIDsetgid最后设置 UIDsetuid。由于设置 UID 后进程将失去对自身组属性的修改权这个顺序保证了目标用户的附加组/etc/group中该用户所属的组与主 GID 都能被正确建立。最后HOME环境变量被更新为目标账户的家目录使后续 shell 拥有正确的起始环境。模拟登录--login的行为差异simulate_login标志在此刻生效Userland/Utilities/su.cppif (simulate_login) TRY(Core::System::chdir(account.home_directory()));普通切换su user保持当前工作目录不变直接进入目标 shell模拟登录su - user/su -l user/su --login user切换到目标账户的家目录再启动 shell模拟真实登录会话的初始环境。启动 shell 或执行命令最后su 通过exec直接替换当前进程镜像Userland/Utilities/su.cppif (command.is_null()) { TRY(Core::System::exec(account.shell(), ArrayStringView, 1 { account.shell().view() }, Core::System::SearchInPath::No)); } else { TRY(Core::System::exec(account.shell(), ArrayStringView, 3 { account.shell().view(), -csv, command }, Core::System::SearchInPath::No)); }未提供-c以目标账户在/etc/passwd中登记的 shellaccount.shell()如/bin/sh启动一个交互式 shell提供了-c command以shell -c command的形式执行单条命令执行完毕后进程随即结束不会进入交互界面。由于exec直接替换了进程映像su 进程自身的 UID/GID 身份、HOME环境变量和当前工作目录都会被新进程完整继承——这正是切换到另一个用户得以成立的机制。完整使用示例示例 1切换到 root$ su不指定用户时默认切换至 UID 0root。若当前进程非 root 且 root 账户设置了密码则提示输入密码认证通过后进入 root 的 shell。示例 2切换到指定用户$ su nona以nona的身份启动交互式 shell保持当前工作目录不变。示例 3模拟登录切换$ su - nona $ su -l nona $ su --login nona三种写法等价将工作目录切换到nona的家目录后再启动 shell模拟真实登录效果。示例 4以目标用户执行单条命令$ su -c whoami root以 root 身份执行whoami并立即退出适合脚本中需要临时以其他用户身份执行单个命令的场景。示例 5组合使用$ su - nona -c id模拟登录并执行id输出会显示nona的 UID、GID 及其附加组信息可用来验证login()中 setgroups/setgid/setuid 的生效结果。与 pls 的分工系统手册在 See also 中引用了pls(8)见 Base/usr/share/man/man8/pls.md。二者的定位差异维度supls功能切换到任意用户默认 root以 rootUID/GID 0执行命令权限前提进程须为 rootgeteuid() 0用户须属于wheel组认证方式目标账户密码当前用户密码交互性可进入交互 shell执行完命令即退出pls还支持用--分隔命令选项例如pls -- ls -la。若需要在普通用户会话中启动su可先通过pls获得 root 环境再在其中调用su。小结su是 SerenityOS 身份切换机制中不可或缺的一环其实现虽简洁却完整覆盖了参数解析-/-l/--login/-c/--command、root 特权校验、基于/etc/shadow的密码认证、附加组与 UID/GID 的顺序切换、HOME与工作目录的环境重建以及最终通过exec启动目标 shell 或执行命令的全流程。理解 Userland/Utilities/su.cpp 与 Userland/Libraries/LibCore/Account.cpp 这两处源码即可完整掌握 su 的内部运作也能为阅读pls、login等相邻命令的实现打下基础。【免费下载链接】serenityThe Serenity Operating System 项目地址: https://gitcode.com/GitHub_Trending/se/serenity创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考