您的位置:首页 > 理论基础 > 数据结构算法

Linux 2.6内核ACL机制数据结构和实现分析

2010-07-09 08:03 429 查看
Linux2.6内核
ACL
机制数据结构和实现分析

123tanzheng123@163.com

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

下表是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
文件系统布局,如下图所示:



图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
是如何存放的。其结构如下图所示:



图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 *, stru
21cd5
ct 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_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
实实在在发挥它的作用,还需要努力。


参考资料

[1]
http://www.shannon-dd.com/content/view/416/50/
[2]

Linux内核情景分析(上)

[3]

Linux ACL学习笔记
http://www.yuanma.org/data/2007/0718/article_2739.htm
[4]

NFSv4 ACLs in POSIX http://www.suse.de/~agruen/nfs4acl/draft-gruenbacher-nfsv4-acls-in-posix-00.html
[5]

POSIX Access Control Lists on Linux http://www.suse.de/~agruen/acl/linux-acls/online/
[6]

Linux man手册
man 1 man 5

[7]

Linux编程从入门到精通

[8]

Posix_1003.1e

[9]
http://www.rpmfind.net/linux/rpm2html/search.php?query=libacl-devel
[10]
http://acl.bestbits.at/
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息