Idmappings

大多数文件系统开发人员都会遇到 idmapping。它们用于从磁盘读取或写入所有权、向用户空间报告所有权或进行权限检查。本文档旨在帮助希望了解 idmapping 工作原理的文件系统开发人员。

正式说明

idmapping 本质上是将一系列 ID 转换为另一个或相同的 ID 范围。用户空间广泛使用的 idmapping 符号约定是

u:k:r

u 表示上部 idmapset U 中的第一个元素,k 表示下部 idmapset K 中的第一个元素。r 参数表示 idmapping 的范围,即映射了多少个 ID。从现在开始,我们总是会在 ID 前面加上 uk,以明确我们讨论的是上部还是下部 idmapset 中的 ID。

为了了解这在实践中是什么样子,让我们采用以下 idmapping

u22:k10000:r3

并写下它将生成的映射

u22 -> k10000
u23 -> k10001
u24 -> k10002

从数学的角度来看,UK 是良序集合,而 idmapping 是从 UK 的顺序同构。因此,UK 是顺序同构的。实际上,UK 始终是给定系统上所有可用 ID 集合的良序子集。

简要地从数学角度来看待这个问题,将有助于我们突出一些属性,这些属性使我们更容易理解如何在 idmapping 之间进行转换。例如,我们知道逆 idmapping 也是一个顺序同构

k10000 -> u22
k10001 -> u23
k10002 -> u24

鉴于我们正在处理顺序同构,并且我们正在处理子集,我们可以将 idmapping 嵌入到彼此中,也就是说,我们可以在不同的 idmapping 之间进行有意义的转换。例如,假设我们得到了三个 idmapping

1. u0:k10000:r10000
2. u0:k20000:r10000
3. u0:k30000:r10000

和 ID k11000,它是通过第一个 idmapping 将 u1000 从上部 idmapset 映射到下部 idmapset 中的 k11000 而生成的。

因为我们正在处理顺序同构子集,所以询问 ID k11000 在第二个或第三个 idmapping 中对应于什么是有意义的。使用的直接算法是应用第一个 idmapping 的逆,将 k11000 映射到 u1000。之后,我们可以使用第二个 idmapping 映射或第三个 idmapping 映射将 u1000 向下映射。第二个 idmapping 会将 u1000 向下映射到 21000。第三个 idmapping 会将 u1000 向下映射到 u31000

如果对于以下三个 idmapping 我们被给予相同的任务

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
    

我们也可以说“向下”而不是“从左向右”,也可以说“向上”而不是“从右向左”。显然,向下映射和向上映射彼此反转。

为了查看上面的简单公式是否有效,请考虑以下两个 idmapping

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_tgid_t 的 idmapping 的上部 idmapset 中的元素,而内核 ID 始终是类型为 kuid_tkgid_t 的 idmapping 的下部 idmapset 中的元素。从现在开始,“用户空间 ID”将用于指代众所周知的 uid_tgid_t 类型,而“内核 ID”将用于指代 kuid_tkgid_t

内核主要关注内核 ID。它们在执行权限检查时使用,并存储在 inode 的 i_uidi_gid 字段中。另一方面,用户空间 ID 是由内核报告给用户空间的 ID,或是由用户空间传递给内核的 ID,或是从磁盘写入或读取的原始设备 ID。

请注意,我们只关注内核存储的 idmapping,而不是用户空间如何指定它们。

对于本文档的其余部分,我们将所有用户空间 ID 都加上前缀 u,并将所有内核 ID 都加上前缀 k。idmapping 的范围将加上前缀 r。因此,idmapping 将被写成 u0:k10000:r10000

例如,在此 idmapping 中,ID u1000 是上部 idmapset 或“用户空间 idmapset”中的 ID,它以 u0 开头。它被映射到 k11000,它是下部 idmapset 或“内核 idmapset”中的内核 ID,它以 k10000 开头。

内核 ID 始终由 idmapping 创建。此类 idmapping 与用户命名空间相关联。由于我们主要关心 idmapping 的工作原理,因此我们不会关注 idmapping 的创建方式,也不会关注它们在文件系统上下文之外的使用方式。这最好留给用户命名空间的解释。

初始用户命名空间很特殊。它始终具有以下形式的 idmapping

u0:k0:r4294967295

