17 ACL setfacl getfacl

ACL 介绍

ACL 在 Linux 内核 2.6 被支持,是用文件的 扩展属性 实现的,扩展属性名为 system.posix_acl_access

要想在 ext2、ext3、ext4 或 reiserfs 文件系统上创建 ACL,装配相应的文件系统时需要带 mount –o acl 选项。

ACL 的功能是传统访问权限的超集,优先于传统访问权限生效。每个 ACL 项目(Access Control Entry,即 ACE)指定一个 rwx(3 个二进制位)的权限。其中:

  1. ACL_USER_OBJACL_GROUP_OBJACL_OTHER(在没有指定其他项目时)相当于传统权限,这个时候 ACL 被称为最小 ACL
  2. 支持对特定用户(ACL_USER)或者特定组(ACL_GRUOP)指定权限。
  3. 支持掩码。ACL_MASK 记录了可由 ACL_USERACL_GROUP_OBJ 以及 ACL_GROUP 型 ACE 所能授予的最高权限。但如果 ACL 含有标记类型为 ACL_USERACL_GROUP 的记录,那么就必须包含一条 ACL_MASK 型的 ACE。如果包含了掩码项目,ACL 就成为扩展 ACL
  4. ACL_USER_OBJACL_GROUP_OBJACL_OTHERACL_MASK 都只能出现一次。

ACL 权限匹配算法

ACL 的权限检查同样是具有短路特性的。

  1. 如果是特权进程,则放行。(实际实现的时候可能会把这一步放在最后作为 fallback,因为系统审计程序只应该记录进程真正需要使用到特权的情况。)
  2. 按照用户检查,如果进程有效用户 ID 和 ACL_USER_OBJ 匹配,则按照这一项检查权限。
  3. 否则,如果进程有效用户 ID 和某个 ACL_USER 匹配,则将它和 ACL_MASK 求与作为权限结果。
  4. 否则,如果进程的有效组 ID 或者辅助组 ID 之一满足全部权限检查,则放行。
    1. 如果组 ID 匹配 ACL_GROUP_OBJ,则将其和 ACL_MASK 求与(如果没有 ACL_MASK 则跳过这一步)。
    2. 如果组 ID 匹配 ACL_GRUOP,则将其和 ACL_MASK 求与(此时 ACL_MASK 必定存在)。
  5. 否则,按照 ACL_OTHER 检查权限(ACL_MASK 不会影响这一项)。

第 4 点的全部权限检查举例:如果有一个进程对应了两个组 A 和 B,且在 ACL 中,A 对文件有读权限,B 对文件有写权限,那么进程单独申请读权限或者单独申请写权限都是能够通过检查的,但是进程申请读写权限时会失败。

ACL 的文本格式

ACL 具有长文本格式和短文本格式。长文本格式是 ACE 用换行符连接,并且可以包含 # 开头的注释;短文本格式是 ACE 用逗号连接。

ACL 的组成是 tag:[tag-qualifier]:permissions(标记类型、标记限定符、权限集合)。当 tag 为用户或者组时,不提供限定符则表示文件所属用户或者所属组,提供限定符(用户名或者组名)则表示给定的用户或者组。一些例子:

命令 setfaclgetfacl

Debian 需要额外安装 acl 包。

每次用 setfacl 操作 ACL 但是又没有指明 mask 规则时,mask 会被自动调整成 ACL_USERACL_GROUP_OBJ 以及 ACL_GROUP 型 ACE 的并集。想要禁用这个行为,需要传入参数 -n / --no-mask

ACL_MASK 的意义以及传统权限与 ACL 的同步

很多应用不知道 ACL 的存在,会使用 chmod 等系统调用来修改权限,比如用 chmod 修改权限为 0700(开头的 0 表示是八进制数),表示除了文件主人之外都没有访问权限,这个时候如果没有 ACL_MASK,就需要把所有特定用户和特定组的权限都改成 0 以阻止它们访问。但是这样做又有一个问题,将来用 chmod 改权限为 0777 的时候,以前设置的特定用户、特定组的规则都丢失了,难道还要给它们都设置成 0777 吗?

在有 ACL_MASK 的时候(回忆第一节的内容,一旦有特定用户规则或者特定组规则,这个掩码就一定存在):

  1. 对传统的组权限(就是 user/group/other 的 group)的修改会反映在 ACL_MASK 而不是 ACL_GROUP_OBJ 上。
  2. stat 系统调用返回的 st_mode 中的 group 对应位置替换为 ACL_MASK 的二进制表示,而不是 inode 中存储的组权限。如果用 ls -l 去显示文件权限,权限串会以 + 结尾,表示当前 ACL 是扩展 ACL,而且 group 权限显示的是 ACL_MASK(因为 stat 返回的数据就是这样)。
  3. 可以用 getxattr 去获得文件的 system.posix_acl_access 扩展属性。(如果没有 ACL_MASK,那么 ACL 是最小 ACL,这个时候去访问会出现 no data available 的错误。)但是因为我不知道如何反序列化它,拿到数据之后也不知道怎么解读,可能得看内核源码去寻找答案

