Linux2.6内核 ACL 机制数据结构和实现分析

http://zhumeng8337797.blog.163.com/blog/static/1007689142010111510214850/


Abstract: This paper makes an analysis on ACL's data structure and implements on Linux 2.6 kernel, which includes

 data structure in abstract layer and EXT4 filesystem layer. It will also focus on the inode's access control algorithms

 and acl's role or function in these algorithms. At last, this paper will describe the procedure of acl control with an 

illustration of open system call.

Key words :ACL; Linux 2.6 kernel; EXT4; Access control algorithm; Open system call

 

 

摘 要 :本文对Linux2.6 内核的 ACL 机制的数据结构和实现进行分析,包括有抽象标准层面上的 ACL 数据结构以及其在 EXT4 具体文件系统层面上的数据结构。本文还将着重分析 Linux 中对节点的访问权限检查算法以及 ACL 访问控制机制在其中的位置与作用。本文最后将以 Open 系统调用为例说明 ACL 总体控制流程。

关键词 :ACL 、 Linux 2.6 内核、 EXT4 、访问权限检查、 Open 系统调用

 

绪 论 
自从1991 年 9 月 17 日 Linux v0.01 版本发布以来,众多的开源组织和个人加入到 Linux 的开发和完善中, 2001 年 1 月 4 日推出 v2.4 版本, 2003 年 12 月 17 日又推出 v2.6 版本。版本的升级带来的是性能的提升和功能的完善,在目前 Linux 版本中已经包含了许多先进的技术和机制。

ACL访问控制机制就是其中的一种,它让人们摆脱了继承自 Unix 的 user/group/other 粗粒度访问控制模式带来的不便,使得人们可以更加自由的控制文件的访问权限。有关于 ACL 标准的描述在 IEEE 的 Posix1003.1e 草案中, ACL 机制的实现在 Linux2.4 内核上是以补丁的方式存在,而在 Linux2.6 内核则以标准功能实现。 Linux 提供了 setfacl 和 getacl 等命令供用户对 ACL 进行设置,而关于如何在程序中应用 ACL 则没有给出相应的规范,尽管目前也有不少开源小组在开发各种 libacl-devel 库来支持 ACL 编程。总的来说,目前对于 ACL 机制的实现和开发编程方面的研究还很少。

本文将分为四个部分对Linux2.6 内核 ACL 机制的数据结构和实现进行分析:

第一部分:简单介绍ACL 命令的用法和其访问控制机制的特点。

第二部分:分析抽象层面上的ACL 数据结构,包括有 Posix ACL 数据结构分析和 Posix 标准中对 ACL 的各种操作,并重点分析抽象层面上 ACL 权限检查算法。

第三部分:分析EXT4 文件系统中 ACL 的数据结构和存储方式,并说明其与抽象 ACL 相互转化方式,如如何从外存中读取 ACL 属性到内存中,以及如何将内存中的 ACL 写入外存中。

第四部分:说明VFS 的基本原理,并从整体流程上阐述 ACL 访问控制的流程。另外分析了与 ACL 相关的系统调用,以及某些实现。

为了对ACL 机制有个完整的了解,本文的分析中会涉及到一些 Linux 2.4 和 2.6 内核的特性,以及 EXT4 文件系统的基本结构。

 

1.  ACL简介 
在ACL 机制出现以前,人们都是通过 user/group/other 模式来实现文件的访问控制权限的设置。 Linux 和 Unix 使用 9 个比特来表示这种文件的访问模式,如 rwxr-xr-- 就表示文件的属主拥有对文件的读写执行权限,文件属组中的成员对文件拥有读和执行权限,其他人只有读的权限。这种访问模式简单高效,对于早期简单的应用十分有效。但是随着人们对安全特性的要求提高,这种方式已经不能够满足现代的安全需求。

一个很简单的例子就是如何指定同组用户对文件的不同权限,如文件属于security 组,该组的成员对文件都有 r-x 权限,但是我们需要为其中的某个用户 tux 添加 w 权限。在上述模式中, Linux 管理员不得不为 tux 再添加一个组,并设置相应的权限,这样做存在许多的不便和安全隐患。如果使用 ACL 机制则可以很好的解决上述问题,用户只需要使用命令 setfacl -m user:tux:rwx file 就能够为 tux 设置对 file 的读写执行的权限。另外也可以使用 ACL 机制对文件进行负授权(或者说权限的撤销),例如使用命令  setfacl -m user:tux:--- file 就可以使 tux 对 file 没有任何权限。

使用命令getfacl file, 可以看到如下输出 :

user::rwx

user:tux:rwx

group::r-w

other::r--

mask::rwx

其中每一行都是一个ACL 实体,对应于某一条具体的访问控制规则,而所有的 ACL 实体就构成了文件的 ACL 属性。每一个 ACL 实体由三部分组成: e_tag 、 e_id 、 e_perm 。 e_tag 表示 ACL 实体的标志,如 user:tux:rwx 中 user 就是一个 e_tag 。 e_id 是 ACL 实体限制的用户或组 id, 如 user:tux:rwx 中的 tux, 在某些实体中这个项可以为空。 e_perm 说明的是具体的访问权限,主要有 rwx 三种,这和传统的 u/g/o 模式是一致的。在 Posix 标准中规定一共有 6 种 e_tag ,分别是 ACL_USER_OBJ, ACL_USER, ACL_GROUP_OBJ, ACL_GROUP, ACL_MASK, ACL_OTHER 。 ACL_USER_OBJ 是文件属主的 ACL 实体, ACL_GROUP_OBJ 是文件属组的 ACL 实体, ACL_MASK 是掩码 ACL 实体, ACL_OTHER 是其他用户的 ACL 实体。这四种 ( 个 )ACL 实体的 id 都为空,其他类型的 ACL 实体的 e_id 都不能为空。

 