这是一个覆盖此系统上可用 ID 的完整范围的身份 idmapping。

其他用户命名空间通常具有非身份 idmapping,例如

u0:k10000:r10000

当进程创建或想要更改文件的所有权,或者当文件系统的所有权从磁盘读取时,用户空间 ID 会根据与相关用户命名空间关联的 idmapping 立即转换为内核 ID。

例如,考虑一个由文件系统存储在磁盘上的文件,其所有者为 u1000

  • 如果文件系统要挂载在初始用户命名空间中(就像大多数文件系统一样),则将使用初始 ID 映射。正如我们所见,这仅仅是身份 ID 映射。这意味着从磁盘读取的 ID u1000 将被映射到 ID k1000。因此,inode 的 i_uidi_gid 字段将包含 k1000

  • 如果文件系统要以 u0:k10000:r10000 的 ID 映射挂载,则从磁盘读取的 u1000 将被映射到 k11000。因此,inode 的 i_uidi_gid 将包含 k11000

转换算法

我们已经简要地了解了在不同的 ID 映射之间进行转换的可能性。现在,我们将更仔细地研究其工作原理。

交叉映射

此转换算法在内核的许多地方使用。例如,当通过 stat() 系统调用系列向用户空间报告文件的所有权时,会使用它。

如果我们从一个 ID 映射中获得了 k11000,我们可以在另一个 ID 映射中向上映射该 ID。为了使此操作有效,两个 ID 映射都需要在其内核 ID 映射集中包含相同的内核 ID。例如,考虑以下 ID 映射

1. u0:k10000:r10000
2. u20000:k10000:r10000

并且我们在第一个 ID 映射中将 u1000 向下映射到 k11000。然后,我们可以使用第二个 ID 映射的内核 ID 映射集将 k11000 转换到第二个 ID 映射中的用户空间 ID

/* Map the kernel id up into a userspace id in the second idmapping. */
from_kuid(u20000:k10000:r10000, k11000) = u21000

请注意,如何通过反转算法来返回第一个 ID 映射中的内核 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 在给定 ID 映射中对应的用户空间 ID 是什么的问题。为了能够回答这个问题,两个 ID 映射都需要在其各自的内核 ID 映射集中包含相同的内核 ID。

例如,当内核从磁盘读取原始用户空间 ID 时,它会根据与文件系统关联的 ID 映射将其向下映射到内核 ID。假设文件系统以 u0:k20000:r10000 的 ID 映射挂载,并且它从磁盘读取一个由 u1000 拥有的文件。这意味着 u1000 将被映射到 k21000,这将存储在 inode 的 i_uidi_gid 字段中。

当用户空间的某个人调用 stat() 或相关函数来获取有关文件的所有权信息时,内核不能简单地根据文件系统的 ID 映射将 ID 映射回,因为如果调用者正在使用 ID 映射,这将给出错误的所有者。

因此,内核将在调用者的 ID 映射中将 ID 映射回去。假设调用者具有一些非常规的 ID 映射 u3000:k20000:r10000,那么 k21000 将映射回 u4000。因此,用户将看到此文件由 u4000 拥有。

重映射

可以通过两个 ID 映射的用户空间 ID 映射集将内核 ID 从一个 ID 映射转换到另一个 ID 映射。这等效于重映射内核 ID。

让我们看一个例子。我们得到以下两个 ID 映射

1. u0:k10000:r10000
2. u0:k20000:r10000

并且我们在第一个 ID 映射中获得 k11000。为了将此内核 ID 在第一个 ID 映射中转换为第二个 ID 映射中的内核 ID,我们需要执行两个步骤

  1. 将内核 ID 向上映射到第一个 ID 映射中的用户空间 ID

    /* Map the kernel id up into a userspace id in the first idmapping. */
    from_kuid(u0:k10000:r10000, k11000) = u1000
    
  2. 将用户空间 ID 向下映射到第二个 ID 映射中的内核 ID

    /* Map the userspace id down into a kernel id in the second idmapping. */
    make_kuid(u0:k20000:r10000, u1000) = k21000
    

如您所见,我们使用了两个 ID 映射中的用户空间 ID 映射集,以将一个 ID 映射中的内核 ID 转换为另一个 ID 映射中的内核 ID。

