天天看點

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 機制資料結構和實作分析

下表是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 機制資料結構和實作分析

圖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 機制資料結構和實作分析

圖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;

__u8 e_name_index;

__le16 e_value_offs;

__le32 e_value_block;

__le32 e_value_size;

__le32 e_hash;

char e_name[0];

};

通過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 機制資料結構和實作分析

圖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: 

9:  if (IS_RDONLY(inode) &&

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

11:  return -EROFS;

12: 

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: 

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

24:  return 0;

25:   check_capabilities:

26: 

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

31:  if (capable(CAP_DAC_OVERRIDE))

32:  return 0;

33: 

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 實實在在發揮它的作用,還需要努力。

繼續閱讀