2.  抽象ACL 表示 
2.1.  内存中的ACL 数据结构 
在Linux 中, ACL 是按照 Posix 标准来实现,其数据结构和 Posix 规定的 ACL 的数据是一致的。其定义在 include/linux/posix_acl.h ,实现在 fs/posix_acl.c 中:

struct posix_acl_entry { //acl_entry

  short e_tag;   //tag element,used to present user/group/other

  unsigned short e_perm;  //permission element,used to present rwx

  unsigned int e_id; //id element,used to present uid/gid

};

struct posix_acl { //file acl,witch contains lot of acl_entry

  atomic_t a_refcount; //counter of process who reference this 

  unsigned int a_count; //count erof acl_entries

  struct posix_acl_entry a_entries[0];//real acl_entries

};

首先我们来对上述数据结构作个说明:

我们在使用setfacl (或 getfacl )设置 ( 或查看 )ACL 的时候,我们通常会表示为如下结构 user:tux:rwx ,这其实就是一个 ACL 实体( posix_acl_entry )。 user 对应 e_tag ,表示 ACL 的类型。 rwx 对应的是 e_perm ,说明的是赋予的权限。 tux 对应的 e_id ,说明的是这个实体的 id, 如果 e_tag 指定的是 user 那么 e_id 表明是用户 id, 如果 e_id 指定的是 group 那么它表示的就是组 id 。采用这种策略部分原因是为了与原始的 9bit 模式兼容。

一个文件的ACL 属性由多个这样的 ACL 实体构成,描述文件的 ACL 属性就是 posix_acl 数据结构。其中的 a_refcount 指示有多少个进程在使用该 ACL (每有一个新的进程用到这个 ACL 属性,就将该计数器加一;每当一个进程不再使用这个 ACL 属性就将该计数器减一,当减到 0 后就会销毁该 ACL )。 a_count 表示这个 ACL 属性中包含多少个 ACL 实体 (posix_acl_entry) , a_entries 为内存中实际存放 ACL 项的数组。这里有一个问题:为什么要采用数组的方式实现,这样实现对实体的增删改不会很不方便吗?我们将在后面的表述中说明 Linux 为什么采用数组的方式。

2.2.  ACL与内核其他数据结构的关系 
上文中我们说到ACL 是一个文件的属性,这是我们通常的观点。在 Linux 中文件这个词的含义稍稍有些变化, File 数据结构特指与进程相关联的一个读写物理文件的上下文关联,实际代表物理文件的是另外一个数据结构 Inode 。这就是为什么你看 File 的数据结构中不包括 9bit 位以及 ACL 属性的原因,它们都是与具体读写上下文无关的属性,是属于物理文件的自身特性的数据。

那么我们来看看内存中的ACL 是如何与具体的 Inode 相关联的吧。查看 Inode 数据结构 (include/linux/fs.h) ,我们看到:

00779: #ifdef CONFIG_FS_POSIX_ACL

00780: struct posix_acl *i_acl;

00781: struct posix_acl *i_default_acl;

00782: #endif

也就是说通过i_acl 和 i_default_acl 两个 posix_acl 指针将 Inode 和具体的 ACL 属性关联起来。我们注意到有两个 ACL 属性, acl 和 default_acl ,这两个属性有不同的作用。 acl 属性是用于访问控制的,对一个文件读写执行都要通过这个 acl 属性来控制。 default_acl 属性是目录特有的 ACL 属性,在此目录中创建的文件和目录都将继承这个 default_acl 属性。(对于普通文件来说,该指针为空)。在这里我们要注意的是这两个 ACL 属性都是缓存的 ACL 的属性,在一开始的时候为空,当某个进程要用到这个 ACL 属性的时候,内核就从外存中读取 ACL 属性,并缓存到 inode 的 i_acl 和 i_default_acl 。以后其他进程需要使用到 ACL 的信息就先在内存缓存中查询。我们将在第二部分介绍如何从外存中读取 inode 的 ACL 属性。

2.3.  内存中对ACL 的操作

Linux2.6内核 ACL 机制数据结构和实现分析 - zhuzhu - 津津计较

  

下表是Posix 中规定的在内存中对 ACL 的操作,列举如下:

函数申明 
 描述 
 
posix_acl_dup() 
 实际上是将acl 引用计数加 1 
 
posix_acl_release() 
 释放内核中acl 的空间 
 
posix_acl_alloc() 
 分配一个新的ACL 
 
posix_acl_valid() 
 检查一个ACL 是否合法 
 
posix_acl_permission() 
 使用acl 检查当前进程是否对 inode 有访问权限 
 
get_posix_acl() 
 对应于具体文件系统中的操作 
 
set_posix_acl() 
 对应于具体文件系统中的操作 
 
posix_acl_equiv_mode() 
 检查acl 是否可以完全代表 9bit 模式 
 
posix_acl_create_masq() 
 创建新节点时修改其ACL 
 
posix_acl_chmod_masq() 
 当发生chmod 系统调用时修改其 ACL 
 
posix_acl_from_mode() 
 创建能够代表9bit 模式的 ACL 
 
posix_acl_clone() 
 克隆一个ACL 
 

表1.1 Posix_acl.h 中 ACL 函数