这使我们能够回答为了在另一个 ID 映射中获得相同的用户空间 ID,我们需要使用什么内核 ID 的问题。为了能够回答这个问题,两个 ID 映射都需要在其各自的用户空间 ID 映射集中包含相同的用户空间 ID。

请注意,如何通过反转算法轻松返回第一个 ID 映射中的内核 ID

  1. 将内核 ID 向上映射到第二个 ID 映射中的用户空间 ID

    /* Map the kernel id up into a userspace id in the second idmapping. */
    from_kuid(u0:k20000:r10000, k21000) = u1000
    
  2. 将用户空间 ID 向下映射到第一个 ID 映射中的内核 ID

    /* Map the userspace id down into a kernel id in the first idmapping. */
    make_kuid(u0:k10000:r10000, u1000) = k11000
    

看待此转换的另一种方法是将其视为反转一个 ID 映射并应用另一个 ID 映射,如果两个 ID 映射都映射了相关的用户空间 ID。这在处理 ID 映射挂载时会很有用。

无效的转换

永远不能将一个 ID 映射的内核 ID 映射集中的 ID 用作另一个或相同 ID 映射的用户空间 ID 映射集中的 ID。虽然内核 ID 映射集始终指示内核 ID 空间中的 ID 映射集,但用户空间 ID 映射集指示用户空间 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_tgid_t,而内核 ID 的类型为 kuid_tkgid_t,因此当它们混淆时,编译器将抛出错误。因此,上面的两个示例将导致编译失败。

创建文件系统对象时的 ID 映射

向下映射 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)

我们将简要了解 ID 映射如何影响创建文件系统对象。为简单起见,我们将仅查看 VFS 在调用文件系统本身之前完成路径查找后发生的情况。因此,我们关注的是调用 vfs_mkdir() 时发生的情况。我们还将假设我们在其中创建文件系统对象的目录对于每个人都是可读写的。

创建文件系统对象时,调用者将查看调用者的文件系统 ID。这些只是常规的 uid_tgid_t 用户空间 ID,但是它们专门用于确定文件所有权,这就是为什么它们被称为“文件系统 ID”。它们通常与调用者的 uid 和 gid 相同,但可能会有所不同。我们只假设它们始终相同,以免陷入太多细节。

当调用者进入内核时,会发生两件事

  1. 在调用者的 ID 映射中将调用者的用户空间 ID 向下映射到内核 ID。(确切地说,内核将简单地查看当前任务的凭据中存储的内核 ID,但为了我们的学习,我们将假装此转换是及时发生的。)

  2. 验证调用者的内核 ID 是否可以映射到文件系统的 ID 映射中的用户空间 ID。

第二步非常重要,因为常规文件系统最终需要在写入磁盘时将内核 ID 映射回用户空间 ID。因此,通过第二步,内核保证可以将有效的用户空间 ID 写入磁盘。如果无法写入,内核将拒绝创建请求,以免出现文件系统损坏的风险。

精明的读者会意识到这只是我们在上一节中提到的交叉映射算法的变体。首先,内核根据调用者的 ID 映射将调用者的用户空间 ID 向下映射到内核 ID,然后根据文件系统的 ID 映射将该内核 ID 向上映射。

