access系统调用及示例

我们继续学习 Linux 系统编程中的重要函数。这次我们介绍 access 函数,它用于检查调用进程是否对指定的文件路径具有特定的访问权限(如读、写、执行)或检查文件是否存在。


1. 函数介绍

access 是一个 Linux 系统调用,用于根据调用进程的实际用户 ID (UID) 和组 ID (GID) 来检查对文件的权限。它回答了这样的问题:“我(当前运行这个程序的用户)能否读/写/执行这个文件?” 或者更简单地,“这个文件存在吗?”。

这在程序需要在尝试打开或执行文件之前,先确认是否具备相应权限时非常有用,可以避免因权限不足而导致后续操作(如 openexecve)失败。

需要注意的是,access 检查的是调用 access 时的实际权限,即使程序后续通过 setuid 或 setgid 改变了有效用户 ID 或组 ID,access 仍然基于最初的 UID/GID 进行检查。


2. 函数原型

#include <unistd.h> // 必需

int access(const char *pathname, int mode);

3. 功能

  • 权限检查: 检查调用进程对由 pathname 指定的文件是否拥有 mode 参数指定的访问权限。
  • 存在性检查: 特别地,当 mode 设置为 F_OK 时,access 仅检查文件是否存在,而不关心具体的读/写/执行权限。

4. 参数

  • const char *pathname: 指向一个以空字符 (\0) 结尾的字符串,该字符串包含了要检查权限的文件或目录的路径名。这可以是相对路径或绝对路径。
  • int mode: 指定要检查的权限类型。这是一个位掩码,可以是以下值的按位或组合:
    • F_OK: 检查文件是否存在。
    • R_OK: 检查文件是否可读。
    • W_OK: 检查文件是否可写。
    • X_OK: 检查文件是否可执行。
      例如:
    • F_OK: 仅检查文件是否存在。
    • R_OK: 检查文件是否可读。
    • R_OK | W_OK: 检查文件是否可读且可写。
    • X_OK: 检查文件(或目录)是否可执行(对于目录,可执行意味着可以进入该目录)。

5. 返回值

  • 成功时 (具备指定权限或文件存在): 返回 0。
  • 失败时 (不具备指定权限或文件不存在):
    • 返回 -1,并设置全局变量 errno 来指示具体的错误原因:
      • EACCES: 请求的权限被拒绝。文件存在,但调用进程没有指定的权限。
      • ENOENT: 文件不存在(或路径名指向的目录不存在)。
      • ELOOP: 解析 pathname 时遇到符号链接环。
      • 其他错误…

6. 相似函数,或关联函数

  • statlstatfstat: 这些函数可以获取文件的详细状态信息,包括权限位 (st_mode)。程序可以手动检查这些权限位来判断权限,但这需要自己实现权限检查逻辑(考虑用户、组、其他用户的权限位以及 UID/GID)。access 提供了更直接、符合系统安全策略的检查方式。
  • openexecve 等: 这些函数在执行时也会进行权限检查。使用 access 可以提前检查,但需要注意“检查与使用之间存在竞争条件 (TOCTOU)”的问题(见下方注意事项)。
  • euidaccess / eaccess: 这些是 GNU 扩展函数,它们根据有效用户 ID (EUID) 和有效组 ID (EGID) 进行检查,而不是实际用户 ID。在 setuid/setgid 程序中可能更有意义。

7. 示例代码

示例 1:基本的文件存在性和权限检查

这个例子演示了如何使用 access 检查文件是否存在、是否可读、是否可写、是否可执行。

#include <unistd.h>  // access
#include <stdio.h>   // perror, printf
#include <stdlib.h>  // exit