我们可以看到在内存中提供对ACL 属性的整体操作,包括引用复制,销毁,分配,校验、克隆以及权限检查等等。这些操作基本上囊括了所有对 ACL 的应用,但是我们注意到这里面并没有增删改某个具体 ACL 实体的函数。事实上关于这些具体 ACL 实体设置的库函数并没有包括在 linux 内核中,需要开发者自己实现其库函数。目前有不少开源小组在做这方面的工作,并做出了 libacl-devel 开发库。 Linux 中使用简单的 Posix_acl_xattr 来对其操作,就连 setfacl 和 getfacl 都是通过 getxattr 和 setxattr 来实现的。在这种方式下,连续地址空间就会方便 getxattr 和 setxattr 的实现,这也是 Posix_acl 采用数组的方式存放 posix_acl_entry 的原因。

Posix标准中规定了 acl 和字符串转换的函数: acl_from_text(),acl_to_text() 主要工作就是将文本格式的 acl 转化为内存中 ACL 以及将内存中的 ACL 转换为文本格式的 acl( 便于显示),这些转化都很简单,并且在内核中没有实现,我们就不再说明,有兴趣的读者可以去读读 libacl-devel 中相关的实现源码。

2.4.  ACL权限检查函数 
当我们得到了一个inode 的 ACL 属性,我们就可以检查进程是否有权限访问这个 inode 。当然了,对 Inode 访问权限的检查不只有 ACL 权限检查,在此之前还必须通过 9bit 位权限检查。一般来说,对 Inode 的权限检查是由 inode_permission() 来做的, inode_permission 里可能会包含 check_acl (如果启用了 ACL 机制)。这里我们只阐述得到 ACL 后如何检查进程是否有访问权限。其他相关东西我们将放在第三部分整体流程中介绍。

Linux内存中对 ACL 的检查是在 posix_acl_permission (定义在 fs/posix_acl.c )中完成的。其函数声明如下:

int posix_acl_permission(struct inode *inode, const struct posix_acl *acl, int want);

对于给定的节点Inode 以及给定的 acl ,判断当前进程是否有 want 权限,如果有权限返回 0 否则返回错误代码。读者可能会有如下疑问,既然可以通过 inode 得到 acl ,那么为什么还需要在传入一个 acl 指针呢。原因是 posix_acl_permission 是与具体文件系统无关的权限检查函数,而通过 Inode 获得 acl 属性的函数会因文件系统而异。为了隔离这种差异性, posix_acl_permission 需要内核先以某种方式获取 Inode 的 ACL 属性,然后调用该函数。如 Ext4 文件系统中 ext4_check_acl 就先调用 ext4_get_acl 得到 acl, 然后调用 posix_acl_permission.

现在我们来详细分析这个函数(源码见fs/posix_acl.c 的 00206~00267 行)

函数首先声明三个posix_acl_entry *pa, *pe, *mask_obj,pa 是当前检查的 acl 指针, pe 是 acl 结束指针。 mask_obj 是掩码 acl 实体(对应于如 mask::rwx 的 acl 实体)。函数下面使用了一个宏 FOREACH_ACL_ENTRY(pa, acl, pe) ,其定义在 include/linux/posix_acl.h 的 46 行:

#define FOREACH_ACL_ENTRY(pa, acl, pe) \

for(pa=(acl)->a_entries, pe=pa+(acl)->a_count; pa<pe; pa++)

我们可以看到这实际上就是一个for 循环, for 循环的主体是一个 switch 语句。按照 ACL_USER_OBJ 、 ACL_USER 、 ACL_GROUP_OBJ 、 ACL_GROUP 、 OTHER 的次序来检查 pa->e_tag 。

首先判断当前进程的是不是inode 的属主:

case ACL_USER_OBJ:

if (inode->i_uid == current_fsuid())

goto check_perm;

其中current_fsuid() 是一个宏,用来获得当前进程的 fsuid( 其定义在 include/linux/cred.h 的 316 行,关于这方面的资料见文件系统基础知识 ) 。如果是则进行文件属主的权限检查。

然后判断当前进程是不是指名用户:

case ACL_USER:

if (pa->e_id == current_fsuid())

     goto mask;

如果当前进程ID 匹配一个有名用户的 id 那么除了要判断有名用户的权限之外还要判断掩码 mask 是否允许访问权限。

再判断当前进程是否是属主组用户:

  case ACL_GROUP_OBJ:

     if (in_group_p(inode->i_gid)) {

found = 1;

if ((pa->e_perm & want) == want)

goto mask;

}

其中in_group_p 判断当前进程是否属于某个组,同样权限的判断还要通过 mask 。

再判断当前进程是否是指名组用户:

case ACL_GROUP:

if (in_group_p(pa->e_id)) {

found = 1;

if ((pa->e_perm & want) == want)

goto mask;

    }

判断同上,读者可以同理推知。

最后检查:

case ACL_MASK:

break;

case ACL_OTHER: 

if (found)

return -EACCES;

else

goto check_perm;

default:

return -EIO;

我们看到,ACL_USER 、 ACL_GROUP_OBJ 、 ACL_GROUP 这三者都要受到 mask 位的影响,而其他类型的均不受到影响,这与 Posix 标准中所规定的是一致的。下面我们分别来看看 check_perm 和 mask

mask:

for (mask_obj = pa+1; mask_obj != pe; mask_obj++) {

if (mask_obj->e_tag == ACL_MASK) {

if ((pa->e_perm & mask_obj->e_perm & want) == want)

return 0;

return -EACCES;

}

}

check_perm:

if ((pa->e_perm & want) == want)

return 0;

return -EACCES;

mask首先获得 ACL 属性中的 mask 实体,如果有就进行 mask 检查,没有的话按照正常流程检查 check_perm 。 (pa->e_perm & want) == want ,这句话表示对 e_perm 和 want 取交集,如果交集就是 want ,说明 e_perm 包含 want 集合。

至此我们已经全部分析完posix_acl_permission 这个函数,从中可知道 Linux 实现的 ACL 权限检查算法是和 Posix 标准中所规定的完全一致。

 

