[TOC]

fs/mnt_idmapping.c 挂载ID映射(Mount ID Mapping) 容器内安全的文件系统访问

历史与背景

这项技术是为了解决什么特定问题而诞生的?

ID-mapped mounts 技术是为了解决在用户命名空间(User Namespaces)中,特别是容器环境下,文件系统用户和组ID(UID/GID)不匹配的核心安全问题而诞生的。

具体来说,它解决了以下痛点:

  • 容器内的root,容器外的安全:容器技术的核心优势之一是隔离。使用用户命名空间,可以实现容器内的root用户(UID 0)被映射为主机上的一个普通非特权用户(例如UID 100000)。 但问题随之而来:当把主机上的一个目录(例如 /srv/www)挂载到容器内时,这个目录及其文件的所有者是主机上的用户。容器内的root进程由于在主机上没有特权,将无法写入这个目录,使得挂载形同虚设。
  • 避免危险且低效的 chown:在ID映射挂载出现之前,唯一的解决办法是在启动容器时,递归地将挂载目录的所有者更改(chown)为主机上对应的映射后用户。这个操作不仅非常耗时,尤其是在目录包含大量文件时,而且是永久性的更改,破坏了主机上原有的文件权限体系。
  • 多容器共享数据:当多个容器需要共享同一个主机目录,但每个容器都有自己独立的ID映射时(容器A的root是主机100000,容器B的root是主机200000),chown方案完全失效。 ID映射挂载则允许为每个挂载点应用不同的映射规则。
  • 便携式家目录:在非容器场景中,当用户将自己的家目录存储在U盘等可移动设备上,并在不同的机器间使用时,该用户在不同机器上的UID可能不同,导致在新机器上无法访问自己的文件。ID映射挂载可以在挂载时动态地将U盘上的文件所有者映射为当前机器上用户的UID。

它的发展经历了哪些重要的里程碑或版本迭代?

ID映射挂载是解决此类问题的一系列探索的最终成果。

  • 早期的尝试 (shiftfs):在ID映射挂载被合并到内核主线之前,存在一个名为 shiftfs 的树外(out-of-tree)文件系统补丁。它是一个堆叠文件系统,工作原理与ID映射挂载类似,通过拦截VFS操作并动态转换UID/GID来实现。但作为树外补丁,它需要用户自行编译和维护,且兼容性有限。
  • VFS层实现:开发者们逐渐意识到,将ID映射功能直接在通用的VFS层实现是更优越的方案。经过多次迭代和讨论,Christian Brauner 提出的ID-mapped mounts补丁集最终被社区接受。
  • 并入主线内核:ID映射挂载功能最终在 Linux 5.12 内核版本中被正式合并,成为了标准的内核功能。

目前该技术的社区活跃度和主流应用情况如何?

ID映射挂载是一项非常活跃和关键的内核技术,被视为现代容器安全和“无根容器”(Rootless Containers)技术的重要基石。

  • 主流容器运行时支持:containerd, runC, LXD 等主流容器运行时都已经支持或正在积极集成ID映射挂载,以替代旧的chownshiftfs方案。
  • Kubernetes集成:Kubernetes也引入了对用户命名空间的支持,其底层实现依赖于容器运行时对ID映射挂载的支持。
  • systemd-homedsystemd 项目利用ID映射挂载来实现其 systemd-homed 功能,即上文提到的便携式家目录。
  • 文件系统支持:该功能已获得主流文件系统(如ext4, XFS, Btrfs, tmpfs, OverlayFS)的支持。

核心原理与设计

它的核心工作原理是什么?

fs/mnt_idmapping.c 的核心原理是在VFS(虚拟文件系统)层,为每个挂载点(struct vfsmount)关联一个特定的用户命名空间。这个用户命名空间定义了一套UID和GID的映射规则。当对该挂载点下的文件进行任何涉及所有权的操作时,VFS会自动应用这套规则进行ID转换。

