专栏首页巫山跬步命名空间介绍之五:用户命名空间

命名空间介绍之五:用户命名空间

继续我们的命名空间系列文章,本文看一下用户命名空间,大部分实现于 Linux 3.8。(剩余的工作是 XFS其它文件系统中的一些改动;后者合并于 3.9)。用户命名空间与用户和组 ID 相映射。这意味着一个进程在某个用户命名空间内的用户和组 ID 可以与用户命名空间外的不同。最重要的是,一个进程可以在一个命名空间外有一个非 0 的用户 ID ,同时在命名空间内有一个为 0 的用户 ID;换句话说,进程在一个用户命名空间外没有特权,但在用户命名空间内有 root 特权。

创建一个用户命名空间

用户命名空间被带有 CLONE_NEWUSER 标志的 clone() 或 unshare() 创建。自 Linux 3.8 开始(不像用来创建其它类型的命名空间的标志),创建一个用户命名空间并不需要特权。在接下来的例子中,所有的用户命名空间都被 ID 为 1000 的非特权用户创建。

为了开始探究用户命名空间,我们将看一个小例子 demo_userns.c,该程序在新的用户空间中创建了一个子进程。该子进程仅展示其有效用户和组 ID 及 capabilities。以非特权用户运行该程序,输出如下:

$ id -u          # Display effective user ID of shell process
1000
$ id -g          # Effective group ID of shell
1000
$ ./demo_userns 
eUID = 65534;  eGID = 65534;  capabilities: =ep

程序的输出展示了一些有趣的细节。其中之一是被分配给子进程的 capabilities。字符串“=ep”(库函数 cap_to_text() 产生,可将 capability 转为文本)说明即便该程序被非特权账户运行,该子进程仍有全部的权限和有效的 capabilities。当一个用户命名空间被创建,其内的第一个进程将被赋予该命名空间中的所有权限。这允许该进程在命名空间内的其它进程创建之前,执行该命名空间内任意必需的初始化操作。

第二个有趣的地方是子进程的用户和组 ID。之前提过,一个进程的用户和组 ID 在一个用户命名空间内、外可以不同。但是,需要将用户命名空间内的用户 ID 映射到用户命名空间外的用户 ID;组 ID 也一样。这样,当一个在用户命名空间内的进程执行影响系统范围较大的操作时,系统可以执行恰当的权限检查。

返回进程的用户和组 ID 的系统调用 --- 例如,getuid() 和 getgid() --- 总是返回调用进程所在的用户命名空间内的凭证。如果一个用户 ID 没有映射到用户命名空间内,那么系统调用会返回 /proc/sys/kernel/overflowuid 文件中的值,在标准系统中默认为 65534。最初,一个用户命名空间并没有用户 ID 映射,所以命名空间内的用户 ID 均映射到该值。同样,一个新的用户命名空间没有对应于组 ID 的映射,所有没有映射的组 ID 都会映射到 /proc/sys/kernel/overflowgid(与 overflowuid 默认值相同)。

还有一个上述输出未能体现的重点。尽管新进程拥有新用户命名空间中的所有权限,但在父命名空间中没有 capabilities。无论调用 clone() 的进程的 capabilities 和凭证是什么样的。尤其是,即便是 root 调用 clone(CLONE_NEWUSER),子进程在父命名空间中也没有 capabilities。

最后一点是,命名空间可以嵌套;也就是说,每个用户命名空间(最初的用户命名空间除外)都有一个父用户命名空间,并且可以有 0 个或多个子用户命名空间。用户命名空间中的进程可通过调用带有 CLONE_NEWUSER 标志的 clone() 或 unshare() 生成当前用户命名空间的子用户命名空间。接下来阐述用户空间的父-子关系。

映射用户和组 ID

通常,创建一个新用户命名空间后的第一步是定义一个用户和组 ID 的映射,将被该命名空间内的新进程使用。这可通过将映射信息写入对应于用户命名空间中某个进程的 /proc/pid/uid_map 和 /proc/pid/gid_map 文件来完成。(最初,这两个文件是空的。)此信息由一行或多行组成,每行包含三个用空格分隔的值:

ID-inside-ns   ID-outside-ns   length