从实现的角度来看,值得一提的是 ID 映射的表示方式。所有 ID 映射均来自相应的用户命名空间。

  • 调用者的 ID 映射(通常取自 current_user_ns()

  • 文件系统的 ID 映射(sb->s_user_ns

  • 挂载的 ID 映射(mnt_idmap(vfsmnt)

让我们看一些带有调用者/文件系统 ID 映射但不带有挂载 ID 映射的示例。这将展示我们可能遇到的一些问题。之后,我们将重新审视/重新考虑这些示例,这次使用挂载 ID 映射,以了解它们如何解决我们之前观察到的问题。

示例 1

caller id:            u1000
caller idmapping:     u0:k0:r4294967295
filesystem idmapping: u0:k0:r4294967295

调用者和文件系统都使用身份 ID 映射

  1. 在调用者的 ID 映射中将调用者的用户空间 ID 映射到内核 ID

    make_kuid(u0:k0:r4294967295, u1000) = k1000
    
  2. 验证调用者的内核 ID 是否可以映射到文件系统的 ID 映射中的用户空间 ID。

    对于第二步,内核将调用函数 fsuidgid_has_mapping(),该函数最终归结为调用 from_kuid()

    from_kuid(u0:k0:r4294967295, k1000) = u1000
    

在此示例中,两个 ID 映射是相同的,因此没有任何令人兴奋的事情发生。最终,落在磁盘上的用户空间 ID 将为 u1000

示例 2

caller id:            u1000
caller idmapping:     u0:k10000:r10000
filesystem idmapping: u0:k20000:r10000
  1. 在调用者的 ID 映射中将调用者的用户空间 ID 向下映射到内核 ID

    make_kuid(u0:k10000:r10000, u1000) = k11000
    
  2. 验证调用者的内核 ID 是否可以根据文件系统的 ID 映射向上映射到用户空间 ID

    from_kuid(u0:k20000:r10000, k11000) = u-1
    

显而易见的是,虽然调用者的用户空间 ID 可以成功地映射到调用者的 ID 映射中的内核 ID,但内核 ID 无法根据文件系统的 ID 映射向上映射。因此,内核将拒绝此创建请求。

请注意,虽然此示例不太常见,因为大多数文件系统都无法使用非初始 ID 映射挂载,但这是一个普遍问题,正如我们在接下来的示例中所看到的那样。

示例 3

caller id:            u1000
caller idmapping:     u0:k10000:r10000
filesystem idmapping: u0:k0:r4294967295
  1. 在调用者的 ID 映射中将调用者的用户空间 ID 向下映射到内核 ID

    make_kuid(u0:k10000:r10000, u1000) = k11000
    
  2. 验证调用者的内核 ID 是否可以根据文件系统的 ID 映射向上映射到用户空间 ID

    from_kuid(u0:k0:r4294967295, k11000) = u11000
    

我们可以看到转换始终成功。文件系统最终写入磁盘的用户空间 ID 将始终与在调用者的 ID 映射中创建的内核 ID 的值相同。这主要有两个后果。

首先,我们不能允许调用者最终以另一个用户空间 ID 写入磁盘。只有当我们使用调用者的或其他 ID 映射挂载整个文件系统时,我们才能这样做。但是该解决方案仅限于少数文件系统,而且不是很灵活。但这在容器化工作负载中是一个非常重要的用例。

其次,调用者通常无法创建任何文件或访问具有更严格权限的目录,因为文件系统的内核 ID 都不会映射到调用者 ID 映射中的有效用户空间 ID

  1. 将原始用户空间 ID 向下映射到文件系统的 ID 映射中的内核 ID

    make_kuid(u0:k0:r4294967295, u1000) = k1000
    
  2. 将内核 ID 向上映射到调用者的 ID 映射中的用户空间 ID

    from_kuid(u0:k10000:r10000, k1000) = u-1
    

示例 4

file id:              u1000
caller idmapping:     u0:k10000:r10000
filesystem idmapping: u0:k0:r4294967295

为了向用户空间报告所有权,内核使用了前一节中介绍的交叉映射算法

  1. 将磁盘上的用户空间 ID 映射到文件系统的 ID 映射中的内核 ID

    make_kuid(u0:k0:r4294967295, u1000) = k1000
    
  2. 将内核 ID 映射到调用者的 ID 映射中的用户空间 ID

    from_kuid(u0:k10000:r10000, k1000) = u-1
    

在这种情况下,交叉映射算法会失败,因为文件系统 ID 映射中的内核 ID 无法映射到调用者的 ID 映射中的用户空间 ID。因此,内核会将此文件的所有权报告为溢出 ID。

示例 5

file id:              u1000
caller idmapping:     u0:k10000:r10000
filesystem idmapping: u0:k20000:r10000

为了向用户空间报告所有权,内核使用了前一节中介绍的交叉映射算法

  1. 将磁盘上的用户空间 ID 映射到文件系统的 ID 映射中的内核 ID

    make_kuid(u0:k20000:r10000, u1000) = k21000
    
  2. 将内核 ID 映射到调用者的 ID 映射中的用户空间 ID

    from_kuid(u0:k10000:r10000, k21000) = u-1
    

同样,在这种情况下,交叉映射算法会失败,因为文件系统 ID 映射中的内核 ID 无法映射到调用者的 ID 映射中的用户空间 ID。因此,内核会将此文件的所有权报告为溢出 ID。

请注意,在最后两个示例中,如果调用者使用初始 ID 映射,事情会变得很简单。对于使用初始 ID 映射安装的文件系统,这将是微不足道的。因此,我们只考虑 ID 映射为 u0:k20000:r10000 的文件系统

  1. 将磁盘上的用户空间 ID 映射到文件系统的 ID 映射中的内核 ID

    make_kuid(u0:k20000:r10000, u1000) = k21000
    
  2. 将内核 ID 映射到调用者的 ID 映射中的用户空间 ID

    from_kuid(u0:k0:r4294967295, k21000) = u21000
    

ID 映射挂载上的 ID 映射

我们在上一节中看到的调用者的 ID 映射和文件系统的 ID 映射不兼容的示例会导致工作负载的各种问题。对于更复杂但常见的示例,请考虑在主机上启动的两个容器。为了完全防止两个容器相互影响,管理员通常可能对两个容器使用不同的、不重叠的 ID 映射

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 及其下面的所有文件从文件系统的 ID 映射交叉映射到容器的 ID 映射中。假设他们更改了所有权,使其与第一个容器的 ID 映射兼容

dir id:       u10000
dir/file1 id: u11000
dir/file2 id: u12000

这仍然会使 dir 对第二个容器相当无用。事实上,dir 及其下面的所有文件对于第二个容器来说将继续显示为溢出 ID 所拥有。

或者考虑另一个日益流行的示例。一些服务管理器(如 systemd)实现了称为“可移植主目录”的概念。用户可能希望在不同的机器上使用其主目录,在这些机器上,他们被分配了不同的登录用户空间 ID。大多数用户在其家庭机器上的登录 ID 将为 u1000,并且其主目录中的所有文件通常都由 u1000 拥有。在大学或工作场所,他们可能有另一个登录 ID,例如 u1125。这使得在他们的工作机器上与他们的主目录进行交互相当困难。

在这两种情况下,递归更改所有权都会产生严重的影响。最明显的影响是,所有权会在全局范围内永久更改。在主目录的情况下,这种所有权更改甚至需要在用户每次从他们的家庭机器切换到工作机器时发生。对于真正的大型文件集,这变得越来越昂贵。

如果用户幸运的话,他们正在处理一个可以在用户命名空间内挂载的文件系统。但这也会全局更改所有权,并且所有权更改与文件系统挂载的生命周期(即超级块)相关联。更改所有权的唯一方法是完全卸载文件系统,然后在另一个用户命名空间中重新挂载它。这通常是不可能的,因为它意味着当前访问该文件系统的所有用户都无法再访问。而且这意味着 dir 仍然无法在具有不同 ID 映射的两个容器之间共享。但通常用户甚至没有此选项,因为大多数文件系统都无法在容器内挂载。并且不让它们可挂载可能是可取的,因为它不需要文件系统处理恶意的文件系统镜像。

但是,上述用例以及更多用例可以通过 ID 映射挂载来处理。它们允许在不同的挂载点以不同的所有权公开同一组目录项。这是通过使用 mount_setattr() 系统调用来标记具有用户命名空间的挂载来实现的。然后,与它关联的 ID 映射将用于使用我们上面介绍的重新映射算法,从调用者的 ID 映射转换为文件系统的 ID 映射,反之亦然。

ID 映射挂载可以以临时和局部的方式更改所有权。所有权更改仅限于特定的挂载点,并且所有权更改与挂载点的生命周期相关联。文件系统公开的所有其他用户和位置均不受影响。

支持 ID 映射挂载的文件系统没有任何真正的理由来支持在用户命名空间内挂载。文件系统可以完全在 ID 映射挂载下公开,以获得相同的效果。这样做的好处是文件系统可以将超级块的创建留给初始用户命名空间中的特权用户。

但是,将 ID 映射挂载与可在用户命名空间内挂载的文件系统结合使用是完全可能的。我们将在下面进一步探讨这一点。

文件系统类型与 ID 映射挂载类型

随着 ID 映射挂载的引入,我们需要区分 VFS 对象(如 inode)的文件系统所有权和挂载所有权。从文件系统的角度看,inode 的所有者可能与从 ID 映射挂载的角度看不同。这种基本概念上的区别几乎总是应该在代码中清楚地表达出来。因此,为了区分 ID 映射挂载所有权和文件系统所有权,引入了单独的类型。

如果 uid 或 gid 是使用文件系统或调用者的 ID 映射生成的,那么我们将使用 kuid_tkgid_t 类型。但是,如果 uid 或 gid 是使用挂载 ID 映射生成的,那么我们将使用专用的 vfsuid_tvfsgid_t 类型。

所有生成或将 uid 和 gid 作为参数的 VFS 助手都使用 vfsuid_tvfsgid_t 类型,并且我们将能够依靠编译器来捕获源于混淆文件系统和 VFS uid 和 gid 的错误。

vfsuid_tvfsgid_t 类型通常会从 kuid_tkgid_t 类型映射而来,类似于 kuid_tkgid_t 类型如何从 uid_tgid_t 类型映射而来

uid_t <--> kuid_t <--> vfsuid_t
gid_t <--> kgid_t <--> vfsgid_t

每当我们基于 vfsuid_tvfsgid_t 类型报告所有权时,例如在 stat() 期间,或者基于 vfsuid_tvfsgid_t 类型将所有权信息存储在共享的 VFS 对象中时,例如在 chown() 期间,我们可以使用 vfsuid_into_kuid()vfsgid_into_kgid() 助手。

为了说明此助手目前存在的原因,请考虑当我们从 ID 映射挂载更改 inode 的所有权时会发生什么。在我们基于挂载 ID 映射生成了 vfsuid_tvfsgid_t 之后,我们稍后会提交此 vfsuid_tvfsgid_t,使其成为新的文件系统范围所有权。因此,我们将 vfsuid_tvfsgid_t 转换为全局的 kuid_tkgid_t。这可以通过使用 vfsuid_into_kuid()vfsgid_into_kgid() 来完成。

请注意,每当共享的 VFS 对象(例如,缓存的 struct inode 或缓存的 struct posix_acl)存储所有权信息时,必须使用文件系统或“全局” kuid_tkgid_t。通过 vfsuid_tvfsgid_t 表示的所有权特定于 ID 映射挂载。

我们已经注意到,vfsuid_tvfsgid_t 类型是基于挂载 ID 映射生成的,而 kuid_tkgid_t 类型是基于文件系统 ID 映射生成的。为了防止滥用文件系统 ID 映射来生成 vfsuid_tvfsgid_t 类型,或滥用挂载 ID 映射来生成 kuid_tkgid_t 类型,文件系统 ID 映射和挂载 ID 映射也是不同的类型。

所有映射到 vfsuid_tvfsgid_t 类型或从 vfsuid_tvfsgid_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_uidi_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

我们可以通过验证创建新文件时发生的情况来测试此算法是否真的有效。假设用户正在创建文件,其 ID 为 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 映射附加到挂载点。现在我们执行三个步骤:

  1. 在调用者的 ID 映射中将调用者的用户空间 ID 映射到内核 ID

    make_kuid(u0:k10000:r10000, u1000) = k11000
    
  2. 将调用者的 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
    
  3. 验证调用者的内核 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

相同的转换算法适用于第三个示例。

  1. 在调用者的 ID 映射中将调用者的用户空间 ID 映射到内核 ID

    make_kuid(u0:k10000:r10000, u1000) = k11000
    
  2. 将调用者的 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
    
  3. 验证调用者的内核 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

为了向用户空间报告所有权,内核现在使用我们前面介绍的转换算法执行三个步骤:

  1. 将磁盘上的用户空间 ID 映射到文件系统的 ID 映射中的内核 ID

    make_kuid(u0:k0:r4294967295, u1000) = k1000
    
  2. 将内核 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
    
  3. 将 VFS ID 向上映射到调用者的 ID 映射中的用户空间 ID

    k11000 = vfsuid_into_kuid(v11000)
    from_kuid(u0:k10000:r10000, k11000) = u1000
    

之前,调用者的内核 ID 无法在文件系统的 ID 映射中交叉映射。通过 ID 映射挂载点,现在可以通过挂载点的 ID 映射将其交叉映射到文件系统的 ID 映射中。现在将根据挂载点的 ID 映射使用 u1000 创建文件。

重新考虑示例 5

file id:              u1000
caller idmapping:     u0:k10000:r10000
filesystem idmapping: u0:k20000:r10000
mount idmapping:      u0:v10000:r10000

同样,为了向用户空间报告所有权,内核现在使用我们前面介绍的转换算法执行三个步骤:

  1. 将磁盘上的用户空间 ID 映射到文件系统的 ID 映射中的内核 ID

    make_kuid(u0:k20000:r10000, u1000) = k21000
    
  2. 将内核 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
    
  3. 将 VFS ID 向上映射到调用者的 ID 映射中的用户空间 ID

    k11000 = vfsuid_into_kuid(v11000)
    from_kuid(u0:k10000:r10000, k11000) = u1000
    

之前,文件的内核 ID 无法在文件系统的 ID 映射中交叉映射。通过 ID 映射挂载点,现在可以通过挂载点的 ID 映射将其交叉映射到文件系统的 ID 映射中。根据挂载点的 ID 映射,该文件现在由 u1000 拥有。

更改主目录的所有权

我们在上面已经看到,当调用者、文件系统或两者都使用非初始 ID 映射时,如何使用 ID 映射挂载点在 ID 映射之间进行转换。当调用者使用非初始 ID 映射时,存在广泛的用例。这主要发生在容器化工作负载的上下文中。正如我们所见,其结果是,对于使用初始 ID 映射挂载的文件系统和使用非初始 ID 映射挂载的文件系统,对文件系统的访问都无法工作,因为内核 ID 无法在调用者和文件系统的 ID 映射之间进行交叉映射。

正如我们在上面看到的,ID 映射挂载点通过根据挂载点的 ID 映射重新映射调用者或文件系统的 ID 映射来为此提供解决方案。

除了容器化的工作负载之外,ID 映射挂载点的优势还在于,当调用者和文件系统都使用初始 ID 映射时,它们也可以工作,这意味着主机上的用户可以按挂载点更改目录和文件的所有权。

考虑我们之前的示例,其中用户在便携式存储设备上拥有他们的主目录。在家时,他们的 ID 为 u1000,并且他们主目录中的所有文件都由 u1000 拥有,而在大学或工作时,他们的登录 ID 为 u1125

携带他们的主目录会变得有问题。他们无法轻易访问他们的文件,他们可能无法在不应用宽松权限或 ACL 的情况下写入磁盘,即使他们可以,他们最终也会拥有令人烦恼的 u1000u1125 拥有的文件和目录的混合。

ID 映射挂载点允许解决此问题。用户可以在他们的工作计算机或家里的计算机上为他们的主目录创建一个 ID 映射挂载点,具体取决于他们希望最终出现在便携式存储设备上的所有权。

假设他们希望磁盘上的所有文件都属于 u1000。当用户将他们的便携式存储设备插入他们工作站时,他们可以设置一个作业,该作业创建一个 ID 映射挂载点,其最小 ID 映射为 u1000:k1125:r1。因此,现在当他们创建文件时,内核会执行我们从上面已经知道的以下步骤:

caller id:            u1125
caller idmapping:     u0:k0:r4294967295
filesystem idmapping: u0:k0:r4294967295
mount idmapping:      u1000:v1125:r1
  1. 在调用者的 ID 映射中将调用者的用户空间 ID 映射到内核 ID

    make_kuid(u0:k0:r4294967295, u1125) = k1125
    
  2. 将调用者的 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
    
  3. 验证调用者的文件系统 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
  1. 将磁盘上的用户空间 ID 映射到文件系统的 ID 映射中的内核 ID

    make_kuid(u0:k0:r4294967295, u1000) = k1000
    
  2. 将内核 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
    
  3. 将 VFS ID 向上映射到调用者的 ID 映射中的用户空间 ID

    k1125 = vfsuid_into_kuid(v1125)
    from_kuid(u0:k0:r4294967295, k1125) = u1125
    

因此,最终将向调用者报告该文件属于 u1125,这是我们的示例中调用者在其工作站上的用户空间 ID。

放置在磁盘上的原始用户空间 ID 是 u1000,因此当用户将他们的主目录带回他们的家用计算机(他们被分配了 u1000,使用初始 ID 映射)并使用初始 ID 映射挂载文件系统时,他们将看到所有这些文件都由 u1000 拥有。