传统权限与 ACL 的同步方式如下:

  1. ACL_MASK 时 ,ACL 是最小 ACL,user 权限对应 ACL_USER_OBJ,group 权限对应 ACL_GROUP_OBJ,other 权限对应 ACL_OTHERchmodsetfacl 操作以上权限都只会修改 inode 文件权限,而扩展属性不存在、也不会被创建。
  2. ACL_MASK 时,ACL 是扩展 ACL,inode 文件权限和文件扩展属性中的 ACL 信息是两份独立数据,但是 chmodsetfacl 在更新 user 和 other 权限时会相互同步。Group 权限比较特殊,它对应于 ACL_MASKchmod 对组权限的修改会直接修改这个,而不影响 inode 模式(st_mode)中的权限,由于操作对象是同一个所以不需要同步。setfaclACL_GROUP_OBJ 的修改还是会同步到 inode 模式的组权限上。

有个小问题:假设文件的 ACL 是 user::rw-,group::---,mask::---,other::r--,对该文件使用 chmod g+rw,ACL 会变成 user::rw-,group::---,mask::rw-,other::r--,由于不能真正改变组权限且 ACL 的优先级高于 inode 模式位,此时文件同组不同用户仍然无法访问文件。要访问文件只能用 setfacl 修改组权限,或者删除掩码(这个时候没有特定用户或特定组的规则,可以删除掩码)。


用以下操作证明文件的 ACL 是扩展 ACL 时,EA 中会 user 权限和 group 权限的冗余数据并保持同步,而不是直接透明修改 inode 模式位:

(py310) xxx /data/ $ diff <(./build/main < A.txt) <(./build/main < B.txt)
# 此时刚刚用 setfacl -m m::rwx 给 A.txt 和 B.txt 分别创建了 ACL_MASK 类型的 ACE
(py310) xxx /data/ $ getfacl A.txt 
# file: A.txt
# owner: simon
# group: simon
user::rw-
group::r--
mask::rwx
other::r--

(py310) xxx /data/ $ setfacl -m u::rw A.txt 
(py310) xxx /data/ $ diff <(./build/main < A.txt) <(./build/main < B.txt)
Binary files /dev/fd/63 and /dev/fd/62 differ

A.txt 的用户权限已经是 rw,现在用 setfacl 设置同样的权限,A、B 两个文件的扩展属性有差异,说明 setfacl 本身是存储了一份信息的,而不是透明映射到 inode 文件权限中去。

build/main 的代码如下,它直接以二进制形式输出标准输入流对应文件的扩展属性:

#include <stdio.h>
#include <sys/xattr.h>

int main() {
    char buf[256] = {0};
    ssize_t sz = getxattr("/proc/self/fd/0", "system.posix_acl_access", buf, sizeof(buf));
    if (sz == -1) {
        perror("getxattr");
        return 1;
    }
    for (ssize_t i = 0; i < sz; ++i) {
        putchar(buf[i]);
    }
}

默认型 ACL(相当于 umask)

之前介绍的 ACL 都是访问型的(从 EA 名的命名方式可以看出来),其实还有一种 ACL,即默认型 ACL,对应的 EA 名为 system.posix_acl_default。这种 ACL 只能针对目录设置,且目录下新创建的子目录会继承默认型 ACL,目录下新创建的子目录和子文件会将默认型 ACL 作为自己的访问型 ACL。默认型 ACL 的作用有点相当于进程的 umask,而且它存在时 umask 会被忽略。

如果目录有默认型 ACL,其下用 mkdir 或者 open 等系统调用创建的新的文件会和默认型 ACL 发生按位与操作:

  1. ACL_USER_OBJ 对应 umask 的 user 部分。
  2. ACL_MASK(若不含 ACL_MASK,则为 ACL_GROUP_OBJ)对应 umask 的 group 部分。
  3. ACL_OTHER 对应 umask 的 other 部分。

想要用 getfaclsetfacl 来查看 / 修改默认型 ACL,需要加 -d 参数;想要用 setfacl 删除默认型 ACL,需要加 -k 参数。

ACL 的数量限制

单个文件的 ACL 记录长度受制于 文件系统扩展属性的长度。在 ext2、ext3 以及 ext4 文件系统中,由于单个文件扩展属性的所有信息不能超过一个逻辑磁盘块大小,因此 ACL 的数量还是很有限的。有些文件系统比如 JFS 限制就要宽松很多。

TLPI 作者的观点是:ACL 数量非常有限,而且内容长了也不便于维护,最好用组的形式去管理权限,而少用特定用户的 ACL 项(ACE)。

ACL API

ACL 的 API 非常多,使用前需要包含 <sys/acl.h>。如果还用到了 POSIX.1e 标准草案中的各种 Linux 扩展(acl(5) 手册页罗列了一系列 Linux 扩展),程序可能还需要包含 <acl/libacl.h>,此时还需要加上 -lacl 选项以完成链接。为了能在 man pages 看到 acl 相关 API 的说明,需要先安装 libacl(在 Debian 上是 libacl1-dev)。

最核心的两个类型是 acl_tacl_entry_t,其中前者被实现为指针类型。

还有一些 ACL 相关的调用没有在图中出现。比如:

  • acl_calc_mask(&acl) 用来给 acl_t 类型的 acl 重新计算 ACL_MASK 型记录(如果必要)为 ACL_USERACL_GROUP 以及 ACL_GROUP_OBJ 型记录的权限并集。
  • acl_valid(acl) 用来检查 acl 的有效性。
  • acl_delete_def_file(pathname) 用来删除目录的默认 ACL。
  • acl_init(count) 创建一个空的 ACL 结构。
  • acl_dup(acl)acl 创建副本。
  • acl_free(handle) 释放 ACL 相关 API 分配的内存。