void check_access(const char *pathname) {
    printf("\n--- Checking access for '%s' ---\n", pathname);

    // 1. 检查文件是否存在
    if (access(pathname, F_OK) == 0) {
        printf("  File exists.\n");
    } else {
        if (errno == ENOENT) {
            printf("  File does NOT exist.\n");
        } else {
            perror("  access F_OK failed for other reason");
        }
        // 如果文件不存在,后续检查无意义,但为了演示,我们仍进行
        // (实际上,通常会在这里 return)
    }

    // 2. 检查是否可读
    if (access(pathname, R_OK) == 0) {
        printf("  File is readable.\n");
    } else {
        if (errno == EACCES) {
            printf("  File exists but is NOT readable.\n");
        } else if (errno == ENOENT) {
            printf("  File does not exist (so not readable).\n");
        } else {
            perror("  access R_OK failed for other reason");
        }
    }

    // 3. 检查是否可写
    if (access(pathname, W_OK) == 0) {
        printf("  File is writable.\n");
    } else {
        if (errno == EACCES) {
            printf("  File exists but is NOT writable.\n");
        } else if (errno == ENOENT) {
            printf("  File does not exist (so not writable).\n");
        } else {
            perror("  access W_OK failed for other reason");
        }
    }

    // 4. 检查是否可执行
    if (access(pathname, X_OK) == 0) {
        printf("  File is executable.\n");
    } else {
        if (errno == EACCES) {
            printf("  File exists but is NOT executable.\n");
        } else if (errno == ENOENT) {
            printf("  File does not exist (so not executable).\n");
        } else {
            perror("  access X_OK failed for other reason");
        }
    }
}