3.  EXT4文件系统中 ACL 表示 
在上一部分中我们分析了ACL 在内存中的存储与各种操作,但是我们知道 ACL 是物理文件系统的一个属性,需要永久保存。如何将 ACL 保存在外存中将是我们这一部分探讨的重点,包括 ACL 在外存中具体存放的位置,以及如何从外存中读取和写入原始 ACL 内容。这里要涉及到 VFS 和具体的物理文件系统,我们以最新的 EXT4 文件系统为例分析上述内容。首先我们还是要了解文件系统一些基本知识。

3.1.  文件系统基本数据结构 
每一个文件都有一个目录项dentry 表示文件所处的路径,同时还有一个 Inode 记录着文件在存储介质上位置与分布等信息,目录项中包含有文件名以及对应的 Inode 。这些 Inode 又可以分为内存中的 inode 、 dentry 以及磁盘中的 inode 、 dentry (又可以叫做为 raw inode /dentry ,在 Ext4 文件系统是称为 ext4_inode 以及 ext4_dentry )。这两种 inode 是不相同的,内存中的 inode 保存了许多的动态信息,在断电后就会丢失(易失性),而外存中的 inode 是磁盘 Inode 结构的真实反映,是需要永久保存的。他们之间的关系是内核从磁盘中读取 raw inode 并加工该信息生成内存中的 inode , inode 中也包含有 raw inode 的某些信息以便后续对底层文件系统进行各种操作。关于如何利用磁盘的 raw inode 生成内存中的 Inode 我们将在第三部分详细介绍。这里我们只需要知道在 EXT4 文件系统在每一个内存 inode 都对应一个 ext4_inode ,并且可以通过 inode 很容易找到 ext4_inode_info 的信息。

在Linux2.4 内核中 Inode 中包含有一个联合体

struct inode{

.........

union{

struct minix_inode_info minix_i;

struct ext2_inode_info ext2_i;

..........

}u;

..........

};

来表示各种文件系统的,这种表示方法过于死板,扩展性不强,且浪费了许多的空间。Linux2.6 内核中采用了一种更为优秀的设计方式 , 即在 ext4_inode_info (定义在 fs/ext4/ext4.h 中)里面包含有 inode:

00571: struct ext4_inode_info{

.........

00629: struct inode vfs_inode;

..........

00656: };

并提供一种宏EXT4_I ( inode )。利用该宏可以轻松由 inode 获得 ext4_inode_info 。这中方式不仅节省了大量的空间,还具有较强的可扩展性(添加一种的新的文件系统的时候不需要对 Inode 结构体进行任何改变)。 Linux 中大量的利用这种方式简化了系统设计,我们也将从下文中看到这种方式带给我们的巨大好处。

3.2.  EXT4文件系统磁盘结构 
我们研究的是ext4 文件系统,要了解 ACL (或者说其依附的 inode )是如何在 EXT4 文件系统存储的,我们首先得知道 EXT4 文件系统的磁盘结构。 EXT4 文件系统对 EXT3 做出了巨大的改进,引进了许多新特性,但是我们的关注点将集中于 Inode 中的 ACL 的存储。首先我们先来看看 EXT4 文件系统布局,如下图所示:

 

Linux2.6内核 ACL 机制数据结构和实现分析 - zhuzhu - 津津计较

 

图3-1 EXT3 和 EXT4 磁盘结构

ext4 中采用了元块组( metablock group )的概念。所谓元块组就是指块组描述符可以存储在一个数据块中的一些连续块组。采用元块组的概念之后,每个元块组中的块组描述符都变成定长的,这对于文件系统的扩展非常有利。原来在  ext3  中,要想扩大文件系统的大小,只能在第一个块组中增加更多块描述符,通常这都需要重新格式化文件系统,无法实现在线扩容;另外一种可能的解决方案是为块组描述符预留一部分空间,在增加数据块时,使用这部分空间来存储对应的块组描述符;但是这样也会受到前面介绍的最大容量的限制。而采用元块组概念之后,如果需要扩充文件系统的大小,可以在现有数据块之后新添加磁盘数据块,并将这些数据块也按照元块组的方式进行管理即可,这样就可以突破文件系统大小原有的限制了。当然,为了使用这些新增加的空间,在  superblock  结构中需要增加一些字段来记录相关信息。【 1 】

整个磁盘分为引导区,超级块区,数据区。数据区分为若干个元块组,每个元块组包括64 个块组。每个块组包含有如下信息:超级块,组描述块,块位图块, Inode 位图块 ,Inode 表,数据块。超级块是整个磁盘超级块的复制( ext4_super_block ),组描述块包含了整个组的描述信息( ext4_group_desc )。块位图是 1 个 block 的位串,每一位代表相应的块的使用情况。 Inode 位图块也是 1 个 block 的位串,每一位代表相应的 inode 的使用情况。 Inode 表是若干个 block ,包含有所有的 Inode 。数据块是块组剩余的部分,用于存放实际数据。

3.3.  ACL属性存储实现 
在 Linux 操作系统中,如果libattr 功能在内核设置中被打开, ext2 、 ext3 、 ext4 、 JFS 、 ReiserFS 以及 XFS 文件系统都支持扩展属性(英文简写为xattr )。任何一个普通文件都可能包含有一系列的扩展属性。每一个属性由一个名字以及与之相关联的数据所表示。其中名字必须为一个 字符串 ,并且必须有一个 命名空间 前缀标识符与一个点字符。目前存在有四种命名空间:用户命名空间、信任命名空间、安全命名空间以及系统命名空间。用户命名空间在命名或者内容上没有任何限制。系统命名空间主要被内核用于 访问控制表 上。目前Linux 的 ACL 存储实现就是基于这种扩展属性的。

