我们来深入学习 setfsgid
和 setfsuid
这两个系统调用。
(https://www.calcguide.tech/2025/08/19/setfsuid%e7%b3%bb%e7%bb%9f%e8%b0%83%e7%94%a8%e5%8f%8a%e7%a4%ba%e4%be%8b/)
1. 函数介绍
在 Linux 系统中,每个进程都有一套与之相关的用户 ID (UID) 和组 ID (GID),比如:
- 真实用户 ID (Real UID): 登录系统的用户 ID。
- 有效用户 ID (Effective UID): 决定进程当前拥有哪些权限,用于权限检查。
- 保存的设置用户 ID (Saved Set-UID): 用于在有效 UID 和真实 UID 之间切换。
通常,文件访问权限检查是基于进程的 有效 UID 和 有效 GID 进行的。
但是,Linux 提供了两个特殊的系统调用:setfsuid
和 setfsgid
。
setfsuid
(Set File System UID): 用来设置或修改进程的 File System UID (文件系统用户 ID)。setfsgid
(Set File System GID): 用来设置或修改进程的 File System GID (文件系统组 ID)。
关键点:
这两个 ID 只用于文件系统的权限检查!它们不影响其他任何权限检查(比如信号发送权限、进程调度优先级等)。当内核执行文件访问权限检查时,它会使用 文件系统 UID/GID 而不是 有效 UID/GID。
默认情况下:
当一个进程启动时,它的 文件系统 UID/GID 会被自动设置为与 有效 UID/GID 相同。
为什么需要它们?(主要场景)
一个经典的例子是 NFS (Network File System) 服务器:
1. NFS 服务器进程通常以 root 权限运行,因此它的 有效 UID 是 0 (root)。
2. 但是,当它代表一个非 root 用户(比如 UID 1000)访问 NFS 文件系统上的文件时,它需要进行权限检查,仿佛是 UID 1000 在访问。
3. 如果只使用 setuid
/seteuid
来切换有效 UID,会影响服务器进程的其他权限(比如绑定到特权端口)。
4. 所以,NFS 服务器可以调用 setfsuid(1000)
,这样在进行文件权限检查时,内核会看到 UID 是 1000,但进程的其他权限(如有效 UID 仍然是 root)保持不变。
简单来说:setfsuid
和 setfsgid
允许进程在进行文件访问权限检查时,“伪装”成另一个用户或组,而不会影响进程的其他特权。
2. 函数原型
#include <sys/fsuid.h> // 包含函数声明 (在某些系统上可能在 unistd.h 或其他地方)
// 设置文件系统用户 ID
int setfsuid(uid_t fsuid);
// 设置文件系统组 ID
int setfsgid(gid_t fsgid);
3. 功能
分别设置调用进程的文件系统用户 ID (FS-UID) 和文件系统组 ID (FS-GID),仅用于文件系统的访问权限检查。
4. 参数
fsuid
:uid_t
类型。- 指定要设置的新文件系统用户 ID。
fsgid
:gid_t
类型。- 指定要设置的新文件系统组 ID。
5. 返回值
这两个函数的返回值比较特殊:
- 总是返回调用者之前的 文件系统 UID (
setfsuid
) 或 文件系统 GID (setfsgid
)。 - 无论调用成功与否!
这与其他大多数系统调用不同(它们通常成功返回 0 或文件描述符,失败返回 -1)。这种设计意味着你无法仅通过返回值判断调用是否成功。
那么如何判断是否成功呢?
通常的做法是:
1. 先调用 setfsuid
/setfsgid
。
2. 然后立即再次调用 setfsuid
/setfsgid
,传入同一个 ID。
3. 如果两次返回的旧 ID 相同,说明第一次调用成功了;如果不同,则说明第一次调用失败。
调用失败的情况:
调用通常会成功,但在某些安全模型下或传入无效 ID 时可能会失败。不过,失败的具体条件比较复杂,实践中很少遇到。
6. 相似函数或关联函数
setuid
/seteuid
/setreuid
/setresuid
: 用于设置进程的真实、有效、保存的用户 ID。这些会影响所有权限检查,而不仅仅是文件系统权限。setgid
/setegid
/setregid
/setresgid
: 用于设置进程的组 ID。getuid
/geteuid
/getgid
/getegid
: 获取进程的真实和有效 UID/GID。capset
/capget
: 用于设置和获取进程的能力 (capabilities),这是 Linux 中更细粒度的权限控制机制,可以作为setuid
/setfsuid
的现代替代方案。- 文件系统权限检查: 内核在
open
,read
,write
,stat
等系统调用时执行的检查。
7. 示例代码
#define _GNU_SOURCE // 启用 GNU 扩展以使用 getresuid 等
#include <stdio.h>
#include <unistd.h>
#include <sys/fsuid.h>
#include <sys/types.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
int main() {
uid_t ruid, euid, suid;
gid_t rgid, egid, sgid;
uid_t old_fsuid, new_fsuid;
gid_t old_fsgid, new_fsgid;
printf("--- Demonstrating setfsuid and setfsgid ---\n");
// 1. 获取当前进程的各种 ID
if (getresuid(&ruid, &euid, &suid) == -1 ||
getresgid(&rgid, &egid, &sgid) == -1) {
perror("getresuid/getresgid");
exit(EXIT_FAILURE);
}
printf("Initial IDs:\n");
printf(" Real UID: %d\n", ruid);
printf(" Effective UID: %d\n", euid);
printf(" Saved UID: %d\n", suid);
printf(" Real GID: %d\n", rgid);
printf(" Effective GID: %d\n", egid);
printf(" Saved GID: %d\n", sgid);
printf(" (By default, FS-UID and FS-GID equal Effective UID/GID)\n");
// 2. 演示 setfsuid
printf("\n--- Testing setfsuid ---\n");
// 获取当前 FS-UID (通过调用 setfsuid 并传入当前 EUID)
old_fsuid = setfsuid(euid);
printf(" Current FS-UID (queried): %d\n", old_fsuid);
// 尝试将 FS-UID 设置为一个不同的值,例如 euid + 1 (如果存在)
// 注意:这只是演示,实际是否有效取决于系统中是否存在该 UID
// 并且权限检查仍然基于文件系统权限
new_fsuid = (euid == 0) ? 1 : euid - 1; // 简单地选择一个不同的 UID
printf(" Attempting to set FS-UID to: %d\n", new_fsuid);
uid_t result1 = setfsuid(new_fsuid);
printf(" setfsuid(%d) returned: %d (should be the old FS-UID: %d)\n", new_fsuid, result1, old_fsuid);
// 验证是否设置成功:再次调用 setfsuid 获取当前值
uid_t result2 = setfsuid(new_fsuid);
printf(" Verifying: setfsuid(%d) again returned: %d\n", new_fsuid, result2);
if (result1 == result2) {
printf(" -> FS-UID was successfully set to %d.\n", new_fsuid);
} else {
printf(" -> FS-UID setting might have failed.\n");
}
// 3. 演示 setfsgid (逻辑同上)
printf("\n--- Testing setfsgid ---\n");
old_fsgid = setfsgid(egid);
printf(" Current FS-GID (queried): %d\n", old_fsgid);
new_fsgid = (egid == 0) ? 1 : egid - 1; // 选择一个不同的 GID
printf(" Attempting to set FS-GID to: %d\n", new_fsgid);
gid_t result3 = setfsgid(new_fsgid);
printf(" setfsgid(%d) returned: %d (should be the old FS-GID: %d)\n", new_fsgid, result3, old_fsgid);
gid_t result4 = setfsgid(new_fsgid);
printf(" Verifying: setfsgid(%d) again returned: %d\n", new_fsgid, result4);
if (result3 == result4) {
printf(" -> FS-GID was successfully set to %d.\n", new_fsgid);
} else {
printf(" -> FS-GID setting might have failed.\n");
}
// 4. 尝试一个实际的文件操作来“感受” FS-UID/GID 的影响
// 注意:这个例子的权限检查结果可能不会明显变化,
// 因为我们没有真正切换到一个权限不同的用户上下文,
// 也没有访问一个严格基于 FS-UID/GID 权限的特殊文件系统。
printf("\n--- Attempting file operation to observe behavior ---\n");
const char *test_file = "test_fsuid_file.txt";
int fd;
// 创建一个测试文件
fd = open(test_file, O_CREAT | O_WRONLY | O_TRUNC, 0644);
if (fd != -1) {
write(fd, "test", 4);
close(fd);
printf(" Created test file '%s'.\n", test_file);
} else {
perror(" Creating test file");
}
// 尝试读取文件 (应该成功,因为我们没有真正改变有效权限)
fd = open(test_file, O_RDONLY);
if (fd != -1) {
printf(" Successfully opened '%s' for reading (as expected).\n", test_file);
close(fd);
} else {
perror(" Opening test file for reading");
}
// 清理测试文件
if (unlink(test_file) == -1) {
perror(" Deleting test file");
} else {
printf(" Deleted test file '%s'.\n", test_file);
}
printf("\n--- Important Notes ---\n");
printf("1. setfsuid/setfsgid primarily affect filesystem permission checks.\n");
printf("2. They do not change effective UID/GID for other operations.\n");
printf("3. Their main use is in system daemons like NFS servers.\n");
printf("4. The return value is the OLD fsid, success is verified by calling again.\n");
return 0;
}
编译和运行:
# 假设代码保存在 fsuid_fsgid_example.c 中
gcc -o fsuid_fsgid_example fsuid_fsgid_example.c
# 运行程序 (普通用户权限即可)
./fsuid_fsgid_example
# 你也可以用 sudo 运行,看看 root 权限下的行为
# sudo ./fsuid_fsgid_example
预期输出 (作为普通用户运行):
--- Demonstrating setfsuid and setfsgid ---
Initial IDs:
Real UID: 1000
Effective UID: 1000
Saved UID: 1000
Real GID: 1000
Effective GID: 1000
Saved GID: 1000
(By default, FS-UID and FS-GID equal Effective UID/GID)
--- Testing setfsuid ---
Current FS-UID (queried): 1000
Attempting to set FS-UID to: 999
setfsuid(999) returned: 1000 (should be the old FS-UID: 1000)
Verifying: setfsuid(999) again returned: 999
-> FS-UID was successfully set to 999.
--- Testing setfsgid ---
Current FS-GID (queried): 1000
Attempting to set FS-GID to: 999
setfsgid(999) returned: 1000 (should be the old FS-GID: 1000)
Verifying: setfsgid(999) again returned: 999
-> FS-GID was successfully set to 999.
--- Attempting file operation to observe behavior ---
Created test file 'test_fsuid_file.txt'.
Successfully opened 'test_fsuid_file.txt' for reading (as expected).
Deleted test file 'test_fsuid_file.txt'.
--- Important Notes ---
1. setfsuid/setfsgid primarily affect filesystem permission checks.
2. They do not change effective UID/GID for other operations.
3. Their main use is in system daemons like NFS servers.
4. The return value is the OLD fsid, success is verified by calling again.