ID-inside-ns 和 length 值一起定义命名空间内 ID 的范围,这些 ID 范围将映射到命名空间外相同长度的 ID 范围。ID-outside-ns 值指定外部范围的起点。如何解释 ID-outside-ns 取决于打开文件 /proc/PID/uid_map(或 /proc/PID/gid_map)的进程是否与进程 PID 在同一个用户命名空间中:

  • 如果两个进程位于同一命名空间中,则 ID-outside-ns 为进程 PID 的父用户命名空间中的用户 ID(组 ID)。常见情况是,进程正在写入自己的映射文件(/proc/self/uid_map 或 /proc/self/gid_map)。
  • 如果两个进程位于不同的命名空间中,那么 ID-outside-ns 为打开 opening/proc/pid/uid_map(/proc/pid/gid_map)的进程所在用户命名空间中的用户 ID(组 ID)。然后,该写进程将定义自己用户命名空间的映射。

再次调用 demo_userns 程序,但这次只调用一个命令行参数(任何字符串)。程序会循环,每隔几秒显示凭证和 capabilities:

$ ./demo_userns x
eUID = 65534;  eGID = 65534;  capabilities: =ep
eUID = 65534;  eGID = 65534;  capabilities: =ep

现在,切换到另一个终端窗口 --- 在另一个命名空间(即运行 demo-userns 进程的父用户命名空间)中运行 shell 的终端,并在 demo-userns 创建的新用户命名空间中为子进程创建一个用户 ID 映射:

$ ps -C demo_userns -o 'pid uid comm'      # Determine PID of clone child
    PID   UID COMMAND 
    4712  1000 demo_userns                    # This is the parent
    4713  1000 demo_userns                    # Child in a new user namespace
$ echo '0 1000 1' > /proc/4713/uid_map

回到运行 demo_userns 的窗口,可以看到:

eUID = 0;  eGID = 65534;  capabilities: =ep

父用户命名空间中的用户 ID 1000(之前映射到 65534)已映射到 demo-userns 创建的用户命名空间中的用户 ID 0。自此,新用户命名空间中处理此用户 ID 的所有操作都将看到数字 0,而父用户命名空间中相应的操作将仍然看到用户 ID 1000。

我们同样可以创建新用户命名空间中组 ID 的映射。切换到另一个终端窗口时,我们为父用户命名空间中的组 ID 1000 创建一个到新用户命名空间中的组 ID 0 的映射:

$ echo '0 1000 1' > /proc/4713/gid_map

回到运行 demo_userns 程序的窗口,可以看到该改变反映在了有效用户组 ID 上。

eUID = 0;  eGID = 0;  capabilities: =ep

写映射文件的规则

写 uid_map 文件时会有一些规则;类似的规则也适用于写 gid_map 文件。最重要的规则如下。

每个命名空间的映射只能被定义一次:对用户命名空间的一个进程的 uid_map 文件仅能执行一次写入(可能包含多个以换行符分隔的记录)。此外,目前可以写入文件的行数限制为 5 行(将来可能会有其它限制)。

/proc/PID/uid_map 文件由创建命名空间的用户 ID 拥有,并且只能由该用户(或特权用户)写入。此外,必须满足以下所有要求:

  • 写进程必须在进程 PID 的用户命名空间中具有 CAP_SETUID(gid_map 为 CAP_SETGID)capability。
  • 无论什么 capabilities,写进程都必须位于进程 PID 的用户命名空间中或进程 PID 的父用户命名空间中。
  • 以下条件必须满足: - 写入 uid_map(gid_map)的数据由一行组成,该行将(仅)父用户命名空间中写进程的有效用户 ID(组 ID)映射到用户命名空间中的用户 ID(组 ID)。此规则允许用户命名空间中的初始进程(即 clone() 创建的子进程)为自己的用户 ID(组 ID)写映射。 - 如果进程在父用户命名空间中具有 CAP_SETUID(gid_map 为 CAP_SETGID)capability,则可以定义父用户命名空间中的任意父用户 ID(组 ID)的映射。如前所述,新用户命名空间中的初始进程在父命名空间中没有任何 capabilities。因此,只有父命名空间中的进程才能编写父用户命名空间中 ID 的映射。

Capabilities, execve(), 和用户 ID 0

本系列的前面文章中,开发了 ns_child_exec 程序。该程序使用 clone() 在新命名空间中创建一个子进程,并在子进程中执行一个 shell 命令。

使用该程序在一个新用户空间中执行一个 shell,然后在该 shell 中定义新用户命名空间的用户 ID 映射。这样的话,会有如下问题:

$ ./ns_child_exec -U  bash
$ echo '0 1000 1' > /proc/$$/uid_map       # $$ is the PID of the shell
bash: echo: write error: Operation not permitted