具体流程如下:

  1. 创建映射:首先,创建一个新的用户命名空间,并在其中定义好映射关系(例如,将内部的UID 0-1000映射到外部的UID 100000-101000)。
  2. 应用到挂载点:使用新的 mount_setattr() 系统调用,将这个配置好映射的用户命名空间附加到一个新的挂载点上(通常是一个bind mount)。
  3. VFS层拦截和翻译
    • 读操作(例如 stat:当容器内的进程读取文件元数据时,内核从底层文件系统读取到的是主机上的UID(如100000)。在将结果返回给容器进程前,VFS通过挂载点关联的ID映射,将主机UID 100000 翻译成容器内的UID 0
    • 写操作(例如 chown:当容器内的root进程(UID 0)尝试创建一个新文件时,VFS在将请求传递给底层文件系统之前,会先将容器内的UID 0 翻译成主机上的UID 100000。最终,文件在物理磁盘上被记录为由用户 100000 所拥有。

这个双向翻译过程对上层应用和底层文件系统都是透明的,实现了文件所有权的动态、本地化视图。

它的主要优势体现在哪些方面?

  • 安全:这是最核心的优势。它允许容器以root权限运行,同时在主机层面将这些操作限制在一个非特权用户范围内,有效防止了容器逃逸后对主机系统的破坏。
  • 高效:所有ID转换都在内核VFS层即时完成,相比于启动容器时递归chown整个目录,ID映射挂载是瞬时完成的,极大地加快了容器启动速度并减少了I/O开销。
  • 非破坏性:ID映射是临时的、本地化的,只对特定的挂载点生效。它不会修改磁盘上文件的实际所有权,保留了主机文件系统的完整性。
  • 通用性:该机制在VFS层实现,因此理论上可以支持所有遵循POSIX权限模型的标准文件系统,无需对每个文件系统进行大规模改造。

它存在哪些已知的劣势、局限性或在特定场景下的不适用性?

  • 内核版本要求:需要Linux内核 5.12 或更高版本,这对于一些使用旧版本内核的生产环境来说是一个限制。
  • 配置复杂性:相比简单的bind mount,设置ID映射挂载需要额外创建和管理用户命名空间及其映射关系,配置过程相对复杂。
  • 文件系统兼容性:虽然设计上是通用的,但仍需要底层文件系统提供支持。一些特殊的文件系统或网络文件系统可能需要额外的工作才能完美兼容。
  • 映射数量限制:用户命名空间中的映射条目数量有限制(尽管在2015/2016年已从5个提高到340个),对于需要极其复杂映射关系的场景可能成为瓶颈。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?请举例说明。

  • 无根容器(Rootless Containers):这是ID映射挂载最典型的应用场景。开发者可以在自己的普通用户账户下运行Docker或Podman,容器运行时会自动创建一个用户命名空间,并将容器内的root映射到该开发者在主机上的UID范围。通过ID映射挂载共享目录,使得容器内的root可以无缝读写主机上属于该开发者的文件。
  • 多租户容器平台:在一个共享主机上,为不同租户(用户)运行的容器提供一个共享的基础镜像或数据卷。每个租户的容器都有自己独立的ID映射,但通过ID映射挂载,它们看到的共享数据的所有者都可以是容器内的root或某个特定用户,实现了数据的安全隔离和共享。
  • ChromeOS的Linux子系统:ChromeOS使用ID映射挂载来安全地与非特权的Linux容器共享用户的“下载”文件夹等资源。
  • 便携式家目录/外部存储:用户将家目录放在移动硬盘上,并在多台电脑上使用。电脑A上用户UID是1000,电脑B是1001。通过在挂载时设置ID映射,可以将移动硬盘上所有UID为1000的文件,在电脑B上挂载后显示为由UID 1001拥有,反之亦然。

是否有不推荐使用该技术的场景?为什么?

  • 不需要权限隔离的场景:如果容器运行的用户与主机上的用户身份一致,或者共享的数据不需要严格的权限隔离(例如,只读的公共数据),那么使用简单的bind mount会更直接、更简单。
  • 不支持POSIX权限的文件系统:在挂载如FAT32等本身不支持Unix风格所有权和权限的文件系统时,虽然ID映射挂滚可以“嫁接”一套权限模型上去,但这可能不是其设计的初衷,且效果可能与预期有差异。
  • 性能极度敏感且无安全需求的场景:尽管ID转换的开销很小,但在每秒进行数百万次文件元数据操作的极端场景下,理论上仍然存在微小的性能开销。如果完全不需要ID映射带来的安全隔离,直接访问可以免去这一层转换。

对比分析

请将其 与 其他相似技术 进行详细对比。

特性 ID-mapped Mounts (fs/mnt_idmapping.c) shiftfs (树外补丁) 递归 chown (用户空间工具)
实现方式 内核VFS层原生实现。通过mount_setattr()系统调用,将用户命名空间的ID映射附加到挂载点。 内核堆叠文件系统。作为一个独立的、覆盖在其他文件系统之上的文件系统模块实现。 用户空间命令。通过遍历文件系统,逐个调用chown()系统调用来修改磁盘上的元数据。
性能开销 极低。ID转换在内存中即时发生,几乎没有性能损失。启动时无开销。 。与ID映射挂载类似,开销很小,但作为额外的文件系统层,理论上路径会长一点。 极高。在容器启动和关闭时对大量文件进行磁盘I/O和元数据更新,非常耗时。
资源占用 忽略不计。 需要加载一个额外的内核模块。 在执行chown期间会消耗大量的CPU和I/O资源。
安全性 。内核级实现,是当前解决容器文件权限问题的标准安全方案。 中等。原理上安全,但作为树外模块,其维护、审计和分发不如主线内核可靠。 。永久性地改变了主机文件权限,可能引入安全风险,且无法处理多容器不同映射的场景。
易用性/维护 标准。自内核5.12起成为标准功能,由内核社区维护,无需额外安装。 复杂。需要用户自行从特定源码树编译、安装DKMS模块,并处理内核版本升级带来的兼容性问题。 简单chown是标准命令,但需要编写复杂的启动/关闭脚本来管理。
状态 当前标准方案 已被取代的过渡方案 应避免使用的传统/权宜之计

include/linux/uidgid.h

内核ID类型与宏定义:用户与组ID的基础设施

本代码片段定义了Linux内核中用于表示用户ID(UID)和组ID(GID)的基础设施。它包括用于类型安全初始化的宏、用于从类型封装中提取原始整数值的内联函数,以及一组关键的全局常量(如root ID和无效ID)。最核心的部分是利用条件编译(#ifdef CONFIG_MULTIUSER)为多用户系统和单用户系统提供了两种截然不同的行为,从而实现了在简化场景下的性能优化。

实现原理分析

这部分代码是内核健壮的类型系统的基础,其设计目标是类型安全和可配置性。

  1. 类型安全的初始化 (KUIDT_INIT, KGIDT_INIT):

    • Linux内核不直接使用uid_t整数,而是将其封装在kuid_t结构体中(struct kuid_t { uid_t val; };)。这样做可以利用C编译器的类型检查系统,防止开发者意外地将一个UID与一个PID(进程ID)或其它类型的整数进行比较或赋值,从而减少编程错误。
    • KUIDT_INIT(value) 宏使用C语言的复合字面量 (Compound Literal) 语法 (kuid_t){ value } 来创建一个临时的kuid_t结构体实例并初始化其val成员。这是创建这些类型安全ID的标准、简洁方式。
  2. 条件化的值提取 (__kuid_val, __kgid_val):

    • 这是此片段中最关键的特性。__kuid_val函数用于从kuid_t结构体中“解封装”,提取出原始的uid_t整数。
    • CONFIG_MULTIUSER被定义时:函数返回结构体中真实的val成员。这是标准的多用户Linux系统(如桌面版、服务器版)的行为,系统需要区分不同用户,进行完整的权限检查。
    • CONFIG_MULTIUSER未定义时:函数被编译为总是返回0。这个分支是为那些不需要多用户功能的、简单的嵌入式或专用系统设计的。通过在编译时将所有UID/GID强制视为0,内核可以优化掉大量与用户权限检查相关的代码,从而减小内核体积并提升性能。
  3. 全局常量定义:

    • GLOBAL_ROOT_UID/GID: 使用KUIDT_INIT宏为超级用户(root)的UID/GID(值为0)定义了全局、类型安全的常量。
    • INVALID_UID/GID: 同样地,为无效的UID/GID(值为-1)定义了常量。这使得代码中可以清晰地表示“无所有者”或“无效用户”的状态,提高了代码的可读性。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// KUIDT_INIT: 一个宏,用于将一个普通的整数值初始化为一个kuid_t类型的结构体。
#define KUIDT_INIT(value) (kuid_t){ value }
// KGIDT_INIT: 逻辑同上,用于初始化kgid_t。
#define KGIDT_INIT(value) (kgid_t){ value }

// 条件编译:检查内核是否配置了多用户支持(CONFIG_MULTIUSER)。
#ifdef CONFIG_MULTIUSER
// 如果支持多用户,则定义正常的ID值提取函数。
// __kuid_val: 从kuid_t结构体中提取出原始的uid_t整数值。
static inline uid_t __kuid_val(kuid_t uid)
{
return uid.val;
}

// __kgid_val: 逻辑同上,用于处理kgid_t。
static inline gid_t __kgid_val(kgid_t gid)
{
return gid.val;
}
#else
// 如果内核被配置为单用户系统(CONFIG_MULTIUSER未定义)。
// __kuid_val: 总是返回0。这会将系统中的所有用户都视为root。
static inline uid_t __kuid_val(kuid_t uid)
{
return 0;
}

// __kgid_val: 总是返回0。这会将系统中的所有组都视为root组。
static inline gid_t __kgid_val(kgid_t gid)
{
return 0;
}
#endif

// GLOBAL_ROOT_UID: 定义一个表示全局root用户的常量,其值为0。
#define GLOBAL_ROOT_UID KUIDT_INIT(0)
// GLOBAL_ROOT_GID: 定义一个表示全局root组的常量,其值为0。
#define GLOBAL_ROOT_GID KGIDT_INIT(0)

// INVALID_UID: 定义一个表示无效UID的常量,其值为-1。
#define INVALID_UID KUIDT_INIT(-1)
// INVALID_GID: 定义一个表示无效GID的常量,其值为-1。
#define INVALID_GID KGIDT_INIT(-1)

include/linux/mnt_idmapping.h

mapped_fsuid - 根据 ID 映射返回调用者的 fsuid

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* mapped_fsuid - 根据 ID 映射返回调用者的 fsuid
* @idmap: 挂载的 ID 映射
* @fs_userns: 文件系统的 ID 映射
*
* 使用此助手根据调用者的 fsuid 初始化新的 vfs 或文件系统对象。
* 一个常见的例子是初始化由创建事件(如 mkdir 或 O_CREAT)触发的新分配 inode 的 i_uid 字段。
* 其他例子包括为特定用户分配配额。
*
* 返回值:根据 @idmap 映射调用者当前的 fsuid。
*/
static inline kuid_t mapped_fsuid(struct mnt_idmap *idmap,
struct user_namespace *fs_userns)
{
return from_vfsuid(idmap, fs_userns, VFSUIDT_INIT(current_fsuid()));
}

VFS ID 到 内核 ID 转换:类型安全的恒等转换

本代码片段定义了一组用于在 vfsuid_t/vfsgid_tkuid_t/kgid_t 之间进行转换的内联函数和宏。其核心功能是提供一个明确且类型安全的机制,将一个已经经过ID映射处理的VFS层用户/组ID,转换成内核其他子系统(如凭证管理、权限检查)所使用的标准内核ID类型。这是一个纯粹的类型转换层,它不改变ID的数值,只改变其类型封装。

实现原理分析

Linux内核为了增强类型安全并支持用户命名空间,引入了kuid_tkgid_t等结构体来封装原始的uid_t整数。这可以防止开发者意外地将一个UID与一个PID(进程ID)等其他类型的整数混用。本代码片段是这个类型安全体系的一部分。

  1. 解封装 (__vfsuid_val): vfsuid_tkuid_t 都是包含一个名为 val 的整型成员的结构体。__vfsuid_val 函数的核心作用就是“解开”vfsuid_t 这个结构体包装,直接返回其内部的原始整数值 (uid_t)。
  2. 重新封装 (AS_KUIDT 宏): AS_KUIDT 宏执行相反的操作。它接收一个 vfsuid_t 类型的参数,首先通过 __vfsuid_val 获取其内部的整数值,然后利用C语言的复合字面量 (Compound Literal) (kuid_t){ ... } 来创建一个临时的、新的 kuid_t 结构体,并将提取出的整数值作为其 val 成员的初始值。
  3. 提供公共接口 (vfsuid_into_kuid): 这个 inline 函数是对 AS_KUIDT 宏的简单封装,提供了一个干净、易于调用的函数接口。当VFS代码(如generic_fillattr)需要将一个 vfsuid_t 赋值给一个 kuid_t 类型的字段时,就会调用这个函数。

整个过程可以概括为 解封装 -> 重新封装,确保了从一种带类型的ID到底层整数,再到另一种带类型的ID的转换是显式和受控的。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*
* 这个宏允许将一个vfs{g,u}id用作一个k{g,u}id,
* 主要用于比较一个映射后的值是否与一个k{g,u}id的值相同。
*/
// AS_KUIDT: 一个宏,将vfsuid_t类型的值强制转换为kuid_t类型。
#define AS_KUIDT(val) (kuid_t){ __vfsuid_val(val) }
// AS_KGIDT: 逻辑同上,用于处理kgid_t。
#define AS_KGIDT(val) (kgid_t){ __vfsgid_val(val) }

/**
* vfsuid_into_kuid - 将vfsuid转换为kuid
* @vfsuid: 要转换的vfsuid
*
* 当一个vfsuid需要被用作kuid时,可以调用此函数。
*
* 返回值: 一个与@vfsuid数值相同的kuid
*/
// vfsuid_into_kuid: 一个内联函数,提供公开的API用于将vfsuid_t转换为kuid_t。
static inline kuid_t vfsuid_into_kuid(vfsuid_t vfsuid)
{
// 内部直接调用AS_KUIDT宏来完成转换。
return AS_KUIDT(vfsuid);
}

fs/mnt_idmapping.c

nop_mnt_idmap - 用于表示无 ID 映射的挂载

1
2
3
4
5
6
7
8
/*
* 携带初始id映射0:0:4294967295,这是一个身份映射。
* 这意味着{g,u}id 0映射到{g,u}id 0,{g,u}id 1映射到{g,u}id 1,[...], {g,u}id 1000映射到{g,u}id 1000,[...]。
*/
struct mnt_idmap nop_mnt_idmap = {
.count = REFCOUNT_INIT(1),
};
EXPORT_SYMBOL_GPL(nop_mnt_idmap);

from_vfsuid 将 vfsuid 映射到文件系统 id 映射中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* from_vfsuid - 将 vfsuid 映射到文件系统 id 映射中
* @idmap: 挂载的 id 映射
* @fs_userns: 文件系统的 id 映射
* @vfsuid : 要映射的 vfsuid
*
* 将 @vfsuid 映射到文件系统 id 映射中。必须使用此函数,例如将 @vfsuid 写入 inode->i_uid。
*
* 返回: 映射到文件系统 id 映射中的 @vfsuid
*/
kuid_t from_vfsuid(struct mnt_idmap *idmap,
struct user_namespace *fs_userns, vfsuid_t vfsuid)
{
uid_t uid;

if (idmap == &nop_mnt_idmap)
return AS_KUIDT(vfsuid);
if (idmap == &invalid_mnt_idmap)
return INVALID_UID;
uid = map_id_up(&idmap->uid_map, __vfsuid_val(vfsuid));
if (uid == (uid_t)-1)
return INVALID_UID;
if (initial_idmapping(fs_userns))
return KUIDT_INIT(uid);
return make_kuid(fs_userns, uid);
}
EXPORT_SYMBOL_GPL(from_vfsuid);

VFS ID 映射核心:将文件系统UID转换为挂载点UID

本代码片段定义了Linux用户命名空间(User Namespace)机制在VFS层中的核心转换函数 make_vfsuid。其关键功能是执行一个双重映射:首先,将一个文件系统层面(即inode层面)的kuid_t从其所属的用户命名空间转换到初始用户命名空间;然后,再根据当前挂载点的ID映射规则(idmap),将该ID映射到当前用户所见的命名空间中。这个函数是容器和ID映射挂载(Idmapped Mounts)能够在文件系统上正确、安全地显示和操作文件所有权的基础。

实现原理分析

make_vfsuid 函数的逻辑通过一系列检查和映射步骤,精确地处理了复杂的ID转换场景。

  1. 快速路径检查:

    • if (idmap == &nop_mnt_idmap): 这是最高效的快速路径。nop_mnt_idmap 是一个特殊的全局变量,代表“无操作映射”。如果当前挂载点没有使用ID映射,函数会立即将输入的kuid直接封装成vfsuid_t并返回。这避免了任何不必要的计算。
    • if (idmap == &invalid_mnt_idmap): 检查是否为无效的映射,如果是则直接返回无效ID。
  2. 第一步映射:文件系统命名空间 -> 初始命名空间:

    • if (initial_idmapping(fs_userns)): 这是一个重要的优化。initial_idmapping 检查inode所属的用户命名空间(fs_userns)是否就是系统启动时的初始命名空间(init_user_ns)。如果是,说明kuid_t的值本身就是全局有效的,无需转换,可以直接通过__kuid_val提取其整数值。
    • else uid = from_kuid(fs_userns, kuid): 如果inode属于一个非初始的用户命名空间,则必须调用from_kuid。这个函数会在fs_userns的映射表中查找kuid,并将其转换为在初始命名空间中对应的uid_t整数值。
    • 如果from_kuid返回-1,表示该kuid在初始命名空间中没有对应的映射,转换失败,函数返回INVALID_VFSUID
  3. 第二步映射:初始命名空间 -> 挂载点命名空间:

    • map_id_down(&idmap->uid_map, uid): 此时,uid已经是相对于初始命名空间的ID了。这一步调用map_id_down,使用当前挂载点的ID映射表(idmap->uid_map),将这个全局ID“向下”映射到当前观察者所属的用户命名空间中。
  4. 封装结果: 最后,VFSUIDT_INIT_RAW将最终计算出的uid_t整数值封装成vfsuid_t类型并返回。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
/*
* Outside of this file vfs{g,u}id_t are always created from k{g,u}id_t,
* never from raw values. These are just internal helpers.
*/
#define VFSUIDT_INIT_RAW(val) (vfsuid_t){ val }
#define VFSGIDT_INIT_RAW(val) (vfsgid_t){ val }


static inline uid_t from_kuid(struct user_namespace *to, kuid_t kuid)
{
return __kuid_val(kuid);
}
static inline u32 map_id_down(struct uid_gid_map *map, u32 id)
{
return id;
}


/*
* 携带 0:0:4294967295 的初始 idmapping,这是一个恒等式
*映射。这意味着 {g,u}id 0 映射到 {g,u}id 0,{g,u}id 1 是
* 映射到 {g,u}id 1, [...], {g,u}id 1000 到 {g,u}id 1000, [...]。
*/
struct mnt_idmap nop_mnt_idmap = {
.count = REFCOUNT_INIT(1),
};
EXPORT_SYMBOL_GPL(nop_mnt_idmap);

/*
* 携带完整 0-4294967295 {g,u}id 范围的无效 idmapping。
* 这意味着所有 {g,u}id 都映射到 INVALID_VFS{G,U}ID。
*/
struct mnt_idmap invalid_mnt_idmap = {
.count = REFCOUNT_INIT(1),
};
EXPORT_SYMBOL_GPL(invalid_mnt_idmap);


// initial_idmapping: 检查给定的用户命名空间是否为初始命名空间。
// @ns: 要检查的用户命名空间。
// 返回值: 如果是初始命名空间则返回true,否则返回false。
static inline bool initial_idmapping(const struct user_namespace *ns)
{
// 直接将传入的命名空间指针与全局的初始命名空间init_user_ns的地址进行比较。
return ns == &init_user_ns;
}

// make_vfsuid: 根据ID映射,将一个文件系统的kuid进行转换。
// @idmap: 挂载点的ID映射表。
// @fs_userns: inode所属的文件系统的用户命名空间。
// @kuid: 需要被映射的kuid。
// 返回值: 映射后的vfsuid_t,如果无法映射则返回INVALID_VFSUID。
vfsuid_t make_vfsuid(struct mnt_idmap *idmap,
struct user_namespace *fs_userns,
kuid_t kuid)
{
uid_t uid;

// 快速路径1:如果挂载点没有ID映射(最常见的情况),直接封装并返回。
if (idmap == &nop_mnt_idmap)
return VFSUIDT_INIT(kuid);
// 检查是否为无效的映射。
if (idmap == &invalid_mnt_idmap)
return INVALID_VFSUID;

// 步骤1: 将kuid从其自身的命名空间(fs_userns)映射到初始命名空间。
// 优化:如果kuid本就属于初始命名空间,则无需查找映射表,直接提取其整数值。
if (initial_idmapping(fs_userns))
uid = __kuid_val(kuid);
else
// 否则,调用from_kuid在fs_userns的映射表中查找,转换为初始命名空间的uid_t。
uid = from_kuid(fs_userns, kuid);

// 如果步骤1的映射失败(返回-1),则整个转换失败。
if (uid == (uid_t)-1)
return INVALID_VFSUID;

// 步骤2: 将相对于初始命名空间的uid_t,根据当前挂载点的idmap,“向下”映射到当前用户空间。
// 最后,将映射得到的最终整数值封装成vfsuid_t类型。
return VFSUIDT_INIT_RAW(map_id_down(&idmap->uid_map, uid));
}
// 导出符号,使得其他内核模块(如网络文件系统)可以调用此函数。
EXPORT_SYMBOL_GPL(make_vfsuid);