当前位置: 首页 > news >正文

acl在内核里的位置_Linux2.6内核 ACL 机制数据结构和实现分析

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

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

shorte_tag;   //tag element,used to present user/group/other

unsigned shorte_perm;//permission element,used to present rwx

unsigned inte_id;//id element,used to present uid/gid

};

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

atomic_ta_refcount;//counter of process who reference this

unsigned inta_count;//count erof acl_entries

struct posix_acl_entrya_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的操作

下表是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

我们可以看到这实际上就是一个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文件系统布局,如下图所示:

图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属性存储实现

在操作系统中,如果libattr功能在内核设置中被打开,、、、、以及文件系统都支持扩展属性(英文简写为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是如何存放的。其结构如下图所示:

图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 {

__u8e_name_len;/* length of name */

__u8e_name_index;/* attribute name index */

__le16e_value_offs;/* offset in disk block of value */

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

__le32e_value_size;/* size of attribute value */

__le32e_hash;/* hash value of name and value */

chare_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 {

__le16e_tag;

__le16e_perm;

__le32e_id;

} ext4_acl_entry;

typedef struct {

__le16e_tag;

__le16e_perm;

} ext4_acl_entry_short;

typedef struct {

__le32a_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(或者说文件打开过程)有着极其重要的作用。我们将在下面两节中详细论述他们的功能。上面的关系可以用如下的图来表示:

图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_tmode = 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实实在在发挥它的作用,还需要努力。

参考资料

[2]Linux内核情景分析(上)

[6]Linux man手册man 1 man 5

[7]Linux编程从入门到精通

[8]Posix_1003.1e

相关文章:

  • WebService与共享COOKIE
  • score函数 机器学习_深度研究:回归模型评价指标R2_score
  • 国际运营商ICT服务发展现状及策略分析
  • bash 将二进制转换为十进制_bash shell实现二进制与十进制数的互转
  • 实体类多层嵌套 遍历_循环运用(多级嵌套结构)
  • 流程设计和优化原则
  • react做h5 例子_【React入门实例(运行于浏览器duan)】
  • XP中用U盘扩充内存
  • 301跳转 https_nginx配置https301跳转
  • php 创建64位的数_使用PHP应用查询SAP HANA Express Edition里的数据
  • 惠普电脑u盘重装系统步骤_惠普U盘重装,惠普电脑U盘一键重装系统方法
  • SQL Server 2005中的SQLCMD工具使用
  • 统计占比_统计局:2019年高等教育在校生中女研究生占比过半
  • 我的个人2007-非工作盘点
  • scritp里怎么传值给php_PHP中的script部分的值怎么传递出来呢?
  • 2018以太坊智能合约编程语言solidity的最佳IDEs
  • android图片蒙层
  • - C#编程大幅提高OUTLOOK的邮件搜索能力!
  • ES6系列(二)变量的解构赋值
  • JavaScript设计模式之工厂模式
  • js数组之filter
  • Linux各目录及每个目录的详细介绍
  • SOFAMosn配置模型
  • 从tcpdump抓包看TCP/IP协议
  • 当SetTimeout遇到了字符串
  • 开发基于以太坊智能合约的DApp
  • 力扣(LeetCode)965
  • 每天一个设计模式之命令模式
  • 深入浅出webpack学习(1)--核心概念
  • 一天一个设计模式之JS实现——适配器模式
  • # MySQL server 层和存储引擎层是怎么交互数据的?
  • (6)STL算法之转换
  • (Demo分享)利用原生JavaScript-随机数-实现做一个烟花案例
  • (搬运以学习)flask 上下文的实现
  • (定时器/计数器)中断系统(详解与使用)
  • (免费领源码)python#django#mysql公交线路查询系统85021- 计算机毕业设计项目选题推荐
  • (亲测成功)在centos7.5上安装kvm,通过VNC远程连接并创建多台ubuntu虚拟机(ubuntu server版本)...
  • (四)Android布局类型(线性布局LinearLayout)
  • (四)TensorRT | 基于 GPU 端的 Python 推理
  • (一)Linux+Windows下安装ffmpeg
  • (终章)[图像识别]13.OpenCV案例 自定义训练集分类器物体检测
  • (转)EOS中账户、钱包和密钥的关系
  • (转载)微软数据挖掘算法:Microsoft 时序算法(5)
  • .Net 路由处理厉害了
  • .NET 同步与异步 之 原子操作和自旋锁(Interlocked、SpinLock)(九)
  • [ C++ ] template 模板进阶 (特化,分离编译)
  • [ 隧道技术 ] 反弹shell的集中常见方式(二)bash反弹shell
  • [ 云计算 | AWS ] AI 编程助手新势力 Amazon CodeWhisperer:优势功能及实用技巧
  • [8-23]知识梳理:文件系统、Bash基础特性、目录管理、文件管理、文本查看编辑处理...
  • [AUTOSAR][诊断管理][ECU][$37] 请求退出传输。终止数据传输的(上传/下载)
  • [BT]BUUCTF刷题第9天(3.27)
  • [BUG] Hadoop-3.3.4集群yarn管理页面子队列不显示任务
  • [bzoj1006]: [HNOI2008]神奇的国度(最大势算法)
  • [C++][基础]1_变量、常量和基本类型
  • [docker] Docker容器服务更新与发现之consul