首先我们先来看看Inode 表中的 Inode 结构,注意到这里是具体的文件系统, inode 实际指的是 ext4_inode 。另外我们还需要关注 ext4_inode_info ,它是把 Inode 装载入内存中时动态生成的关于 inode 的信息。 Ext_inode_info 中有一项 i_state, 如果其中的 EXT4_STATE_XATTR 被设置了表明 inode 的 ACL 信息就在存放在 ibody 体内,否者表示 ACL 信息存放在另外的数据块中。

我们有必要了解Inode 表中 inode 是如何存放的。其结构如下图所示:

 

Linux2.6内核 ACL 机制数据结构和实现分析 - zhuzhu - 津津计较

 

图3-2  使用扩展属性存储的 ACL

inode Table中保存有若干个 Ext_inode ,每个 Inode 大小为 ext4_super_block 中指定的 s_inode_size, 然而一个 Inode 不一定用到这么多的大小,节点信息只用到 128 个字节的空间。剩下的部分作为扩展文件属性 (Xattr),Ext4_inode_info 中有一项 i_extra_isize 指定了这个扩展属性的大小,当然了原始的 128 加上这个 i_extra_isize 必须得小于等于 s_inode_size 。扩展属性内部是由一个扩展属性头和若干个扩展属性实体项构成的。代码如下(定义在 fs/ext4/xattr.h 的 00037~00045 行):

struct ext4_xattr_entry {

__u8 e_name_len; /* length of name */

__u8 e_name_index; /* attribute name index */

__le16 e_value_offs; /* offset in disk block of value */

__le32 e_value_block; /* disk block attribute is stored on (n/i) */

__le32 e_value_size; /* size of attribute value */

__le32 e_hash; /* hash value of name and value */

char e_name[0]; /* attribute name */

};

通过ext4_xattr_entry 我们可以找到存放 ACL 信息的扩展属性的块以及块号,并知道块所占据的大小。这段存储空间的组织形式如上图所示,先是一个 ext4_acl_header, 然后接着是四个 ext4_acl_entry_short ,分别代表 ACL_USER_OBJ,ACL_GROUP_OBJ,ACL_OTHER, ACL_MASK, 这四项。然后后面紧跟的是普通的 ACL 实体。代码如下(定义在 fs/ext4/acl.h 的第 00011~00024 行):

typedef struct {

__le16 e_tag;

__le16 e_perm;

__le32 e_id;

} ext4_acl_entry;

 

typedef struct {

__le16 e_tag;

__le16 e_perm;

} ext4_acl_entry_short;

 

typedef struct {

__le32 a_version;

} ext4_acl_header;

当然了,一个文件的ACL 项数可能会很多,在一个 inode 的扩展属性中无法放得下,那么 Ext_inode_info 的 i_state 没有设置 EXT4_STATE_XATTR ,表明扩展属性存放在另外的数据块中。那么我们如何找到这个数据块呢?

此时Ext_inode_info 中有一项 i_file_acl 指出了扩展属性所在的磁盘块,我们可以根据这个块号在磁盘块中搜索相应的扩展属性并得到 ACL 属性值。接下来的问题是我们怎么知道 i_file_acl 就是指向扩展属性的块号呢?答案在于内核在装载一个 inode 的时候就把它的 Ext_inode_info 一并设置好了。通过查看 inode 装载函数 ext4_iget (定义在 fs/ext4/inode.c 的 04314~04489 行)我们可以很轻松的指导, i_file_acl 实际上就是 ext4_inode 的 i_file_acl_lo 和 i_file_acl_high 拼接而成的。

至此为止,ACL 在外存中的存储我们已经完全搞清楚了,并且我们顺带的分析了知道 Inode 的情况下如何从磁盘中存取 ACL 信息。

 

4.  ACL控制流程 
上面两个部分分别讲述了ACL 在内存和外存中的表示,以及其各自的操作,为本部分的探讨提供了基础。这一节我们主要分析 ACL 的控制机制,以及 ACL 对其他内核代码的影响。

4.1.  VFS基本原理 
为了使Linux 支持其他各种不同文件系统, Linux 将各种不同的文件系统的操作和管理纳入到一个统一的框架中,让内核中的文件系统界面成为一条文件系统“总线”,使得用户程序可以通过同一个文件系统操作界面,也就是同一种系统调用对各种不同的文件系统(以及文件)进行操作。这样就可以对用户程序隐去各种不同文件系统的实现细节,为用户程序提供一个统一的、抽象的、虚拟的文件系统界面。这就是所谓的“虚拟文件系统” VFS ( Virtual Filesystem Switch )。这个抽象的界面主要由一组标准的、抽象的文件操作构成,以系统调用的形式提供与用户程序,如 read() 、 write() 、 lseek() 等等。这样用户程序就可以把所有的文件看作一致的、抽象的“ VFS 文件”。

VFS的主体是一个 file_operation 的数据结构(定义在 include/linux/fs.h 中),里面全是函数指针:

struct file_operations {

struct module *owner;

loff_t (*llseek) (struct file *, loff_t, int);

ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);

ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);

ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);

ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);

int (*readdir) (struct file *, void *, filldir_t);

unsigned int (*poll) (struct file *, struct poll_table_struct *);

int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);

long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);

long (*compat_ioctl) (struct file *, unsigned int, unsigned long);

int (*mmap) (struct file *, struct vm_area_struct *);

int (*open) (struct inode *, struct file *);

int (*flush) (struct file *, fl_owner_t id);

int (*release) (struct inode *, struct file *);

int (*fsync) (struct file *, struct dentry *, int datasync);

