setfsuid系统调用及示例

我们来深入学习 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 的现代替代方案。
  • 文件系统权限检查: 内核在 openreadwritestat 等系统调用时执行的检查。

7. 示例代码

由于 setfsuid/setfsgid 的使用场景比较特殊(主要是像 NFS 这样的服务器程序),编写一个完全展示其效果的用户态程序比较困难,因为它需要特定的文件系统环境和权限设置。下面的示例将演示如何调用它们,并尝试解释其行为。

#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.

总结:
对于 Linux 编程新手,setfsuid 和 setfsgid 是比较特殊的系统调用。它们不是日常编程中会频繁使用的。理解它们有助于理解 Linux 权限模型的细节,特别是文件系统权限检查是如何与进程的其他权限分离的。在编写需要代表不同用户执行文件操作的系统服务(如文件服务器)时,它们会很有用。在常规应用程序开发中,使用标准的 setuid/seteuid 或现代的 capabilities 通常更合适。

此条目发表在linux文章分类目录。将固定链接加入收藏夹。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注