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
拥有。