int main(int argc, char *argv[]) {
    if (argc < 2) {
        fprintf(stderr, "Usage: %s <file1> [file2] ...\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    // 对每个命令行参数进行检查
    for (int i = 1; i < argc; i++) {
        check_access(argv[i]);
    }

    return 0;
}

代码解释:

  1. 定义了一个 check_access 函数,它接受一个文件路径作为参数。
  2. 在 check_access 函数内部:
    • 首先调用 access(pathname, F_OK) 检查文件是否存在。
    • 然后分别调用 access(pathname, R_OK)access(pathname, W_OK)access(pathname, X_OK) 检查读、写、执行权限。
    • 每次调用后都检查返回值。如果返回 0,表示检查通过;如果返回 -1,则检查 errno 来区分是“文件不存在”还是“权限不足”等其他原因。
  3. main 函数遍历所有命令行参数,并对每个参数调用 check_access

编译和运行:

gcc -o check_access check_access.c
touch test_file
chmod 644 test_file # rw-r--r--
chmod 755 test_script.sh # 创建一个可执行脚本用于测试
echo '#!/bin/bash\necho "Hello from script"' > test_script.sh
chmod +x test_script.sh

./check_access test_file test_script.sh /etc/passwd /nonexistent_file

示例 2:在打开文件前进行检查

这个例子展示了如何在尝试打开文件进行写入之前,先使用 access 检查文件是否存在以及是否可写,以提供更友好的错误信息。

#include <unistd.h>  // access
#include <fcntl.h>   // open, O_WRONLY, O_CREAT, O_EXCL
#include <stdio.h>   // perror, printf
#include <stdlib.h>  // exit

int safe_write_file(const char *pathname, const char *data) {
    int fd;

    // 1. 检查文件是否存在
    if (access(pathname, F_OK) == 0) {
        printf("File '%s' already exists.\n", pathname);

        // 2. 如果存在,检查是否可写
        if (access(pathname, W_OK) != 0) {
            if (errno == EACCES) {
                fprintf(stderr, "Error: Permission denied. Cannot write to '%s'.\n", pathname);
            } else {
                perror("Error checking write permission");
            }
            return -1; // Failure
        }
        printf("File exists and is writable.\n");
        // 注意:即使可写,open 时仍可能因为其他原因失败(如磁盘满)

    } else {
        // 文件不存在,检查目录是否可写 (间接判断能否创建文件)
        // 这里简化处理,实际可能需要解析路径
        printf("File '%s' does not exist. Checking if we can create it...\n", pathname);
        // 一个简单的检查:检查当前目录是否可写
        if (access(".", W_OK) != 0) {
             if (errno == EACCES) {
                 fprintf(stderr, "Error: Permission denied. Cannot create file in current directory.\n");
             } else {
                 perror("Error checking current directory write permission");
             }
             return -1;
        }
        printf("Current directory is writable. Proceeding to create file.\n");
    }

    // 3. 尝试打开文件进行写入
    // 使用 O_CREAT | O_EXCL 确保仅在文件不存在时创建,防止覆盖
    // 如果前面检查了存在性,这里可能用 O_WRONLY | O_TRUNC 更合适
    // 这里演示结合检查的逻辑
    if (access(pathname, F_OK) == 0) {
        // 文件存在,以只写和截断模式打开
        fd = open(pathname, O_WRONLY | O_TRUNC);
    } else {
        // 文件不存在,创建它
        fd = open(pathname, O_WRONLY | O_CREAT | O_EXCL, 0644);
    }

    if (fd == -1) {
        perror("open");
        return -1; // Failure
    }

    printf("File '%s' opened successfully for writing.\n", pathname);

    // 4. 写入数据 (简化)
    ssize_t data_len = 0;
    const char *p = data;
    while (*p++) data_len++;
    
    if (write(fd, data, data_len) != data_len) {
        perror("write");
        close(fd);
        return -1;
    }

    printf("Successfully wrote data to '%s'.\n", pathname);

    // 5. 关闭文件
    if (close(fd) == -1) {
        perror("close");
        return -1;
    }

    return 0; // Success
}

int main() {
    const char *filename = "output_from_safe_write.txt";
    const char *content = "This is data written by the safe_write_file function.\n";

    if (safe_write_file(filename, content) == 0) {
        printf("Operation completed successfully.\n");
    } else {
        printf("Operation failed.\n");
        exit(EXIT_FAILURE);
    }

    return 0;
}

代码解释:

  1. 定义了一个 safe_write_file 函数,它接受文件名和要写入的数据。
  2. 首先使用 access(pathname, F_OK) 检查文件是否存在。
  3. 如果文件存在,再使用 access(pathname, W_OK) 检查是否可写。
  4. 如果文件不存在,则检查当前工作目录(.)是否可写,以此判断是否有权限创建新文件(这是一个简化的检查)。
  5. 根据检查结果,决定是以 O_WRONLY | O_TRUNC(覆盖)还是 O_WRONLY | O_CREAT | O_EXCL(新建)模式打开文件。
  6. 打开文件后,执行写入操作。
  7. 最后关闭文件。
  8. 通过这种方式,可以在实际执行可能导致失败的操作(openwrite)之前,提供更具体、更早的错误反馈。

重要注意事项:TOCTOU 竞争条件

使用 access 时需要特别注意一个潜在的安全问题:TOCTOU (Time-of-Check to Time-of-Use) 竞争条件

  • 问题access 检查权限和后续使用文件(如 openexecve)之间存在时间差。在这段时间内,文件的权限或存在性可能被其他进程改变。
  • 例子: 一个程序用 access("myfile", W_OK) 检查 myfile 是否可写,返回 0(表示可写)。但在程序调用 open("myfile", O_WRONLY) 之前,另一个有权限的进程删除了 myfile 并创建了一个指向敏感文件(如 /etc/passwd)的符号链接,并命名为 myfile。此时,程序的 open 调用将会打开并可能修改 /etc/passwd,这显然不是预期行为。
  • 缓解方法:
    1. 尽量避免使用 access: 最好的方法是直接尝试执行操作(如 openexecve),并根据其返回的错误码来处理权限或存在性问题。内核会在 open/execve 时进行原子性的权限检查。
    2. 如果必须使用 access: 要意识到这种风险,并确保在权限检查和文件使用之间的时间窗口尽可能短。在高安全性要求的程序中,应避免依赖 access 的结果来做关键决策。

总结:

access 函数提供了一种方便的方式来检查文件权限和存在性。虽然它有其用途,但在涉及安全性的场景中,直接尝试操作并处理错误通常是更安全、更可靠的做法。理解其工作原理和潜在的 TOCTOU 问题是正确使用它的关键。

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

access系统调用及示例》有一条回应

  1. Pingback引用通告: io_cancel系统调用及示例 - LinuxGuideLinuxGuide

发表回复

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