该错误是因为 shell 没有新用户命名空间中的 capabilities,从下面命令的输出可以看出:

$ id -u         # Verify that user ID and group ID are not mapped
65534
$ id -g
65534
$ cat /proc/$$/status | egrep 'Cap(Inh|Prm|Eff)'
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000

在执行 bash shell 的 execve() 调用中出现该问题:当有非零用户 ID 的进程执行 execve() 时,将清除该进程的 capability (Capabilities(7) 手册页详细介绍了 execve() 对 capabilities 的处理。)

为避免此问题,必须在执行 execve() 之前在用户命名空间内创建用户 ID 映射。这在 ns_child_exec 程序中是不可能的;为此,我们需要一个增强版的程序。

userns_child_exec.c 程序执行与 ns_child_exec 程序执行相同的任务,并有相同的命令行界面,但它可有两个附加的命令行选项 -M 和 -G。这些选项接受用于定义新用户命名空间的用户和组 ID 映射的字符串参数。例如,以下命令将新用户命名空间中的用户 ID 1000 和组 ID 1000 映射到 0:

$ ./userns_child_exec -U -M '0 1000 1' -G '0 1000 1' bash

这次,成功地更新了映射文件,并且看到了 shell 输出了预期的用户 ID,组 ID 和 capabilities:

$ id -u
0
$ id -g
0
$ cat /proc/$$/status | egrep 'Cap(Inh|Prm|Eff)'
CapInh: 0000000000000000
CapPrm: 0000001fffffffff
CapEff: 0000001fffffffff

userns_child_exec 程序中有一些巧妙之处。首先,父进程(clone() 的调用者)或子进程可以更新新用户命名空间中的用户 ID 和组 ID 映射。然而,根据上述规则,子进程只能定义自身有效用户 ID 的映射。只有父进程可以定义子进程的任意用户和组 ID 的映射。此外,父进程必须具有适当的 capabilities,即 CAP_SETUID、CAP_SETGID 和(父进程打开映射文件所需的权限)CAP_DAC_OVERRIDE。

此外,父进程必须在子进程调用 execve() 之前更新映射文件(否则我们就遇到了上面描述的问题,在调用 execve() 期间子进程将失去 capabilities)。为此,这两个进程需使用一个管道进行同步;程序源代码中有注释。

查看用户和组 ID 映射

到目前为止的示例展示了通过 /proc/PID/uid_map 和 /proc/PID/gid_map 文件来定义映射的用法。还可用这些文件查看控制进程的映射。当写入这些文件时,第二个(ID-outside-ns)值的解释取决于打开文件的进程。如果打开文件的进程与进程 PID 在同一个用户命名空间中,则 ID-outside-ns 是关于父用户命名空间定义的。如果打开文件的进程位于不同的用户命名空间中,则会根据打开文件的进程的用户命名空间定义 ID-outside-ns。

我们可以通过创建几个运行 shell 的用户命名空间,并检查命名空间中进程的 uid_map 文件来说明这一点。首先,使用运行 shell 的进程来创建新的用户命名空间:

$ id -u            # Display effective user ID
1000
$ ./userns_child_exec -U -M '0 1000 1' -G '0 1000 1' bash
$ echo $$          # Show shell's PID for later reference
2465
$ cat /proc/2465/uid_map
            0       1000          1
$ id -u            # Mapping gives this process an effective user ID of 0
0

切换到另一个终端窗口,创建一个使用不同用户和组 ID 映射的同级用户命名空间:

$ ./userns_child_exec -U -M '200 1000 1' -G '200 1000 1' bash
$ cat /proc/self/uid_map
        200       1000          1
$ id -u            # Mapping gives this process an effective user ID of 200
200
$ echo $$          # Show shell's PID for later reference
2535

继续留在第二个终端窗口,该终端运行在第二个用户命名空间,看一下另一个用户命名空间中进程的用户 ID 映射:

$ cat /proc/2465/uid_map
             0        200          1

上述输出显示,另一个用户命名空间中的用户 ID 0 映射到此命名空间中的用户 ID 200。注意,同一个命令在另一个用户命名空间中执行时输出不同,因为内核根据从文件中读取的用户命名空间来生成 ID-outside-ns 值。

回到第一个终端,查看第二个用户命名空间中进程的的用户 ID 映射,可以看到如下相反的映射:

$ cat /proc/2535/uid_map
        200          0          1

