Idmappings¶
大多數檔案系統開發者都會遇到 idmappings。它們用於從磁碟讀取或寫入所有權、向用戶空間報告所有權或進行許可權檢查。本文件旨在幫助想要了解 idmappings 工作原理的檔案系統開發者。
正式說明¶
idmapping 本質上是將一系列 ID 轉換為另一系列 ID 或相同的 ID 系列。在使用者空間廣泛使用的 idmappings 表示約定是
u:k:r
u 表示上層 idmapset U 中的第一個元素,k 表示下層 idmapset K 中的第一個元素。r 引數表示 idmapping 的範圍,即對映的 ID 數量。從現在開始,我們將始終使用 u 或 k 作為 ID 的字首,以明確我們討論的是上層還是下層 idmapset 中的 ID。
為了瞭解這在實踐中的樣子,我們來看下面的 idmapping
u22:k10000:r3
並寫下它將生成的對映
u22 -> k10000
u23 -> k10001
u24 -> k10002
從數學的角度來看,U 和 K 是良序集,idmapping 是從 U 到 K 的順序同構。因此 U 和 K 是順序同構的。事實上,U 和 K 始終是給定系統上可用的所有可能 ID 集合的良序子集。
簡要地從數學角度分析一下將有助於我們強調一些屬性,這些屬性使得我們更容易理解如何在 idmappings 之間進行轉換。例如,我們知道逆 idmapping 也是順序同構
k10000 -> u22
k10001 -> u23
k10002 -> u24
鑑於我們正在處理順序同構以及我們正在處理子集的事實,我們可以將 idmappings 彼此嵌入,即,我們可以明智地在不同的 idmappings 之間進行轉換。例如,假設我們已經給出了三個 idmappings
1. u0:k10000:r10000
2. u0:k20000:r10000
3. u0:k30000:r10000
以及 ID k11000,它是透過將上層 idmapset 中的 u1000 對映到下層 idmapset 中的 k11000 而由第一個 idmapping 生成的。
因為我們正在處理順序同構子集,所以詢問 ID k11000 在第二個或第三個 idmapping 中對應於哪個 ID 是有意義的。使用的直接演算法是應用第一個 idmapping 的逆,將 k11000 對映到 u1000。之後,我們可以使用第二個 idmapping 對映或第三個 idmapping 對映將 u1000 向下對映。第二個 idmapping 會將 u1000 向下對映到 k21000。第三個 idmapping 會將 u1000 向下對映到 k31000。
如果我們被賦予相同的任務來處理以下三個 idmappings
1. u0:k10000:r10000
2. u0:k20000:r200
3. u0:k30000:r300
我們將無法轉換,因為這些集合在第一個 idmapping 的整個範圍內不再是順序同構的(但是它們在第二個 idmapping 的整個範圍內是順序同構的)。第二個或第三個 idmapping 都不包含上層 idmapset U 中的 u1000。這等同於沒有對映 ID。我們可以簡單地說 u1000 在第二個和第三個 idmapping 中未對映。核心會將未對映的 ID 報告為溢位 uid (uid_t)-1 或溢位 gid (gid_t)-1 到使用者空間。
計算給定 ID 對映到什麼值的演算法非常簡單。首先,我們需要驗證該範圍是否可以包含我們的目標 ID。為了簡單起見,我們將跳過此步驟。之後,如果我們想知道 id 對映到什麼值,我們可以進行簡單的計算
如果我們想從左到右對映
u:k:r id - u + k = n
如果我們想從右到左對映
u:k:r id - k + u = n
我們可以用“向下”代替“從左到右”,也可以用“向上”代替“從右到左”。顯然,向下對映和向上對映彼此相反。
為了瞭解上面的簡單公式是否有效,請考慮以下兩個 idmappings
1. u0:k20000:r10000
2. u500:k30000:r10000
假設我們在第一個 idmapping 的下層 idmapset 中給出了 k21000。我們想知道該 ID 是從第一個 idmapping 的上層 idmapset 中的哪個 ID 對映而來的。因此,我們正在第一個 idmapping 中向上對映
id - k + u = n
k21000 - k20000 + u0 = u1000
現在假設我們在第二個 idmapping 的上層 idmapset 中給出了 ID u1100,我們想知道該 ID 在第二個 idmapping 的下層 idmapset 中對映到什麼值。這意味著我們正在第二個 idmapping 中向下對映
id - u + k = n
u1100 - u500 + k30000 = k30600
一般說明¶
在核心的上下文中,idmapping 可以解釋為將一系列使用者空間 ID 對映到一系列核心 ID
userspace-id:kernel-id:range
使用者空間 ID 始終是型別為 uid_t 或 gid_t 的 idmapping 的上層 idmapset 中的元素,核心 ID 始終是型別為 kuid_t 或 kgid_t 的 idmapping 的下層 idmapset 中的元素。從現在開始,“使用者空間 ID”將用於指代眾所周知的 uid_t 和 gid_t 型別,“核心 ID”將用於指代 kuid_t 和 kgid_t。
核心主要關注核心 ID。它們用於執行許可權檢查,並存儲在 inode 的 i_uid 和 i_gid 欄位中。另一方面,使用者空間 ID 是核心報告給使用者空間,或者使用者空間傳遞給核心的 ID,或者是從磁碟寫入或讀取的原始裝置 ID。
請注意,我們只關注 idmappings 的核心儲存方式,而不是使用者空間如何指定它們。
對於本文件的其餘部分,我們將以 u 作為所有使用者空間 ID 的字首,並以 k 作為所有核心 ID 的字首。idmappings 的範圍將以 r 作為字首。因此,idmapping 將寫為 u0:k10000:r10000。
例如,在此 idmapping 中,ID u1000 是從 u0 開始的上層 idmapset 或“使用者空間 idmapset”中的一個 ID。它對映到 k11000,這是從 k10000 開始的下層 idmapset 或“核心 idmapset”中的一個核心 ID。
核心 ID 始終由 idmapping 建立。此類 idmappings 與使用者名稱空間相關聯。由於我們主要關心 idmappings 的工作方式,因此我們不會關注 idmappings 是如何建立的,也不會關注它們如何在檔案系統上下文之外使用。這最好留給使用者名稱空間的解釋。
初始使用者名稱空間是特殊的。它始終具有以下形式的 idmapping
u0:k0:r4294967295
這是系統上可用 ID 的整個範圍上的恆等 idmapping。
其他使用者名稱空間通常具有非恆等 idmappings,例如
u0:k10000:r10000
當程序建立或想要更改檔案的所有權,或者檔案系統從磁碟讀取檔案的所有權時,使用者空間 ID 會立即根據與相關使用者名稱空間關聯的 idmapping 轉換為核心 ID。
例如,考慮一個由檔案系統儲存在磁碟上的檔案,該檔案歸 u1000 所有
如果檔案系統要在初始使用者名稱空間中掛載(就像大多數檔案系統一樣),則將使用初始 idmapping。正如我們所看到的,這僅僅是恆等 idmapping。這意味著從磁碟讀取的 ID
u1000將對映到 IDk1000。因此,inode 的i_uid和i_gid欄位將包含k1000。如果檔案系統要以
u0:k10000:r10000的 idmapping 掛載,則從磁碟讀取的u1000將對映到k11000。因此,inode 的i_uid和i_gid將包含k11000。
轉換演算法¶
我們已經簡要地看到可以在不同的 idmappings 之間進行轉換。現在我們將仔細看看它是如何工作的。
交叉對映¶
核心在很多地方都使用此轉換演算法。例如,它用於透過 stat() 系統呼叫系列將檔案的所有權報告回用戶空間。
如果我們從一個 idmapping 中獲得了 k11000,我們可以將其在另一個 idmapping 中向上對映。為了使此操作生效,兩個 idmappings 都需要在它們的核心 idmapsets 中包含相同的核心 ID。例如,考慮以下 idmappings
1. u0:k10000:r10000
2. u20000:k10000:r10000
我們正在第一個 idmapping 中將 u1000 向下對映到 k11000。然後,我們可以使用第二個 idmapping 的核心 idmapset 將 k11000 轉換為第二個 idmapping 中的使用者空間 ID
/* Map the kernel id up into a userspace id in the second idmapping. */
from_kuid(u20000:k10000:r10000, k11000) = u21000
請注意,我們如何透過反轉演算法來返回第一個 idmapping 中的核心 ID
/* Map the userspace id down into a kernel id in the second idmapping. */
make_kuid(u20000:k10000:r10000, u21000) = k11000
/* Map the kernel id up into a userspace id in the first idmapping. */
from_kuid(u0:k10000:r10000, k11000) = u1000
此演算法允許我們回答給定核心 ID 在給定 idmapping 中對應的使用者空間 ID 是什麼的問題。為了能夠回答這個問題,兩個 idmappings 都需要在它們各自的核心 idmapsets 中包含相同的核心 ID。
例如,當核心從磁碟讀取原始使用者空間 ID 時,它會根據與檔案系統關聯的 idmapping 將其向下對映到核心 ID。假設檔案系統以 u0:k20000:r10000 的 idmapping 掛載,並且它從磁碟讀取一個歸 u1000 所有的檔案。這意味著 u1000 將對映到 k21000,這將儲存在 inode 的 i_uid 和 i_gid 欄位中。
當用戶空間中的某人呼叫 stat() 或相關函式來獲取有關檔案的所有權資訊時,核心不能簡單地根據檔案系統的 idmapping 將 ID 向上映射回去,因為如果呼叫者正在使用 idmapping,這將給出錯誤的所有者。
因此,核心將在呼叫者的 idmapping 中將 ID 向上映射回去。假設呼叫者具有有些非常規的 idmapping u3000:k20000:r10000,則 k21000 將向上映射回 u4000。因此,使用者將看到該檔案歸 u4000 所有。
重新對映¶
可以透過兩個 idmappings 的使用者空間 idmapset 將一個 idmapping 中的核心 ID 轉換為另一個 idmapping 中的核心 ID。這等效於重新對映核心 ID。
讓我們看一個例子。我們給出了以下兩個 idmappings
1. u0:k10000:r10000
2. u0:k20000:r10000
並且在第一個 idmapping 中給出了 k11000。為了將第一個 idmapping 中的這個核心 ID 轉換為第二個 idmapping 中的核心 ID,我們需要執行兩個步驟
將核心 ID 向上對映到第一個 idmapping 中的使用者空間 ID
/* Map the kernel id up into a userspace id in the first idmapping. */ from_kuid(u0:k10000:r10000, k11000) = u1000
將使用者空間 ID 向下對映到第二個 idmapping 中的核心 ID
/* Map the userspace id down into a kernel id in the second idmapping. */ make_kuid(u0:k20000:r10000, u1000) = k21000
正如你所看到的,我們使用兩個 idmappings 中的使用者空間 idmapset 將一個 idmapping 中的核心 ID 轉換為另一個 idmapping 中的核心 ID。
這允許我們回答我們需要使用哪個核心 ID 才能在另一個 idmapping 中獲得相同的使用者空間 ID 的問題。為了能夠回答這個問題,兩個 idmappings 都需要在它們各自的使用者空間 idmapsets 中包含相同的使用者空間 ID。
請注意,我們如何透過反轉演算法輕鬆地返回第一個 idmapping 中的核心 ID
將核心 ID 向上對映到第二個 idmapping 中的使用者空間 ID
/* Map the kernel id up into a userspace id in the second idmapping. */ from_kuid(u0:k20000:r10000, k21000) = u1000
將使用者空間 ID 向下對映到第一個 idmapping 中的核心 ID
/* Map the userspace id down into a kernel id in the first idmapping. */ make_kuid(u0:k10000:r10000, u1000) = k11000
檢視此轉換的另一種方式是將其視為反轉一個 idmapping 並應用另一個 idmapping(如果兩個 idmappings 都映射了相關的使用者空間 ID)。這在處理 idmapped 掛載時會派上用場。
無效轉換¶
永遠不能將一個 idmapping 的核心 idmapset 中的 ID 用作另一個或同一 idmapping 的使用者空間 idmapset 中的 ID。雖然核心 idmapset 始終指示核心 ID 空間中的 idmapset,但使用者空間 idmapset 指示使用者空間 ID。因此,禁止以下轉換
/* Map the userspace id down into a kernel id in the first idmapping. */
make_kuid(u0:k10000:r10000, u1000) = k11000
/* INVALID: Map the kernel id down into a kernel id in the second idmapping. */
make_kuid(u10000:k20000:r10000, k110000) = k21000
~~~~~~~
同樣錯誤
/* Map the kernel id up into a userspace id in the first idmapping. */
from_kuid(u0:k10000:r10000, k11000) = u1000
/* INVALID: Map the userspace id up into a userspace id in the second idmapping. */
from_kuid(u20000:k0:r10000, u1000) = k21000
~~~~~
由於使用者空間 ID 的型別為 uid_t 和 gid_t,核心 ID 的型別為 kuid_t 和 kgid_t,因此當它們混淆時,編譯器將丟擲錯誤。因此,上面的兩個示例將導致編譯失敗。
建立檔案系統物件時的 Idmappings¶
向下對映 ID 或向上對映 ID 的概念在檔案系統開發者非常熟悉的兩個核心函式中表達,我們已經在本文件中使用過這些函式
/* Map the userspace id down into a kernel id. */
make_kuid(idmapping, uid)
/* Map the kernel id up into a userspace id. */
from_kuid(idmapping, kuid)
我們將簡要地瞭解 idmappings 如何影響檔案系統物件的建立。為簡單起見,我們將僅檢視當 VFS 已完成路徑查詢並在呼叫到檔案系統本身之前發生的事情。因此,我們關心的是呼叫 vfs_mkdir() 時發生的事情。我們還將假設我們正在其中建立檔案系統物件的目錄對於每個人都是可讀和可寫的。
建立檔案系統物件時,呼叫者將檢視呼叫者的檔案系統 ID。這些只是常規的 uid_t 和 gid_t 使用者空間 ID,但它們專門用於確定檔案所有權,這就是為什麼它們被稱為“檔案系統 ID”。它們通常與呼叫者的 uid 和 gid 相同,但可能不同。我們只假設它們始終相同,以免迷失在太多的細節中。
當呼叫者進入核心時,會發生兩件事
將呼叫者的使用者空間 ID 向下對映到呼叫者的 idmapping 中的核心 ID。(準確地說,核心將簡單地檢視儲存在當前任務憑據中的核心 ID,但為了我們的教育,我們將假裝此轉換及時發生。)
驗證呼叫者的核心 ID 是否可以在檔案系統的 idmapping 中向上對映到使用者空間 ID。
第二步很重要,因為常規檔案系統最終需要將核心 ID 向上映射回使用者空間 ID 以寫入磁碟。因此,透過第二步,核心保證可以將有效的使用者空間 ID 寫入磁碟。如果不能,核心將拒絕建立請求,以免發生檔案系統損壞的風險。
精明的讀者會意識到,這只是我們在上一節中提到的交叉對映演算法的一種變體。首先,核心根據呼叫者的 idmapping 將呼叫者的使用者空間 ID 向下對映到核心 ID,然後根據檔案系統的 idmapping 將該核心 ID 向上對映。
從實現的角度來看,值得一提的是 idmappings 是如何表示的。所有 idmappings 都取自相應的使用者名稱空間。
呼叫者的 idmapping(通常取自
current_user_ns())檔案系統的 idmapping (
sb->s_user_ns)掛載的 idmapping (
mnt_idmap(vfsmnt))
讓我們看一些呼叫者/檔案系統 idmapping 的示例,但不使用掛載 idmappings。這將展示我們可能會遇到的一些問題。之後,我們將重新審視/重新考慮這些示例,這次使用掛載 idmappings,看看它們如何解決我們之前觀察到的問題。
示例 1¶
caller id: u1000
caller idmapping: u0:k0:r4294967295
filesystem idmapping: u0:k0:r4294967295
呼叫者和檔案系統都使用恆等 idmapping
將呼叫者的使用者空間 ID 對映到呼叫者的 idmapping 中的核心 ID
make_kuid(u0:k0:r4294967295, u1000) = k1000
驗證呼叫者的核心 ID 是否可以在檔案系統的 idmapping 中對映到使用者空間 ID。
對於第二步,核心將呼叫函式
fsuidgid_has_mapping(),該函式最終歸結為呼叫from_kuid()from_kuid(u0:k0:r4294967295, k1000) = u1000
在此示例中,兩個 idmappings 相同,因此沒有什麼令人興奮的事情發生。最終,落在磁碟上的使用者空間 ID 將是 u1000。
示例 2¶
caller id: u1000
caller idmapping: u0:k10000:r10000
filesystem idmapping: u0:k20000:r10000
將呼叫者的使用者空間 ID 向下對映到呼叫者的 idmapping 中的核心 ID
make_kuid(u0:k10000:r10000, u1000) = k11000
驗證呼叫者的核心 ID 是否可以在檔案系統的 idmapping 中向上對映到使用者空間 ID
from_kuid(u0:k20000:r10000, k11000) = u-1
很明顯,雖然呼叫者的使用者空間 ID 可以成功地向下對映到呼叫者的 idmapping 中的核心 ID,但核心 ID 無法根據檔案系統的 idmapping 向上對映。因此,核心將拒絕此建立請求。
請注意,雖然此示例不太常見,因為大多數檔案系統無法以非初始 idmappings 掛載,但這是一個普遍問題,正如我們在下面的示例中看到的那樣。
示例 3¶
caller id: u1000
caller idmapping: u0:k10000:r10000
filesystem idmapping: u0:k0:r4294967295
將呼叫者的使用者空間 ID 向下對映到呼叫者的 idmapping 中的核心 ID
make_kuid(u0:k10000:r10000, u1000) = k11000
驗證呼叫者的核心 ID 是否可以在檔案系統的 idmapping 中向上對映到使用者空間 ID
from_kuid(u0:k0:r4294967295, k11000) = u11000
我們可以看到轉換總是成功的。檔案系統最終將寫入磁碟的使用者空間 ID 始終與在呼叫者的 idmapping 中建立的核心 ID 的值相同。這主要有兩個後果。
首先,我們不能允許呼叫者最終使用另一個使用者空間 ID 寫入磁碟。只有當我們以呼叫者或其他 idmapping 掛載整個檔案系統時才能這樣做。但是,該解決方案僅限於幾個檔案系統,並且不是很靈活。但是,這在容器化工作負載中是一個非常重要的用例。
其次,呼叫者通常將無法建立任何檔案或訪問具有更嚴格許可權的目錄,因為檔案系統的核心 ID 都無法在呼叫者的 idmapping 中向上對映到有效的使用者空間 ID
將原始使用者空間 ID 向下對映到檔案系統的 idmapping 中的核心 ID
make_kuid(u0:k0:r4294967295, u1000) = k1000
將核心 ID 向上對映到呼叫者的 idmapping 中的使用者空間 ID
from_kuid(u0:k10000:r10000, k1000) = u-1
示例 4¶
file id: u1000
caller idmapping: u0:k10000:r10000
filesystem idmapping: u0:k0:r4294967295
為了向用戶空間報告所有權,核心使用上一節中介紹的交叉對映演算法
將磁碟上的使用者空間 ID 向下對映到檔案系統的 idmapping 中的核心 ID
make_kuid(u0:k0:r4294967295, u1000) = k1000
將核心 ID 向上對映到呼叫者的 idmapping 中的使用者空間 ID
from_kuid(u0:k10000:r10000, k1000) = u-1
在這種情況下,交叉對映演算法失敗,因為檔案系統 idmapping 中的核心 ID 無法向上對映到呼叫者的 idmapping 中的使用者空間 ID。因此,核心會將此檔案的所有權報告為溢位 ID。
示例 5¶
file id: u1000
caller idmapping: u0:k10000:r10000
filesystem idmapping: u0:k20000:r10000
為了向用戶空間報告所有權,核心使用上一節中介紹的交叉對映演算法
將磁碟上的使用者空間 ID 向下對映到檔案系統的 idmapping 中的核心 ID
make_kuid(u0:k20000:r10000, u1000) = k21000
將核心 ID 向上對映到呼叫者的 idmapping 中的使用者空間 ID
from_kuid(u0:k10000:r10000, k21000) = u-1
同樣,在這種情況下,交叉對映演算法失敗,因為檔案系統 idmapping 中的核心 ID 無法對映到呼叫者的 idmapping 中的使用者空間 ID。因此,核心會將此檔案的所有權報告為溢位 ID。
請注意,在最後兩個示例中,如果呼叫者使用初始 idmapping,事情會很簡單。對於以初始 idmapping 掛載的檔案系統,這將很簡單。因此,我們只考慮具有 u0:k20000:r10000 idmapping 的檔案系統
將磁碟上的使用者空間 ID 向下對映到檔案系統的 idmapping 中的核心 ID
make_kuid(u0:k20000:r10000, u1000) = k21000
將核心 ID 向上對映到呼叫者的 idmapping 中的使用者空間 ID
from_kuid(u0:k0:r4294967295, k21000) = u21000
Idmapped 掛載上的 Idmappings¶
我們在上一節中看到的呼叫者的 idmapping 和檔案系統的 idmapping 不相容的示例會導致工作負載出現各種問題。對於一個更復雜但常見的示例,請考慮在主機上啟動的兩個容器。為了完全防止兩個容器相互影響,管理員通常會為兩個容器使用不同的非重疊 idmappings
container1 idmapping: u0:k10000:r10000
container2 idmapping: u0:k20000:r10000
filesystem idmapping: u0:k30000:r10000
想要為以下檔案集提供簡單的讀寫訪問許可權的管理員
dir id: u0
dir/file1 id: u1000
dir/file2 id: u2000
目前無法同時為兩個容器提供此許可權。
當然,管理員可以選擇透過 chown() 遞迴地更改所有權。例如,他們可以更改所有權,以便可以將 dir 及其下面的所有檔案從檔案系統的 idmapping 交叉對映到容器的 idmapping 中。假設他們更改所有權使其與第一個容器的 idmapping 相容
dir id: u10000
dir/file1 id: u11000
dir/file2 id: u12000
這仍然使 dir 對於第二個容器來說相當無用。事實上,dir 及其下面的所有檔案將繼續顯示為歸第二個容器的溢位 ID 所有。
或者考慮另一個越來越流行的例子。一些服務管理器(例如 systemd)實現了一個稱為“可移植主目錄”的概念。使用者可能希望在不同的機器上使用他們的主目錄,在這些機器上他們被分配了不同的登入使用者空間 ID。大多數使用者在其家中的機器上將 u1000 作為登入 ID,並且他們主目錄中的所有檔案通常由 u1000 擁有。在大學或工作中,他們可能擁有另一個登入 ID,例如 u1125。這使得在他們的工作機器上與他們的主目錄互動變得相當困難。
在這兩種情況下,遞迴地更改所有權都會產生嚴重的後果。最明顯的一個是所有權是全域性且永久地更改的。在主目錄的情況下,這種所有權更改甚至需要在使用者從他們的家庭機器切換到工作機器時每次都發生。對於非常大的檔案集,這變得越來越昂貴。
如果使用者很幸運,他們正在處理可以在使用者名稱空間內掛載的檔案系統。但這也會全域性地更改所有權,並且所有權的更改與檔案系統掛載的生命週期相關聯,即超級塊。更改所有權的唯一方法是完全解除安裝檔案系統,然後在另一個使用者名稱空間中再次掛載它。這通常是不可能的,因為這意味著當前訪問檔案系統的所有使用者都不能再訪問它。並且這意味著 dir 仍然無法在具有不同 idmappings 的兩個容器之間共享。但通常使用者甚至沒有此選項,因為大多數檔案系統都無法在容器內掛載。並且不讓它們可掛載可能是可取的,因為它不需要檔案系統處理惡意檔案系統映像。
但是上面提到的用例以及更多可以透過 idmapped 掛載來處理。它們允許在不同的掛載上公開具有不同所有權的相同 dentry 集。這是透過使用 mount_setattr() 系統呼叫將使用者名稱空間標記到掛載上來實現的。然後,與它關聯的 idmapping 用於使用我們在上面介紹的重新對映演算法從呼叫者的 idmapping 轉換為檔案系統的 idmapping,反之亦然。
Idmapped 掛載使得以臨時和區域性方式更改所有權成為可能。所有權更改僅限於特定的掛載,並且所有權更改與掛載的生命週期相關聯。所有其他使用者和公開檔案系統的位置都不受影響。
支援 idmapped 掛載的檔案系統沒有任何理由支援在使用者名稱空間內可掛載。檔案系統可以完全在 idmapped 掛載下公開以獲得相同的效果。這具有檔案系統可以將超級塊的建立留給初始使用者名稱空間中的特權使用者的優勢。
但是,將 idmapped 掛載與使用者名稱空間內可掛載的檔案系統結合使用是完全可能的。我們將在下面進一步討論這一點。
檔案系統型別 vs idmapped 掛載型別¶
隨著 idmapped 掛載的引入,我們需要區分檔案系統所有權和 VFS 物件(例如 inode)的掛載所有權。從檔案系統的角度來看,inode 的所有者可能與從 idmapped 掛載的角度來看不同。這種基本的概念區別幾乎總是應該在程式碼中清楚地表達出來。因此,為了區分 idmapped 掛載所有權和檔案系統所有權,引入了單獨的型別。
如果 uid 或 gid 是使用檔案系統或呼叫者的 idmapping 生成的,那麼我們將使用 kuid_t 和 kgid_t 型別。但是,如果 uid 或 gid 是使用掛載 idmapping 生成的,那麼我們將使用專用的 vfsuid_t 和 vfsgid_t 型別。
所有生成或將 uids 和 gids 作為引數的 VFS 助手都使用 vfsuid_t 和 vfsgid_t 型別,我們將能夠依靠編譯器來捕獲因混淆檔案系統和 VFS uids 和 gids 而引起的錯誤。
vfsuid_t 和 vfsgid_t 型別通常從 kuid_t 和 kgid_t 型別對映和對映到 kuid_t 和 kgid_t 型別,類似於 kuid_t 和 kgid_t 型別如何從 uid_t 和 gid_t 型別對映和對映到 uid_t 和 gid_t 型別
uid_t <--> kuid_t <--> vfsuid_t
gid_t <--> kgid_t <--> vfsgid_t
每當我們報告基於 vfsuid_t 或 vfsgid_t 型別的所所有權時,例如,在 stat() 期間,或者基於 vfsuid_t 或 vfsgid_t 型別在共享 VFS 物件中儲存所有權資訊時,例如,在 chown() 期間,我們可以使用 vfsuid_into_kuid() 和 vfsgid_into_kgid() 助手。
為了說明為什麼當前存在此助手,請考慮當我們從 idmapped 掛載更改 inode 的所有權時會發生什麼。在我們基於掛載 idmapping 生成 vfsuid_t 或 vfsgid_t 之後,我們稍後會提交此 vfsuid_t 或 vfsgid_t 以成為新的檔案系統範圍的所有權。因此,我們將 vfsuid_t 或 vfsgid_t 轉換為全域性 kuid_t 或 kgid_t。這可以透過使用 vfsuid_into_kuid() 和 vfsgid_into_kgid() 來完成。
請注意,每當共享 VFS 物件(例如,快取的 struct inode 或快取的 struct posix_acl)儲存所有權資訊時,都必須使用檔案系統或“全域性” kuid_t 和 kgid_t。透過 vfsuid_t 和 vfsgid_t 表達的所有權特定於 idmapped 掛載。
我們已經注意到 vfsuid_t 和 vfsgid_t 型別是基於掛載 ID 對映生成的,而 kuid_t 和 kgid_t 型別是基於檔案系統 ID 對映生成的。為了防止濫用檔案系統 ID 對映生成 vfsuid_t 或 vfsgid_t 型別,或濫用掛載 ID 對映生成 kuid_t 或 kgid_t 型別,檔案系統 ID 對映和掛載 ID 對映也是不同的型別。
所有對映到 vfsuid_t 和 vfsgid_t 型別或從這些型別對映的輔助函式都需要傳遞掛載 ID 對映,型別為 struct mnt_idmap。傳遞檔案系統或呼叫者 ID 對映會導致編譯錯誤。
類似於我們在本文件中用 u 作為所有使用者空間 ID 的字首,用 k 作為所有核心 ID 的字首,我們將用 v 作為所有 VFS ID 的字首。因此,掛載 ID 對映將寫成:u0:v10000:r10000。
重對映輔助函式¶
添加了 ID 對映函式,用於在 ID 對映之間進行轉換。它們利用了我們之前介紹的重對映演算法。我們將看看
i_uid_into_vfsuid()和i_gid_into_vfsgid()i_*id_into_vfs*id()函式將檔案系統的核心 ID 轉換為掛載的 ID 對映中的 VFS ID/* Map the filesystem's kernel id up into a userspace id in the filesystem's idmapping. */ from_kuid(filesystem, kid) = uid /* Map the filesystem's userspace id down ito a VFS id in the mount's idmapping. */ make_kuid(mount, uid) = kuid
mapped_fsuid()和mapped_fsgid()mapped_fs*id()函式將呼叫者的核心 ID 轉換為檔案系統 ID 對映中的核心 ID。這種轉換是透過使用掛載的 ID 對映重對映呼叫者的 VFS ID 來實現的/* Map the caller's VFS id up into a userspace id in the mount's idmapping. */ from_kuid(mount, kid) = uid /* Map the mount's userspace id down into a kernel id in the filesystem's idmapping. */ make_kuid(filesystem, uid) = kuid
vfsuid_into_kuid()和vfsgid_into_kgid()每當
請注意,這兩個函式彼此反轉。考慮以下 ID 對映
caller idmapping: u0:k10000:r10000
filesystem idmapping: u0:k20000:r10000
mount idmapping: u0:v10000:r10000
假設一個檔案由 u1000 擁有,從磁碟讀取。檔案系統根據其 ID 對映將此 ID 對映到 k21000。這就是儲存在 inode 的 i_uid 和 i_gid 欄位中的內容。
當呼叫者透過 stat() 查詢此檔案的所有權時,核心通常會簡單地使用交叉對映演算法,並將檔案系統的核心 ID 對映到呼叫者的 ID 對映中的使用者空間 ID。
但是當呼叫者訪問 ID 對映掛載上的檔案時,核心將首先呼叫 i_uid_into_vfsuid(),從而將檔案系統的核心 ID 轉換為掛載的 ID 對映中的 VFS ID
i_uid_into_vfsuid(k21000):
/* Map the filesystem's kernel id up into a userspace id. */
from_kuid(u0:k20000:r10000, k21000) = u1000
/* Map the filesystem's userspace id down into a VFS id in the mount's idmapping. */
make_kuid(u0:v10000:r10000, u1000) = v11000
最後,當核心將所有者報告給呼叫者時,它會將掛載的 ID 對映中的 VFS ID 轉換為呼叫者的 ID 對映中的使用者空間 ID
k11000 = vfsuid_into_kuid(v11000)
from_kuid(u0:k10000:r10000, k11000) = u1000
我們可以透過驗證建立新檔案時會發生什麼來測試此演算法是否真正有效。假設使用者正在使用 u1000 建立檔案。
核心將其對映到呼叫者的 ID 對映中的 k11000。通常,核心現在將應用交叉對映,驗證 k11000 是否可以對映到檔案系統的 ID 對映中的使用者空間 ID。由於 k11000 無法直接在檔案系統的 ID 對映中向上對映,因此此建立請求失敗。
但是,當呼叫者訪問 ID 對映掛載上的檔案時,核心將首先呼叫 mapped_fs*id(),從而根據掛載的 ID 對映將呼叫者的核心 ID 轉換為 VFS ID
mapped_fsuid(k11000):
/* Map the caller's kernel id up into a userspace id in the mount's idmapping. */
from_kuid(u0:k10000:r10000, k11000) = u1000
/* Map the mount's userspace id down into a kernel id in the filesystem's idmapping. */
make_kuid(u0:v20000:r10000, u1000) = v21000
最終寫入磁碟時,核心會將 v21000 向上對映到檔案系統的 ID 對映中的使用者空間 ID
k21000 = vfsuid_into_kuid(v21000)
from_kuid(u0:k20000:r10000, k21000) = u1000
正如我們所看到的,我們最終得到了一種可逆且因此保留資訊的演算法。從 ID 對映掛載上的 u1000 建立的檔案也將報告為由 u1000 擁有,反之亦然。
現在,讓我們簡要地重新考慮一下前面在 ID 對映掛載的上下文中失敗的示例。
重新考慮示例 2¶
caller id: u1000
caller idmapping: u0:k10000:r10000
filesystem idmapping: u0:k20000:r10000
mount idmapping: u0:v10000:r10000
當呼叫者使用非初始 ID 對映時,常見的情況是將相同的 ID 對映附加到掛載。我們現在執行三個步驟
將呼叫者的使用者空間 ID 對映到呼叫者的 idmapping 中的核心 ID
make_kuid(u0:k10000:r10000, u1000) = k11000
將呼叫者的 VFS ID 轉換為檔案系統 ID 對映中的核心 ID
mapped_fsuid(v11000): /* Map the VFS id up into a userspace id in the mount's idmapping. */ from_kuid(u0:v10000:r10000, v11000) = u1000 /* Map the userspace id down into a kernel id in the filesystem's idmapping. */ make_kuid(u0:k20000:r10000, u1000) = k21000
驗證呼叫者的核心 ID 是否可以對映到檔案系統的 ID 對映中的使用者空間 ID
from_kuid(u0:k20000:r10000, k21000) = u1000
因此,最終磁碟上的所有權將為 u1000。
重新考慮示例 3¶
caller id: u1000
caller idmapping: u0:k10000:r10000
filesystem idmapping: u0:k0:r4294967295
mount idmapping: u0:v10000:r10000
相同的轉換演算法適用於第三個示例。
將呼叫者的使用者空間 ID 對映到呼叫者的 idmapping 中的核心 ID
make_kuid(u0:k10000:r10000, u1000) = k11000
將呼叫者的 VFS ID 轉換為檔案系統 ID 對映中的核心 ID
mapped_fsuid(v11000): /* Map the VFS id up into a userspace id in the mount's idmapping. */ from_kuid(u0:v10000:r10000, v11000) = u1000 /* Map the userspace id down into a kernel id in the filesystem's idmapping. */ make_kuid(u0:k0:r4294967295, u1000) = k1000
驗證呼叫者的核心 ID 是否可以對映到檔案系統的 ID 對映中的使用者空間 ID
from_kuid(u0:k0:r4294967295, k1000) = u1000
因此,最終磁碟上的所有權將為 u1000。
重新考慮示例 4¶
file id: u1000
caller idmapping: u0:k10000:r10000
filesystem idmapping: u0:k0:r4294967295
mount idmapping: u0:v10000:r10000
為了將所有權報告給使用者空間,核心現在使用我們之前介紹的轉換演算法執行三個步驟
將磁碟上的使用者空間 ID 向下對映到檔案系統的 idmapping 中的核心 ID
make_kuid(u0:k0:r4294967295, u1000) = k1000
將核心 ID 轉換為掛載的 ID 對映中的 VFS ID
i_uid_into_vfsuid(k1000): /* Map the kernel id up into a userspace id in the filesystem's idmapping. */ from_kuid(u0:k0:r4294967295, k1000) = u1000 /* Map the userspace id down into a VFS id in the mounts's idmapping. */ make_kuid(u0:v10000:r10000, u1000) = v11000
將 VFS ID 向上對映到呼叫者的 ID 對映中的使用者空間 ID
k11000 = vfsuid_into_kuid(v11000) from_kuid(u0:k10000:r10000, k11000) = u1000
之前,呼叫者的核心 ID 無法在檔案系統的 ID 對映中進行交叉對映。透過 ID 對映掛載,現在可以透過掛載的 ID 對映將其交叉對映到檔案系統的 ID 對映中。現在將使用 u1000 建立檔案,根據掛載的 ID 對映。
重新考慮示例 5¶
file id: u1000
caller idmapping: u0:k10000:r10000
filesystem idmapping: u0:k20000:r10000
mount idmapping: u0:v10000:r10000
同樣,為了將所有權報告給使用者空間,核心現在使用我們之前介紹的轉換演算法執行三個步驟
將磁碟上的使用者空間 ID 向下對映到檔案系統的 idmapping 中的核心 ID
make_kuid(u0:k20000:r10000, u1000) = k21000
將核心 ID 轉換為掛載的 ID 對映中的 VFS ID
i_uid_into_vfsuid(k21000): /* Map the kernel id up into a userspace id in the filesystem's idmapping. */ from_kuid(u0:k20000:r10000, k21000) = u1000 /* Map the userspace id down into a VFS id in the mounts's idmapping. */ make_kuid(u0:v10000:r10000, u1000) = v11000
將 VFS ID 向上對映到呼叫者的 ID 對映中的使用者空間 ID
k11000 = vfsuid_into_kuid(v11000) from_kuid(u0:k10000:r10000, k11000) = u1000
之前,檔案的核心 ID 無法在檔案系統的 ID 對映中進行交叉對映。透過 ID 對映掛載,現在可以透過掛載的 ID 對映將其交叉對映到檔案系統的 ID 對映中。該檔案現在由 u1000 擁有,根據掛載的 ID 對映。
更改主目錄的所有權¶
我們已經在上面看到了如何在呼叫者、檔案系統或兩者都使用非初始 ID 對映時,可以使用 ID 對映掛載在 ID 對映之間進行轉換。當呼叫者使用非初始 ID 對映時,存在廣泛的用例。這主要發生在容器化工作負載的上下文中。正如我們所看到的,結果是對於使用初始 ID 對映掛載的檔案系統和使用非初始 ID 對映掛載的檔案系統,訪問檔案系統都無法正常工作,因為核心 ID 無法在呼叫者和檔案系統的 ID 對映之間進行交叉對映。
正如我們上面所看到的,ID 對映掛載透過根據掛載的 ID 對映重新對映呼叫者或檔案系統的 ID 對映來提供解決方案。
除了容器化工作負載之外,ID 對映掛載的優勢在於,它們也適用於呼叫者和檔案系統都使用初始 ID 對映的情況,這意味著主機上的使用者可以按掛載更改目錄和檔案的所有權。
考慮我們之前的示例,其中使用者在行動式儲存上擁有他們的主目錄。在家裡,他們的 ID 是 u1000,並且他們主目錄中的所有檔案都由 u1000 擁有,而在大學或工作中,他們的登入 ID 是 u1125。
隨身攜帶他們的主目錄變得有問題。他們無法輕鬆訪問他們的檔案,他們可能無法在不應用寬鬆許可權或 ACL 的情況下寫入磁碟,即使他們可以,他們最終也會得到一個令人討厭的混合檔案和目錄,這些檔案和目錄由 u1000 和 u1125 擁有。
ID 對映掛載允許解決此問題。使用者可以在他們的工作電腦或家裡的電腦上為他們的主目錄建立一個 ID 對映掛載,具體取決於他們希望最終出現在行動式儲存上的所有權是什麼。
假設他們希望磁碟上的所有檔案都屬於 u1000。當用戶將他們的行動式儲存插入他們的工作站時,他們可以設定一個作業,建立一個具有最小 ID 對映 u1000:k1125:r1 的 ID 對映掛載。因此,現在當他們建立一個檔案時,核心會執行以下我們從上面已經知道的步驟:
caller id: u1125
caller idmapping: u0:k0:r4294967295
filesystem idmapping: u0:k0:r4294967295
mount idmapping: u1000:v1125:r1
將呼叫者的使用者空間 ID 對映到呼叫者的 idmapping 中的核心 ID
make_kuid(u0:k0:r4294967295, u1125) = k1125
將呼叫者的 VFS ID 轉換為檔案系統 ID 對映中的核心 ID
mapped_fsuid(v1125): /* Map the VFS id up into a userspace id in the mount's idmapping. */ from_kuid(u1000:v1125:r1, v1125) = u1000 /* Map the userspace id down into a kernel id in the filesystem's idmapping. */ make_kuid(u0:k0:r4294967295, u1000) = k1000
驗證呼叫者的檔案系統 ID 是否可以對映到檔案系統的 ID 對映中的使用者空間 ID
from_kuid(u0:k0:r4294967295, k1000) = u1000
因此,最終檔案將使用 u1000 在磁碟上建立。
現在讓我們簡要地看一下 ID 為 u1125 的呼叫者在他們的工作電腦上會看到什麼所有權
file id: u1000
caller idmapping: u0:k0:r4294967295
filesystem idmapping: u0:k0:r4294967295
mount idmapping: u1000:v1125:r1
將磁碟上的使用者空間 ID 向下對映到檔案系統的 idmapping 中的核心 ID
make_kuid(u0:k0:r4294967295, u1000) = k1000
將核心 ID 轉換為掛載的 ID 對映中的 VFS ID
i_uid_into_vfsuid(k1000): /* Map the kernel id up into a userspace id in the filesystem's idmapping. */ from_kuid(u0:k0:r4294967295, k1000) = u1000 /* Map the userspace id down into a VFS id in the mounts's idmapping. */ make_kuid(u1000:v1125:r1, u1000) = v1125
將 VFS ID 向上對映到呼叫者的 ID 對映中的使用者空間 ID
k1125 = vfsuid_into_kuid(v1125) from_kuid(u0:k0:r4294967295, k1125) = u1125
因此,最終呼叫者將被報告該檔案屬於 u1125,這是我們的示例中呼叫者工作站上的使用者空間 ID。
放置在磁碟上的原始使用者空間 ID 是 u1000,因此當用戶將他們的主目錄帶回他們的家庭電腦時,他們在那裡被分配 u1000,使用初始 ID 對映並使用初始 ID 對映掛載檔案系統,他們將看到所有這些檔案都由 u1000 擁有。