int (*aio_fsync) (struct kiocb *, int datasync);

int (*fasync) (int, struct file *, int);

int (*lock) (struct file *, int, struct file_lock *);

ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);

unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);

int (*check_flags)(int);

int (*flock) (struct file *, int, struct file_lock *);

ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);

ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);

int (*setlease)(struct file *, long, struct file_lock **);

};

而每种文件系统必须实现自己的file_operation ,我们来看看 ext4 的 file_operation( 定义在 fs/ext4/file.c 中 )

const struct file_operations ext4_file_operations = {

.llseek = generic_file_llseek,

.read = do_sync_read,

.write = do_sync_write,

.aio_read = generic_file_aio_read,

.aio_write = ext4_file_write,

.unlocked_ioctl = ext4_ioctl,

#ifdef CONFIG_COMPAT

.compat_ioctl = ext4_compat_ioctl,

#endif

.mmap = ext4_file_mmap,

.open = ext4_file_open,

.release = ext4_release_file,

.fsync = ext4_sync_file,

.splice_read = generic_file_splice_read,

.splice_write = generic_file_splice_write,

};

在EXT4 文件系统中 open 函数就指向具体的 ext4_file_open, 后者跟据实现在 EXT4 文件系统上的打开操作。

每个进程通过" 打开文件 open" 与具体的文件建立起连接,或者说建立起一个读写的上下文,这种连接以个 file 数据结构作为代表,结构中还有一个 file_operation 结构指针 f_op 。将 file 结构中的指针 f_op 设置成指向某个具体的 file_operations 结构,就指定了这个文件所属的文件系统,并且与具体文件系统所提供的一组函数挂上了钩。【 2 】

我们前面说VFS 与具体文件系统联系界面的主体是 file_operations ,是因为除此之外还有另外一些数据结构。其中主要的还有与目录项相联系的 dentry_operations 数据结构以及与索引节点相联系的 inode_operations 数据结构。这两个数据结构中的内容也是一些函数指针,这些函数大多只是在打开文件的过程中使用,或者仅在文件操作的底层使用。我们正好需要分析文件的打开过程,因此这两个数据结构也是我们要重点分析的对象。

inode_operation的定义在 include/linux/fs.h 中,代码如下:

struct inode_operations {

int (*create) (struct inode *,struct dentry *,int, struct nameidata *);

struct dentry * (*lookup) (struct inode *,struct dentry *, struct nameidata *);

int (*link) (struct dentry *,struct inode *,struct dentry *);

int (*unlink) (struct inode *,struct dentry *);

int (*symlink) (struct inode *,struct dentry *,const char *);

int (*mkdir) (struct inode *,struct dentry *,int);

int (*rmdir) (struct inode *,struct dentry *);

int (*mknod) (struct inode *,struct dentry *,int,dev_t);

int (*rename) (struct inode *, struct dentry *,

struct inode *, struct dentry *);

int (*readlink) (struct dentry *, char __user *,int);

void * (*follow_link) (struct dentry *, struct nameidata *);

void (*put_link) (struct dentry *, struct nameidata *, void *);

void (*truncate) (struct inode *);

int (*permission) (struct inode *, int);

int (*setattr) (struct dentry *, struct iattr *);

int (*getattr) (struct vfsmount *mnt, struct dentry *, struct kstat *);

int (*setxattr) (struct dentry *, const char *,const void *,size_t,int);

ssize_t (*getxattr) (struct dentry *, const char *, void *, size_t);

ssize_t (*listxattr) (struct dentry *, char *, size_t);

int (*removexattr) (struct dentry *, const char *);

void (*truncate_range)(struct inode *, loff_t, loff_t);

long (*fallocate)(struct inode *inode, int mode, loff_t offset,

  loff_t len);

int (*fiemap)(struct inode *, struct fiemap_extent_info *, u64 start,

      u64 len);

};

EXT4文件系统的 inode_operation 结构定义在 fs/ext4/file.c 中,代码如下:

const struct inode_operations ext4_file_inode_operations = {

.truncate = ext4_truncate,

.setattr =  ext4_setattr,

.getattr =   ext4_getattr ,

#ifdef CONFIG_EXT4_FS_XATTR

.setxattr =  generic_setxattr ,

.getxattr =   generic_getxattr ,

.listxattr = ext4_listxattr,

.removexattr = generic_removexattr,

#endif

.permission =   ext4_permission ,

.fallocate = ext4_fallocate,

.fiemap = ext4_fiemap,

};

其中红色斜字体标识是比较重要的一些函数,对我们分析ACL (或者说文件打开过程)有着极其重要的作用。我们将在下面两节中详细论述他们的功能。上面的关系可以用如下的图来表示:

 

Linux2.6内核 ACL 机制数据结构和实现分析 - zhuzhu - 津津计较

 

图4-1 VFS 原理图

4.2.  ACL访问控制点 
首先让我们来考虑这么一个问题:当某个用户要打开一个文件(指定文件路径)的时候,如何判断该用户对这个文件有访问权限,以及该用户对这条路径上每一个目录(节点)都拥有访问权限?更进一步来说,对于任何操作(创建、删除、更改文件等)我们如何判断用户拥有相应的操作权限。这个实际上是一个访问控制点设计问题,在一个糟糕的系统设计中用户可能需要翻遍所有的代码才能够找到所有访问控制点(有时还得对这个“所有”表示怀疑,毕竟谁知道用户有没有漏掉那个角落里的控制点呢)。然而感谢Linux 的设计者所做的优秀设计,我们并不需要这么麻烦。 Linux 将所有的访问控制点集中到少数几个函数,我们只需要查看这很少的几个函数就能确信我们确实找到了所有的访问控制点。