再次,此处的输出与执行于另一个用户命名空间中的相同命令的输出不同,因为 ID-outside-ns 值是根据从文件中读取的进程的用户命名空间生成的。当然,第一个命名空间中的用户 ID 0 和第二个用户命名空间中的用户 ID 200 均映射到最初的命名空间中的用户 ID 1000。可以在运行于最初的用户命名空间中的第三个 shell 执行如下命令:

$ cat /proc/2465/uid_map
            0       1000          1
$ cat /proc/2535/uid_map
        200       1000          1

结束语

本文中,我们看了用户命名空间的一些概念:创建一个用户命名空间,使用用户和组 ID 映射文件的用法,以及与用户命名空间和 capabilities 的交互。

如之前的文章所述,实现用户命名空间的动机之一是让非 root 应用程序访问以前仅限于 root 用户的功能。在传统的 UNIX 系统中,为了防止非特权用户操纵特权程序的运行时环境(这可能会以意外或不希望的方式影响这些程序的操作),各种功能都仅限于 root 用户。

用户命名空间允许进程(在命名空间之外没有权限)具有 root 权限,同时将该权限的范围限制在命名空间,结果是进程无法在更大的系统中操作特权程序的运行时环境。为了有意义地使用这些 root 特权,我们需要将用户名空间与其他类型的名称空间结合起来,该主题将构成本系列下一篇文章的主题。


原文:https://lwn.net/Articles/532593/

公众号:Geek乐园

博客:https://blog.csdn.net/u012319493/article/details/102819429

原文链接:https://lwn.net/Articles/532593/

原文作者:Michael Kerrisk

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 命名空间介绍之六:用户命名空间的延伸

    本文中,继续上周关于用户命名空间的讨论。特别的,我们看一下更多有关与用户命名空间、capabilities 的交互及用户命名空间与其它类型的命名空间的结合。本文...

    谛听
  • 命名空间介绍之一:总览

    Linux 3.8 合并窗口接受了 Eric Biederman 的大量用户命名空间及相关的补丁。尽管仍有一些细节待完成,例如,许多 Linux 文件系统还不知...

    谛听
  • 命名空间介绍之二:API

    命名空间将全局系统资源包装在一个抽象中,使得命名空间中的进程认为它们拥有独立的资源实例。命名空间可用于多种目的,最重要的是实现容器,一种轻量级虚拟化技术。本系列...

    谛听
  • 爬取简书26万+用户信息:数据可视化

    简书上有哪些优质用户?有多少大V粉丝数上万,获赞数上万?小透明的自己能排到多少位?大V之间相互关注情况如何?签约作者有多少人......

    古柳_DesertsX
  • [Web安全]PHP伪协议

    [Web安全]PHP伪协议 最近php伪协议的各种神奇妙用好像突然又常常提到了,php中支持的伪协议有下面这么多 复制代码 file:// — 访问本地文件...

    安恒网络空间安全讲武堂
  • 有点优雅的处理你的 Java 异常

    本文仅按照业务系统开发角度描述异常的一些处理看法.不涉及java的异常基础知识,可以自行查阅 《Java核心技术 卷I》 和 《java编程思想》 可以得到更多...

    芋道源码
  • P1993 小 K 的农场

    题目描述 小 K 在 Minecraft 里面建立很多很多的农场,总共 n 个,以至于他自己都忘记了每个 农场中种植作物的具体数量了,他只记得一些含糊的信息(共...

    attack
  • 观点 | 有道CEO周枫:四个理由告诉你,为什么手机端深度学习是个大机会

    网易高级副总裁,网易有道CEO周枫 响应更快(不需要网络通信延迟),节省流量(不需要上传数据),可以实时处理视频(实时上传和处理视频不够快),对开发者更便宜(不...

    AI科技大本营
  • 2019数据库面试题:三大范式理解(实例超全解析)

    数据库表的每一列都是不可分割的基本数据项,同一列中不能有多个值,即实体中的某个属性不能有多个值或者不能有重复的属性。(保持数据的原子性)

    葆宁
  • 这种场景你还写ifelse你跟孩子坐一桌去吧

    if else,并不是一个非常坏的关键字,只不过有人把他用坏了。尤其在接到产品需求如下这样;日期需求紧急程度程序员(话外音)星期一.早上猿哥哥,老板说要搞一下营...

    小傅哥

扫码关注云+社区

领取腾讯云代金券

玩转腾讯云 有奖征文活动