11扩展属性和访问控制表
许多文件系统都提供了一些特性,扩展了VFS层提供的标准功能。虚拟文件系统不可能为想到每个特性都提供具体的数据结构。超出标准的UNIX文件模型的附加特性,通常需要将一个组扩展属性关联到每个文件系统对象。但内核能够提供的是一个框架,容许增加特定于文件系统的扩展。扩展属性(extended attribute,xattrs)是能够关联到文件的(或多或少)任意属性。由于每个文件通常都只关联了所有可能扩展属性的一个子集,扩展属性存储在常规的inode数据结构之外
,以避免增加该结构在内存中的长度和浪费磁盘空间。这实际上容许使用一个通用的属性集合,而不会对文件系统性能或磁盘空间需求有任何显著影响。
扩展的一种用途是实现访问控制表
(access control list),对UNIX风格的权限模型进行扩展。它们允许实现细粒度的访问控制,不仅仅使用用户、组、其他用户的概念,而是将各个用户及其允许的操作组成一个明确的列表,关联到文件。这种列表很自然地融合到扩展属性模型中。扩展属性的另一种用途是为SE-Linux提供标记信息
11.1 扩展属性
从文件系统用户的角度来看,一个扩展属性就是与文件系统对象关联的一个“名称/值”对。名称是一个普通的字符串,内核对值的内容不作限制。它可以是文本串,但也可以包含任意的二进制数据
。属性可以定义,也可以不定义(如果文件没有关联属性,就是这种情形)。如果定义了属性,可以有值,也可以没有
。
属性名称会按命名空间细分。这意味着,访问属性也需要给出命名空间
。按照符号约定,用一个点来分隔命名空间和属性名(例如user.mime_type)
。内核使用宏定义了有效的顶层命名空间的列表。形如XATTR_*_PREFIX
。在从用户空间传递来一个名字串,需要与命名空间前缀比较时,一组辅助性的宏XATTR_*_PREFIX_LEN
是比较有用的。
//include/linux/xattr.h
#define XATTR_OS2_PREFIX "os2."
#define XATTR_OS2_PREFIX_LEN (sizeof (XATTR_OS2_PREFIX) - 1)
#define XATTR_SECURITY_PREFIX "security."
#define XATTR_SECURITY_PREFIX_LEN (sizeof (XATTR_SECURITY_PREFIX) - 1)
#define XATTR_SYSTEM_PREFIX "system."
#define XATTR_SYSTEM_PREFIX_LEN (sizeof (XATTR_SYSTEM_PREFIX) - 1)
#define XATTR_TRUSTED_PREFIX "trusted."
#define XATTR_TRUSTED_PREFIX_LEN (sizeof (XATTR_TRUSTED_PREFIX) - 1)
#define XATTR_USER_PREFIX "user."
#define XATTR_USER_PREFIX_LEN (sizeof (XATTR_USER_PREFIX) - 1)
内核提供了几个系统调用来读取和操作扩展属性
setxattr
用于设置或替换某个扩展属性的值,或创建一个新的扩展属性
getxattr
获取某个扩展属性的值
removexattr
删除一个扩展属性
listxattr
列出与给定的文件系统对象相关的所有扩展属性
这些系统调用都还有前缀为l的变体
。这些变体不跟踪符号链接,而是对符号链接本身的扩展属性进行操作。另外,前缀为f的变体
不处理文件名,而是对通过文件描述符指定的对象进行处理
11.1.1 到虚拟文件系统的接口
虚拟文件系统向用户空间提供了一个抽象层,使得所有应用程序都可以使用扩展属性,而无需考虑底层文件系统实现如何在磁盘上存储该信息。尽管VFS为扩展属性提供了一个抽象层,这并不意味着每个文件系统都必须实现该特性。实际上,情况刚好相反。内核中的大多数文件系统都不支持扩展属性。但我们也应该注意到,Linux上所有主要的硬盘文件系统(Ext3、reiserfs、xfs等)都支持扩展属性
-
数据结构
内核使用了一个简单的字符串来表示名称,而用一个void指针来表示值在内存中的存储位置扩展属性操作函数
//include/linux/fs.h struct inode_operations { ... 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*); ... }
文件系统可以为这些操作提供定制实现,但内核也提供了一组通用的处理程序。例如,Ext3文件系统即使用了内核的通用实现
//include/linux/xattr.h //与块设备间传输信息,处理扩展属性的函数集合 struct xattr_handler { char *prefix;//命名空间,该操作即应用到这个命名空间的属性上, XATTR_*_PREFIX 中的任何值 size_t (*list)(struct inode *inode, char *list, size_t list_size, const char *name, size_t name_len);//列出与一个文件相关的所有扩展属性 int (*get)(struct inode *inode, const char *name, void *buffer, size_t size);//从底层块设备读取扩展属性 int (*set)(struct inode *inode, const char *name, const void *buffer, size_t size, int flags);//向底层块设备写入扩展属性 }; //include/linux/fs.h //超级块结构体 struct super_block { ... struct xattr_handler **s_xattr;//数组,指向用于处理扩展属性的函数指针 ... };
super_block
中处理程序在数组(s_xattr
)中出现的次序不是固定的。内核通过比较处理程序的前缀
与所述扩展属性的命名空间前缀
,可以找到一个适当的次序 -
系统调用
每个扩展属性操作(get、set和list)都对应了3个系统调用,三者的区别在于指定目标的方式。为避免代码复制,这些系统调用在结构上分为两部分:
(1) 查找与目标对象相关联的dentry实例;
(2) 其他工作委托给一个函数,该函数对3个系统调用是通用的。
查找dentry实例时,可使用user_path_walk或user_path_walk_link,或直接读取包含在file实例中的指针,具体的做法取决于使用了哪个系统调用。在找到dentry实例后,就为3个系统调用建立了一个公共的基础。fs/xattr.c
:
listxattr函数流程:
- 修改用户空间程序给出的列表的最大程度,使之不超过内核所允许的扩展属性列表的最大长度XATTR_LIST_MAX,并分配所需的内存
- 调用inode_operations的listxattr方法,用各个名称/值对来填充分配的空间
- 将结果复制回用户空间
-
内核提供的扩展属性默认处理函数
//fs/xattr.c ssize_t generic_getxattr(struct dentry *dentry, const char *name, void *buffer, size_t size) ssize_t generic_listxattr(struct dentry *dentry, char *buffer, size_t buffer_size) //内核设置文件扩展属性的默认实现 int generic_setxattr(struct dentry *dentry, const char *name, const void *value, size_t size, int flags) int generic_removexattr(struct dentry *dentry, const char *name)
generic_setxattr函数流程:
- 查找扩展属性的命名空间的 xattr_handler 实例,通过遍历超级块中的属性函数数组比较前缀与属性名相同的
- 调用特定于文件系统的set函数(由具体文件系统实现)
generic_listxattr函数流程:
- 如果保存结果的缓冲区为NULL,返回需要保存的buffer大小
- buffer不为NULL,将结果保存到buffer中,并返回大小
11.1.2 Ext3中的实现
-
数据结构
Ext3
采纳了一些提高编码效率的良好建议.按照标识号来访问处理程序函数
,而不是按照字符串标识符。这简化了许多操作,并能够更高效地利用磁盘空间,因为与前缀字符串相比,标识号只需要存储一个数字//fs/ext3/xattr.c static struct xattr_handler *ext3_xattr_handler_map[] = { [EXT3_XATTR_INDEX_USER] = &ext3_xattr_user_handler, #ifdef CONFIG_EXT3_FS_POSIX_ACL [EXT3_XATTR_INDEX_POSIX_ACL_ACCESS] = &ext3_xattr_acl_access_handler, [EXT3_XATTR_INDEX_POSIX_ACL_DEFAULT] = &ext3_xattr_acl_default_handler, #endif [EXT3_XATTR_INDEX_TRUSTED] = &ext3_xattr_trusted_handler, #ifdef CONFIG_EXT3_FS_SECURITY [EXT3_XATTR_INDEX_SECURITY] = &ext3_xattr_security_handler, #endif };
Ext3扩展属性的磁盘布局:
说明:
扩展属性所占的空间,由一个标识头开始,接下来是一系列数据项的列表。每个数据项都包含了属性名称和指向属性值存储区域的一个指针。在向文件添加新的扩展属性时,列表向下增长。
属性值存储在扩展属性数据空间的尾部,值表与属性名称表相对增长。属性值可以按任意次序存储,通常与属性名排序不同
这种类型的结构可以放置在两个地方:inode末尾的未使用空间
磁盘上一个独立的数据块
第一种情况,只有使用了允许动态inode长度的新文件系统格式时(即,
EXT3_DYNAMIC_REV
),才可能出现。空闲空间的长度保存在ext3_inode_info->i_extra_isize
。这两种方案可以同时使用,但所有扩展属性头和值的总长度不能超过一个数据块加上inode中空闲空间的长度
。除了inode中的空闲空间之外,最多只能再使用一个数据块来存储扩展属性。实际上,所需的空间通常比一个完整的磁盘块要少得多。
如果两个文件的扩展属性集合是相同的,那么二者可以共享同一磁盘表示//fs/ext3/xattr.h //用来实现ext3扩展属性在磁盘上存储的布局的头部结构 struct ext3_xattr_header { __le32 h_magic; /* 用于标识的魔数 *//* magic number for identification */ __le32 h_refcount; /* 引用计数 *//* reference count */ __le32 h_blocks; /* 使用磁盘块的数目,目前这个值总是1 *//* number of disk blocks used */ __le32 h_hash; /* 所有属性的散列值 *//* hash value of all attributes */ __u32 h_reserved[4]; /* 目前为0 *//* zero right now */ }; //扩展属性的数据项结构,各个数据项的长度不见得是相同的,因为属性名的长度是可变的 struct ext3_xattr_entry { __u8 e_name_len; /* 属性名字长度 *//* length of name */ __u8 e_name_index; /* 属性名索引,用于索引 ext3_xattr_handler_map 表 *//* attribute name index */ __le16 e_value_offs; /* 属性值在所处磁盘块中的偏移量.如果扩展属性存储在inode内,则将ext3_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 */ };
-
实现
由于对不同的属性命名空间来说,处理程序的实现十分相似,所以以下讨论仅限于user命名空间的实现。其他命名空间的处理程序函数,仅有少许不同或完全相同。ext3_xattr_user_handler定义如下
//fs/ext3/xattr_user.c struct xattr_handler ext3_xattr_user_handler = { .prefix = XATTR_USER_PREFIX, .list = ext3_xattr_user_list, .get = ext3_xattr_user_get, .set = ext3_xattr_user_set, };
-
获取扩展属性 ext3_xattr_user_get
-
设置扩展属性 ext3_xattr_user_set
ext3_xattr_set_handle函数流程:
- 获取inode位置
- 查找扩展属性数据,如果失败则从磁盘中找
- 操作属性,删除或修改或新建
- 如果传递到函数的数据缓冲区为NULL,则删除现存扩展属性。
- 如果数据缓冲区包含了属性值,则替换现存扩展属性的值或创建一个新的扩展属性。按照手册页setxattr(2)的文档,标志
XATTR_REPLACE
和XATTR_CREATE
分别表示在调用之前,所述属性必须是存在的还是不存在的
-
列出扩展属性 ext3_listxattr
ext3_listxattr 函数流程:- 在inode或块中读取属性并将属性拷贝到缓冲中给用户层
-
11.1.3 Ext2中的实现
Ext2中的扩展属性的实现与Ext3的实现非常相似,但由于Ext3中的一些特性在Ext2中是不可用的,这是二者扩展属性的实现的差别
- 由于Ext2并不支持动态的inode长度,所以磁盘上的inode中没有足够空间存储扩展属性的数据。因此,扩展属性总是存储在一个独立的数据块中。这简化了一些函数,因为无须区分扩展属性的不同存储位置了
- Ext2并不使用日志,因此所有日志相关函数的调用都是不必要的。这也使得一些只处理句柄操作的包装器函数变得不必要
11.2 访问控制表
POSIX访问控制表(ACL)是POSIX标准定义的一种扩展,用于细化Linux的自主访问控制(DAC)模型。可以参考手册页acl(5)
进一步了解相关概念。ACL借助扩展属性实现,修改ACL所用的方法与其他扩展属性也是相同的
。内核对其他扩展属性的内容并不感兴趣,但ACL扩展属性将集成到inode的权限检查中。尽管文件系统可以自由选择用于表示扩展属性的物理格式,但内核仍然定义了用于表示访问控制表的交换结构。对于承载访问控制表的扩展属性,必须使用下列命名空间
//include/linux/posix_acl_xattr.h
#define POSIX_ACL_XATTR_ACCESS "system.posix_acl_access"
#define POSIX_ACL_XATTR_DEFAULT "system.posix_acl_default"
用户层程序getfacl、setfacl和chacl
用于获取、设置和修改ACL的内容。它们使用可以操作扩展属性的标准系统调用
,并不需要与内核进行非标准交互。许多其他实用程序(例如ls)也内建了对访问控制表的支持。
11.2.1 通用实现
用于实现ACL的通用代码包含在两个文件中:fs/posix_acl.c
包含了分配新ACL、复制ACL、进行扩展权限检查等功能的代码;而fs/xattr_acl.c
包含的函数用于在扩展属性和ACL的通用表示之间进行转换,并且转换是双向的。所有通用数据结构都定义在include/linux/posix_acl.h和include/linux/posix_acl_xattr.h
中
-
数据结构
ACL相关数据的内存表示:
//include/linux/posix_acl.h /* a_type field in acl_user_posix_entry_t */ //用于acl_user_posix_entry_t中的a_type字段,ACL类型 #define ACL_TYPE_ACCESS (0x8000) #define ACL_TYPE_DEFAULT (0x4000) /* e_tag entry in struct posix_acl_entry */ //用于struct posix_acl_entry中的e_tag项,ACL标记 #define ACL_USER_OBJ (0x01) #define ACL_USER (0x02) #define ACL_GROUP_OBJ (0x04) #define ACL_GROUP (0x08) #define ACL_MASK (0x10) #define ACL_OTHER (0x20) /* permissions in the e_perm field */ //e_perm字段中的权限值,ACL权限 #define ACL_READ (0x04) #define ACL_WRITE (0x02) #define ACL_EXECUTE (0x01) //#define ACL_ADD (0x08) //#define ACL_DELETE (0x10) //用于存储与ACL相关的所有数据的内存表示,表项 struct posix_acl_entry { short e_tag;//标记,ACL_USER_OBJ 等 unsigned short e_perm;//权限 ACL_READ 等 unsigned int e_id;//用户/组 id }; //用于存储与ACL相关的所有数据的内存表示的主要数据结构,Access Control List(访问控制列表),inode的ACL struct posix_acl { atomic_t a_refcount;//引用计数 unsigned int a_count;//ACL项的数目 struct posix_acl_entry a_entries[0];//表项 };
ACL用扩展属性的表示,用于与用户层的交互
//include/linux/posix_acl_xattr.h //用于ACL的扩展属性表示,用于与用户层的外部交互 typedef struct { __le16 e_tag; __le16 e_perm; __le32 e_id; } posix_acl_xattr_entry; //用于ACL的扩展属性表示,用于与用户层的外部交互 typedef struct { __le32 a_version; posix_acl_xattr_entry a_entries[0]; } posix_acl_xattr_header;
用于内部和外部表示的结构十分相似,但用于
外部表示的类型明确指定了字节序和位长
。此外,对磁盘表示来说,引用计数是不必要的有两个函数用于在两种表示之间来回转换:
posix_acl_from_xattr
和posix_acl_to_xattr
-
权限检查
对涉及访问控制表的权限检查,内核通常需要底层文件系统的支持。或者文件系统自行实现所有的权限检查(通过struct inode_operations的permission函数),或者文件系统提供一个回调函数供generic_permission
使用。内核中的大多数文件系统,都使用了后一种方法。
generic_permission
中使用的回调函数如下(请注意,check_acl表示回调函数)//fs/namei.c //通用权限检查函数,check_acl 为检查ACL权限的函数 int generic_permission(struct inode *inode, int mask, int (*check_acl)(struct inode *inode, int mask))
generic_permission
调用的check_acl
回调函数最后会调用内核实现的posix_acl_permission
函数//fs/posix_acl.c /* acl权限检查 * inode:指向inode结构体的指针, * acl:指向acl(的内存表示)的指针, * want:要检查的权限( mode 中的 MAY_READ 、 MAY_WRITE 或 MAY_EXEC 权限位)。 * 如果授予访问权限,则函数返回0,否则返回相应的错误码 */ int posix_acl_permission(struct inode *inode, const struct posix_acl *acl, int want)
11.2.2 Ext3中的实现
由于ACL基于扩展属性实现,并借助了许多通用的辅助例程,所以Ext3对ACL的实现相当简洁
-
数据结构
ACL的磁盘表示的格式与通用的POSIX辅助函数所需的内存表示非常类似//fs/ext3/acl.h //ext3文件系统对ACL的表示,用于磁盘存储 typedef struct { __le16 e_tag; __le16 e_perm; __le32 e_id; } ext3_acl_entry; //ext3_acl_entry的另一个版本,少了e_id节省磁盘存储空间 typedef struct { __le16 e_tag; __le16 e_perm; } ext3_acl_entry_short; //ACL项的列表头 typedef struct { __le32 a_version;//区分ACL实现的不同版本,目前为EXT3_ACL_VERSION(即0x0001) } ext3_acl_header;
//include/linux/ext3_fs_i.h //Ext3 inode的内存表示 struct ext3_inode_info { ... #ifdef CONFIG_EXT3_FS_POSIX_ACL struct posix_acl *i_acl;//该posix_acl用作与inode关联的常规ACL列表,如果该信息不在内存中该值为EXT3_ACL_NOT_CACHED [定义为 (void *)-1 ] struct posix_acl *i_default_acl;//指向默认ACL,可以关联到目录,并由子目录继承,如果该信息不在内存中该值为EXT3_ACL_NOT_CACHED [定义为 (void *)-1 ] #endif };
-
磁盘和内存表示之间的转换
有两个转换函数可用于磁盘和内存表示之间的转换:ext3_acl_to_disk
和ext3_acl_from_disk
。二者的实现在fs/ext3/acl.c
中。
后者从inode中包含的信息获取裸数据,剥去头信息,将ACL列表中每个ACL项的数据从小端序格式转换为适用于系统本机CPU的格式。
与此相对的函数是ext3_acl_to_disk
,工作原理类似:它遍历给定的posix_acl实例中的所有ACL项,将其中包含的数据从特定于CPU的格式转换为小端序格式,并指定适当的位长。 -
inode初始化 ext3_init_acl
- ext3_new_inode
- ext3_init_acl
函数流程:
- 检查父目录inode是否支持acl,如果支持则获取父目录的acl,如果目录的超级块不支持acl或没有默认acl则将进程的umask应用到文件
- 如果当前inode表示目录则使用父目录的acl
- 如果不是目录,对默认 ACL 的内存表示创建一个副本
- 从inode创建进程指定的访问权限中,删除默认ACL不能授予的所有权限
- ext3_new_inode
-
获取ACL内存表示 ext3_get_acl
函数流程:
- 检查ACL的内存表示是否已经缓存到了 ext3_inode_info->i_acl,如果已经缓存,创建该表示的一个副本,将其作为结果返回
- 如果ACL并无缓存,从扩展属性子系统获取裸数据转为内存表示,并更新 ext3_inode_info 中acl的缓存字段
-
修改ACL ext3_acl_chmod
- ext3_setattr
- ext3_acl_chmod 负责保持ACL为最新数据并维护其一致性,ext3_setattr最后调用该函数,新权限已经设置到inode中
- ext3_acl_chmod 负责保持ACL为最新数据并维护其一致性,ext3_setattr最后调用该函数,新权限已经设置到inode中
函数流程:
- 从inode中获取acl
- 创建acl副本
- 根据inode->i_mode修改acl
- 记录硬盘操作日志
- 将修改后的ACL数据写回
- 释放ACL副本
- ext3_setattr
-
权限检查 ext3_check_acl
- ext3_permission
- generic_permission
- check_acl(ext3中为 ext3_check_acl)
- posix_acl_permission
- posix_acl_permission
- check_acl(ext3中为 ext3_check_acl)
- generic_permission
- ext3_permission
11.2.3 Ext2中的实现
Ext2对ACL的实现,几乎与Ext3完全相同。差别甚至比二者在扩展属性实现方面的差别还小,因为对于ACL,句柄相关的部分并没有划分为独立的函数。因此,将所有函数和数据结构中的ext3_前缀替换为ext2_
总结
许多文件系统都提供了一些特性,扩展了VFS层提供的标准功能
扩展属性存储在 inode 数据结构外
扩展属性所占的空间,由一个标识头开始,接下来是一系列数据项的列表。每个数据项都包含了属性名称和指向属性值存储区域的一个指针。在向文件添加新的扩展属性时,列表向下增长。
属性值存储在扩展属性数据空间的尾部,值表与属性名称表相对增长。属性值可以按任意次序存储,通常与属性名排序不同
扩展属性结构可以放置在两个地方:
inode末尾的未使用空间
磁盘上一个独立的数据块
扩展属性也可以用于 posix的访问控制表(ACL),对UNIX风格的权限模型进行扩展
=========================================
涉及的命令和配置:
手册页attr(5)对属性有详细说明
手册页acl(5)对posix访问控制表有详细说明
请注意,另一篇很好的综述性文章是Andreas Grünbacher的Usenix论文[Grü03],其中给出了ACL的一般性概述,以及Linux支持的各种文件系统ACL实现的当前状态。Andreas Grünbacher是Ext2和Ext3文件系统中ACL支持的主要开发者之一