在根据路径名得到对应的inode 的时候(著名的 namei 和 lnamei 函数), Linux2.4 和 2.6 内核都是用 path_init() 和 path_walk() 两个函数去实现这个功能,也就是说只要你是通过路径的方式访问文件都必须先经过这两个函数才能得到相应的 Inode 。 path_walk() 会把不在内存中的 inode 节点装载入内存,并一边装载一边检查是否对该 inode 有访问权限,这样 path_walk() 就能够完成对于目标文件和目标文件所在路径上每一个节点进行权限检查。

path_walk定义在 fs/namei.c 中,我们可以看到它调用了 link_path_walk(), 而 link_path_walk (定义在 fs/namei.c 中)又调用了 __link_path_walk , __link_path_walk 调用 inode_permission() 这个函数。经过分析我们得知,这个函数就是非常重要的一个访问控制点。下面我们来分析这个函数(定义在 fs/namei.c 的 00241~00276 行):

1:  int inode_permission(struct inode *inode, int mask)

2:  {

3:  int retval;

4:  if (mask & MAY_WRITE) {

5:  umode_t mode = inode->i_mode;

6:  /*

7:   * Nobody gets write access to a read-only fs.

8:   */

9:  if (IS_RDONLY(inode) &&

10:      (S_ISREG(mode) || S_ISDIR(mode) || S_ISLNK(mode)))

11:  return -EROFS;

12:  /*

13:   * Nobody gets write access to an immutable file.

14:   */

15:  if (IS_IMMUTABLE(inode))

16:  return -EACCES;

17:  }

18:  if (inode->i_op->permission)

19:  retval = inode->i_op->permission(inode, mask);

20:  else

21:  retval = generic_permission(inode, mask, NULL);

22:  if (retval)

23:  return retval;

24:  retval = devcgroup_inode_permission(inode, mask);

25:  if (retval)

26:  return retval;

27:  return security_inode_permission(inode,

28:  mask & (MAY_READ|MAY_WRITE|MAY_EXEC|MAY_APPEND));

29:  }

该函数的主要功能是检查当前进程对Inode 是否有 mask 的访问权限。对于文件的写权限的检查( 4~17 行)稍微特殊一点,我们显然不能允许对只读文件系统进行写操作 (9 行 ) ,同样我们也不能对常规文件、目录和符合链接之外的文件进行写操作( 10 行)。如果文件系统设置了 immutable 属性,那么我即便是系统管理员我们同样不能对其进行写操作( 15 )。函数的第 18 行判断 inode->i_op->permission 是否被设置,对于 EXT4 文件系统的 inode 来说,在 Inode 装载进入内存的时候内核就已经将其 i_op 设置为 ext4_file_inode_operations 。查看该表我们可以看到 19 行 inode->i_op->permission() 实际调用的是 ext4_permission (上一节中介绍过的)。

ext4_permission定义在 fs/ext4/acl.c 中,源码如下:

int ext4_permission(struct inode *inode, int mask)

{

return generic_permission(inode, mask, ext4_check_acl);

}

我们看到它实际上调用了generic_permission ,并将 ext4_check_acl 函数指针作为参数传入进去。我们来看看 generic_permission 的源代码(定义在 fs/namei.c 中):

1:  int generic_permission(sruct inode *inode, int mask,

2:  int (*check_acl)(struct inode *inode, int mask))

3:  {

4:  umode_t mode = inode->i_mode;

5:  mask &= MAY_READ | MAY_WRITE | MAY_EXEC;

6:  if (current_fsuid() == inode->i_uid)

7:  mode >>= 6;

8:  else {

9:  if (IS_POSIXACL(inode) && (mode & S_IRWXG) && check_acl) {

10:  int error = check_acl(inode, mask);

11:  if (error == -EACCES)

12:  goto check_capabilities;

13:  else if (error != -EAGAIN)

14:  return error;

15:  }

16: 

17:  if (in_group_p(inode->i_gid))

18:  mode >>= 3;

19:  }

20:  /*

21:   * If the DACs are ok we don't need any capability check.

22:   */

23:  if ((mask & ~mode) == 0)

24:  return 0;

25:   check_capabilities:

26:  /*

27:   * Read/write DACs are always overridable.

28:   * Executable DACs are overridable if at least one exec bit is set.

29:   */

30:  if (!(mask & MAY_EXEC) || execute_ok(inode))

31:  if (capable(CAP_DAC_OVERRIDE))

32:  return 0;

33:  /*

34:   * Searching includes executable on directories, else just read.

35:   */

36:  if (mask == MAY_READ || (S_ISDIR(inode->i_mode) && !(mask & MAY_WRITE)))

37:  if (capable(CAP_DAC_READ_SEARCH))

38:  return 0;

39:  return -EACCES;

40:  }

函数第4~24 行进行自主访问控制检查( DAC ),第 6 行用当前进程的 fsuid 和 inode 的 uid 做比较进行下一步的判断。函数的 25~40 进行特权检查。我们所要关注的是第 10 行,对文件的 ACL 权限进行检查。在上一步调用中,我们知道 check_acl 实际上是 ext4_check_acl, 该函数定义在 fs/ext4/acl.c 中,源码如下:

static int

ext4_check_acl(struct inode *inode, int mask)

{

struct posix_acl *acl = ext4_get_acl(inode, ACL_TYPE_ACCESS);

//when acl==NULL,IS_ERR(acl) return false

if (IS_ERR(acl))

return PTR_ERR(acl);

//we should check acl point is  NULL,because we may mount the fs without the option acl! 

if (acl) {

int error = posix_acl_permission(inode, acl, mask);

posix_acl_release(acl);

return error;

}

return -EAGAIN;

}

我们看到该函数首先用ext4_get_acl 得到节点的 ACL 属性,然后使用 posix_acl_permission 检查 ACL 权限。这两个函数我们都在第一二部分探讨过,这里就不再述说了。

至此为止我们就已经找到了内核中的访问控制点,同时ACL 的访问控制点也已经找到了。

4.3.  相关系统调用 
上一节我们将所有访问控制点都找到了,但是系统中和ACL 有关的代码并没有完全找全,比如说对 ACL 进行设置(增加、删除、修改等)以及由于 ACL 的加入给其他系统调用带来的影响。如果说不把这些函数都找到那么我们就不能说我们把 ACL 机制完全弄懂。这一部分的系统代码相当繁杂并且琐碎,如何才能够保证能够找到全部的代码呢?主要有两种方法:

1) Posix 文档中指出了一些受 ACL 影响的系统函数。

2)使用 Strace,Kscope 等工具追踪高层的命令调用过程。

在Posix 文档中指出了下列系统调用要做出修改以反映 ACL 的作用: access(), chmod(), creat(), fstat(), mkdir(), mkfifo(), open(), stat() 。

首先让我们来看看open() 系统调用,它在内核中的函数名为 sys_open, 其函数申明在 include/linux/Syscalls.h 中,定义使用宏封装起来了,在 fs 的 Open.c 中。

SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, int, mode),该函数将调用 do_sys_open(), 其函数定义在 fs 的 Open.c 中,我们即来分析这段代码。

do_sys_open 通过 get_unused_fd(), 在当前进程空间内的 struct file 结构数组中 , 找一个空的 struct file{} 结构,并返回一个数组的下标号,之后 do_sys_open 又调用 do_filp_open.

do_filp_open首先会调用 path_lookup_open 检查是否能够打开文件, path_lookup_open 调用 do_path_lookup(),do_path_lookup 调用 path_init 和 path_walk,path_walk 调用 link_path_walk, 调用 __link_path_walk, 调用 exec_permission_lite 和 inode_permission 检查路径上每个节点是否有权访问, exec_permission 将会调用 security_inode_permission,inode_permission 将会调用 inode->i_op->permission() ,调用 ext4_permission 调用, ext4_check_acl 进行 ACL 访问控制检查。

do_filp_open调用 nameidata_to_filp , nameidata_to_filp 调用 __dentry_open ,在 __dentry_open, 通过关键语句 ,f->f_op = fops_get(inode->i_fop) ;得到了具有一个指向 struct file_operations 结构的指针的 struct file 结构指针。

需要指出的是系统调用creat() 实际调用的是 sys_creat() 实际调用的是 sys_open 函数。

我们再来看看stat() 系统调用,它在内核中的函数名为 sys_fstat, 其函数申明在 include/linux/Syscalls.h 中 , 当然该定义也是用宏封装起来了,在 fs 的 Stat.c 中 .

SYSCALL_DEFINE2(stat, char __user *, filename, struct __old_kernel_stat __user *, statbuf),该函数会调用 vfs_stat 得到内核空间的的 stat 然后用 cp_old_stat() 转化为用户空间的 stat 格式。

其中vfs_stat 会调用 vfs_statat, 调用 user_path_at 进入目标节点,然后用 vfs_getattr 从目标节点获得 stat 信息。 user_path_at 会调用 do_path_lookup 进入目标节点,同时做权限检查(同上,不再赘述)。我们再来关注 vfs_getattr ,它会调用 security_inode_getattr , security_inode_getattr 调用 security_ops->inode_getattr 得到文件属性,如果得不到再调用 inode->i_op->getattr 调用底层文件系统的 getattr. 在 ext4 文件系统中将会调用 ext4_getattr ,调用 generic_fillattr 获得 stat 信息。当然了目前的 stat() 和 fstat() 系统调用还无法反映 ACL 的变化,需要做出修改。

mkdir()系统调用,它在内核中的函数名为 sys_mkdirat(), 其函数申明在 include/linux/Syscalls.h 中,实现在 fs/Namei.c 中。

chmod()系统调用,在内核中的函数名为 sys_fchmodat() 其函数申明在 include/linux/Syscalls.h 中,实现在 fs/Open.c 中。

access()系统调用,在内核中的函数名为 sys_faccessat() 其函数申明在 include/linux/Syscalls.h 中,实现在 fs/Open.c 中。

fstat()系统调用,在内核中的函数名为 vfs_fstat() 其函数申明在 include/linux/Syscalls.h 中,实现在 fs/Open.c 中。

mkfifo不是系统调用,它最终会调用 mknod() 系统调用,在内核中函数名字为 sys_mknodat() 其函数申明在 include/linux/Syscalls.h 中,实现在 fs/Namei.c 中 .

通过Strace 工具我们追踪系统命令 setfacl 和 getfacl ,发现二者在实现上都是使用 getxattr 和 setxattr 系统接口。由此可见 Linux 内核中并没有实现增删改某个具体 ACL 实体的函数。事实上关于这些具体 ACL 实体设置的库函数并没有包括在 linux 内核中,需要开发者自己实现其库函数。目前有不少开源小组在做这方面的工作,并做出了 libacl-devel 开发库。我们将在下一个文档中给出如何使用这些开源库进行 Linux ACL 编程。

 

总结与展望 
Linux ACL机制是一种新型的访问控制机制,能够实现任意粒度的访问控制权限设置。本文通过对 Linux 2.6 内核源码的分析,阐述了 ACL 机制的数据结构和实现原理。然而对于 ACL 的大规模以及广泛的应用现在还没有普及,应用 ACL 编程也是存在着一些问题。如何解决这些问题,让 ACL 实实在在发挥它的作用,还需要努力。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值