名称

user_namespaces-Linux用户名称空间概述

备注

多年来,Linux内核中已经添加了许多功能,这些功能仅对特权用户可用,因为它们可能会混淆set-user-ID-root应用程序。通常,允许用户名称空间中的root用户使用这些功能是安全的,因为在用户名称空间中不可能获得比用户名称空间的root用户更多的特权。

Availability

用户名称空间的使用需要使用CONFIG_USER_NS选项配置的内核。用户名称空间需要整个内核中一系列子系统的支持。在内核中配置了不受支持的子系统后,就无法配置用户名称空间支持。

与Linux 3.8一样,大多数相关子系统都支持用户名称空间,但是许多文件系统不具备在用户名称空间之间映射用户ID和组ID所需的基础结构。 Linux 3.9为许多其他不受支持的文件系统(计划9(9P),Andrew文件系统(AFS),Ceph,CIFS,CODA,NFS和OCFS2)添加了必需的基础结构支持。 Linux 3.12添加了对最后一个不受支持的主要文件系统XFS的支持。

另外参见

newgidmap(1),newuidmap(1),clone(2),ptrace(2),setns(2),unshare(2),proc(5),subgid(5),subuid(5),功能(7), cgroup_namespaces(7),凭据(7),名称空间(7),pid_namespaces(7)

内核源文件Documentation / namespaces / resource-control.txt。

USER_NAMESPACES - Linux手册页

Linux程序员手册 第7部分
更新日期: 2020-06-09

说明

有关名称空间的概述,请参见namespaces(7)。

用户名称空间隔离了与安全性相关的标识符和属性,尤其是用户ID和组ID(请参阅凭据(7)),根目录,密钥(请参见keyrings(7))和功能(请参见capability(7))。进程的用户ID和组ID在用户名称空间的内部和外部可以不同。特别是,进程可以在用户名称空间之外具有正常的非特权用户ID,而同时在名称空间内部具有用户ID 0;换句话说,该进程对用户名称空间内的操作具有完全特权,但对于名称空间外的操作则没有特权。

Nested namespaces, namespace membership

用户名称空间可以嵌套;也就是说,每个用户名称空间(除了初始("根")名称空间除外)都具有父用户名称空间,并且可以具有零个或多个子用户名称空间。父用户名称空间是通过使用CLONE_NEWUSER标志调用unshare(2)或clone(2)来创建用户名称空间的进程的用户名称空间。

内核(自版本3.11起)施加32个嵌套嵌套的用户名称空间级别限制。调用unshare(2)或clone(2)可能导致超过此限制,但失败,并显示错误EUSERS。

每个进程都是一个用户命名空间的成员。通过fork(2)或clone(2)创建而没有CLONE_NEWUSER标志的进程是与其父进程相同的用户名称空间的成员。如果单线程进程在该命名空间中具有CAP_SYS_ADMIN,则它可以将另一个用户命名空间与setns(2)联接。这样,它将在该命名空间中获得全套功能。

使用CLONE_NEWUSER标志调用clone(2)或unshare(2)会使新的子进程(对于clone(2))或调用者(对于unshare(2))成为该调用创建的新用户名称空间的成员。

NS_GET_PARENT ioctl(2)操作可用于发现用户名称空间之间的父级关系。参见ioctl_ns(2)。

Capabilities

clone(2)使用CLONE_NEWUSER标志创建的子进程从新用户名称空间中的一组完整功能开始。同样,使用unshare(2)创建新用户名称空间或使用setns(2)加入现有用户名称空间的过程将获得该名称空间中的全部功能。另一方面,即使创建了新的名称空间,该进程在父级(对于clone(2)而言)或先前的用户(对于unshare(2)和setns(2)而言)中也没有任何功能或由根用户加入(即,根名称空间中用户ID为0的进程)。

请注意,对execve(2)的调用将导致以通常的方式重新计算流程的功能(请参阅capabilities(7))。因此,除非该进程在名称空间内的用户ID为0,或者该可执行文件具有非空的可继承功能掩码,否则该进程将丢失所有功能。请参阅下面有关用户和组ID映射的讨论。

使用CLONE_NEWUSER标志对clone(2)或unshare(2)的调用或将调用者移动到另一个用户名称空间的setns(2)的调用会将"安全位"标志(请参阅功能(7))设置为其默认值(子级(对于clone(2))或调用方(对于unshare(2)或setns(2))中的所有标志都被禁用。请注意,由于在调用setns(2)之后,调用方不再具有其原始用户名称空间的功能,因此进程无法通过使用一对setns保留其用户名称空间成员身份的同时重置其"安全位"标志。 2)调用以移至另一个用户名称空间,然后返回其原始用户名称空间。

确定进程在特定用户名称空间中是否具有功能的规则如下:

1.
如果进程是用户名称空间的成员,并且在其有效功能集中具有该功能,则该进程将在该用户名称空间内具有该功能。流程可以通过各种方式获得其有效功能集的功能。例如,它可以执行设置用户ID程序或具有关联文件功能的可执行文件。此外,如已描述的,进程可以通过clone(2),unshare(2)或setns(2)的作用获得功能。
2.
如果进程在用户名称空间中具有功能,那么它在所有子(以及进一步删除的子代)名称空间中也具有该功能。
3.
创建用户名称空间后,内核会将创建过程的有效用户ID记录为名称空间的"所有者"。驻留在用户名称空间的父级中且其有效用户ID与名称空间的所有者匹配的进程具有名称空间中的所有功能。根据先前的规则,这意味着该进程在所有进一步删除的后代用户名称空间中也具有所有功能。 NS_GET_OWNER_UID ioctl(2)操作可用于发现名称空间所有者的用户ID。参见ioctl_ns(2)。

Effect of capabilities within a user namespace

在用户名称空间内具有功能允许进程仅对由该名称空间控制的资源执行操作(需要特权)。换句话说,在用户名称空间中具有功能允许进程对由用户名称空间拥有(关联)的(非用户)名称空间管理的资源执行特权操作(请参阅下一部分)。

另一方面,有许多特权操作会影响与任何名称空间类型都不相关的资源,例如,更改系统(即日历)时间(由CAP_SYS_TIME控制),加载内核模块(由CAP_SYS_MODULE控制),并创建一个设备(由CAP_MKNOD控制)。只有在初始用户名称空间中具有特权的进程才能执行此类操作。

将CAP_SYS_ADMIN保留在拥有进程的安装名称空间的用户名称空间中,可以使该进程创建绑定安装并安装以下类型的文件系统:

*
/ proc(从Linux 3.8开始)
*
/ sys(从Linux 3.8开始)
*
devpts(从Linux 3.9开始)
*
tmpfs(5)(从Linux 3.9开始)
*
ramfs(从Linux 3.9开始)
*
mqueue(从Linux 3.9开始)
*
bpf(自Linux 4.4起)

将CAP_SYS_ADMIN保留在拥有进程的cgroup命名空间的用户命名空间中允许(自Linux 4.6起)该进程安装cgroup版本2文件系统和cgroup版本1命名层次结构(即,使用none,name =选项安装的cgroup文件系统)。

将CAP_SYS_ADMIN保留在拥有进程的PID命名空间的用户命名空间中允许(自Linux 3.8起)该进程挂载/ proc文件系统。

但是请注意,只能通过将CAP_SYS_ADMIN保留在初始用户名称空间中的进程来完成基于块的挂载文件系统。

Interaction of user namespaces and other types of namespaces

从Linux 3.8开始,无特权的进程可以创建用户名称空间,而其他类型的名称空间可以仅使用调用者的用户名称空间中的CAP_SYS_ADMIN功能来创建。

创建非用户名称空间时,该名称空间由创建名称空间时创建过程所属的用户名称空间拥有。对由非用户名称空间控制的资源的特权操作要求该进程在拥有非用户名称空间的用户名称空间中具有必要的功能。

如果在单个clone(2)或unshare(2)调用中指定了CLONE_NEWUSER以及其他CLONE_NEW *标志,则将确保首先创建用户名称空间,从而给子级(clone(2))或调用方(unshare(2) )对调用创建的其余名称空间的特权。因此,非特权调用者可以指定这些标志组合。

通过clone(2)或unshare(2)创建新的名称空间(而不是用户名称空间)时,内核会将创建过程的用户名称空间记录为新名称空间的所有者。 (不能更改此关联。)当新名称空间中的进程随后执行对由该名称空间隔离的全局资源的特权操作时,将根据内核与之关联的用户名称空间中的进程功能来执行权限检查。新的名称空间。例如,假设某个进程尝试更改主机名(sethostname(2)),该主机名由UTS命名空间控制。在这种情况下,内核将确定哪个用户名称空间拥有该进程的UTS名称空间,并检查该进程在该用户名称空间中是否具有所需的功能(CAP_SYS_ADMIN)。

NS_GET_USERNS ioctl(2)操作可用于发现拥有非用户名称空间的用户名称空间。参见ioctl_ns(2)。

User and group ID mappings: uid_map and gid_map

创建用户名称空间后,它开始时不会将用户ID(组ID)映射到父用户名称空间。 / proc / [pid] / uid_map和/ proc / [pid] / gid_map文件(从Linux 3.5开始可用)公开了进程pid的用户名称空间中用户ID和组ID的映射。可以读取这些文件以查看用户命名空间中的映射,并将其写入(一次)以定义映射。

以下各段中的描述解释了uid_map的详细信息; gid_map完全相同,但是"用户ID"的每个实例都由"组ID"代替。

uid_map文件公开了从进程pid的用户名称空间到打开uid_map的进程的用户名称空间的用户ID映射(但请参阅下面的说明)。换句话说,在不同的用户名称空间中的进程从特定的uid_map文件读取时可能会看到不同的值,具体取决于读取进程的用户名称空间的用户ID映射。

uid_map文件中的每一行都指定两个用户名称空间之间一系列连续用户ID的一对一映射。 (首次创建用户名称空间时,该文件为空。)每行中的规范采用由空格分隔的三个数字的形式。前两个数字分别指定两个用户名称空间中的起始用户ID。第三个数字指定映射范围的长度。详细地,这些字段解释如下:

(1)
进程pid的用户名称空间中用户ID范围的开始。
(2)
The start of the range of user IDs to which the user IDs specified by field one map. How field two is interpreted depends on whether the process that opened uid_map

and the process
pid

are in the same user namespace, as follows:

a)
如果两个进程位于不同的用户命名空间中:字段2是打开uid_map的进程的用户命名空间中一系列用户ID的开头。
b)
如果两个进程在同一用户名称空间中:字段2是进程pid的父用户名称空间中用户ID范围的开始。这种情况使uid_map的打开程序(此处的常见情况是打开/ proc / self / uid_map)可以查看用户ID到创建该用户名称空间的进程的用户名称空间的映射。
(3)
在两个用户名称空间之间映射的用户ID范围的长度。

返回用户ID(组ID)的系统调用-例如,getuid(2),getgid(2)和stat(2)返回的结构中的凭据字段-返回用户ID(组ID)映射到调用者的用户名称空间。

当进程访问文件时,其用户ID和组ID被映射到初始用户名称空间中,以便在创建文件时进行权限检查和分配ID。当进程通过stat(2)检索文件用户ID和组ID时,这些ID的映射方向相反,从而产生相对于进程用户ID和组ID映射的值。

初始用户名称空间没有父名称空间,但是为了保持一致,内核为此名称空间提供了虚拟用户和组ID映射文件。从初始名称空间中的shell查看uid_map文件(gid_map是相同的)显示:

$ cat /proc/$$/uid_map
         0          0 4294967295

该映射告诉我们,该命名空间中以用户ID 0开始的范围映射到(不存在的)父命名空间中以0开始的范围,并且该范围的长度是最大的32位无符号整数。这样就不会映射4294967295(32位带符号-1值)。这是故意的:(uid_t)-1在多个接口(例如setreuid(2))中用作指定"无用户ID"的方式。将(uid_t)-1保留为未映射且不可用的情况可确保使用这些接口时不会造成混乱。

Defining user and group ID mappings: writing to uid_map and gid_map

创建新的用户名称空间后,可以将名称空间中进程之一的uid_map文件写入一次,以定义新用户名称空间中用户ID的映射。尝试多次向用户名称空间中的uid_map文件写入一次失败,并显示错误EPERM。类似的规则适用于gid_map文件。

写入uid_map(gid_map)的行必须符合以下规则:

*
这三个字段必须是有效数字,最后一个字段必须大于0。
*
行以换行符终止。
*
文件中的行数有限制。在Linux 4.14和更早版本中,此限制(任意)设置为5行。从Linux 4.15开始,限制为340行。此外,写入文件的字节数必须小于系统页面大小,并且写入必须在文件的开头执行(即,lseek(2)和pwrite(2)不能用于写入文件中的非零偏移量)。
*
每行中指定的用户ID(组ID)的范围不能与其他任何行中的范围重叠。在最初的实现中(Linux 3.8),该要求通过一种简单的实现得到满足,该实现提出了进一步的要求,即连续行的字段1和字段2中的值都必须按数字升序排列,从而避免了某些其他有效的映射创建。 Linux 3.9和更高版本解决了此限制,允许任何有效的非重叠映射集。
*
必须至少将一行写入文件。

违反上述规则的写入将失败,并显示错误EINVAL。

为了使进程写入/ proc / [pid] / uid_map(/ proc / [pid] / gid_map)文件,必须满足以下所有要求:

1.
写入进程在进程pid的用户名称空间中必须具有CAP_SETUID(CAP_SETGID)功能。
2.
写入过程必须位于进程pid的用户名称空间中,或者位于进程pid的父用户名称空间中。
3.
映射的用户ID(组ID)必须在父用户名称空间中具有映射。
4.
One of the following two cases applies:
*
Either

the writing process has the
CAP_SETUID

(CAP_SETGID)

capability in the
parent

user namespace.

+
没有进一步的限制:该过程可以映射到父用户名称空间中的任意用户ID(组ID)。
*
Or

otherwise all of the following restrictions apply:

+
写入uid_map(gid_map)的数据必须由一行组成,该行将父用户名称空间中的写入过程的有效用户ID(组ID)映射到用户名称空间中的用户ID(组ID)。
+
写入过程必须具有与创建用户名称空间的过程相同的有效用户ID。
+
对于gid_map,在写入gid_map之前,必须首先通过将dq deny dq写入/ proc / [pid] / setgroups文件(参见下文)来拒绝使用setgroups(2)系统调用。

违反上述规则的写入将失败,并显示错误EPERM。

Interaction with system calls that change process UIDs or GIDs

在尚未写入uid_map文件的用户名称空间中,更改用户ID的系统调用将失败。同样,如果尚未写入gid_map文件,则更改组ID的系统调用将失败。写入uid_map和gid_map文件之后,只有映射的值可以用于更改用户和组ID的系统调用中。

对于用户ID,相关的系统调用包括setuid(2),setfsuid(2),setreuid(2)和setresuid(2)。对于组ID,相关的系统调用包括setgid(2),setfsgid(2),setregid(2),setresgid(2)和setgroups(2)。

在写入/ proc / [pid] / gid_map之前,将dq deny dq写入/ proc / [pid] / setgroups文件将永久禁用用户名称空间中的setgroups(2),并允许在不使用的情况下写入/ proc / [pid] / gid_map在父用户名称空间中具有CAP_SETGID功能。

The /proc/[pid]/setgroups file

如果允许包含进程pid的用户名称空间中的进程使用setgroups(2)系统调用,则/ proc / [pid] / setgroups文件将显示字符串dq allow dq。如果在该用户名称空间中不允许使用setgroups(2),它将显示dq deny dq。请注意,无论/ proc / [pid] / setgroups文件中的值如何(以及该进程的功能如何),如果尚未设置/ proc / [pid] / gid_map,也不允许调用setgroups(2) 。

特权进程(在名称空间中具有CAP_SYS_ADMIN功能的特权进程)可以在将此用户名称空间的组ID映射写入文件/ proc / [pid] / gid_map之前,将字符串dq allow dq或dq deny dq写入该文件中。 。编写字符串dq deny dq可防止用户名称空间中的任何进程使用setgroups(2)。

上一段中描述的限制的实质是,仅在由于未设置/ proc / [pid] / gid_map而不允许调用setgroups(2)的情况下才允许写入/ proc / [pid] / setgroups 。这样可以确保进程无法从允许setgroups(2)的状态过渡到拒绝setgroups(2)的状态;进程只能从不允许的setgroups(2)过渡到允许的setgroups(2)。

初始用户名称空间中此文件的默认值为dq allow dq。

一旦将/ proc / [pid] / gid_map写入其中(具有在用户名称空间中启用setgroups(2)的效果),就不再可以通过将dq deny dq写入/ proc /来禁止setgroups(2)。 [pid] / setgroups(写入失败,错误为EPERM)。

子用户名称空间从其父级继承/ proc / [pid] / setgroups设置。

如果setgroups文件的值为dq deny dq,则随后无法在此用户名称空间中重新启用setgroups(2)系统调用(通过将dq allow dq写入文件)。 (这样做会失败,并显示错误EPERM。)此限制还向下传播到该用户名称空间的所有子用户名称空间。

/ proc / [pid] / setgroups文件是在Linux 3.19中添加的,但由于解决了一个安全问题,因此被反向移植到许多较早的稳定内核系列中。该问题涉及具有" rwx --- rwx"之类的权限的文件。与"其他"文件相比,此类文件对"组"的权限较少。这意味着使用setgroups(2)删除组可能允许访问以前没有的进程文件。在存在用户名称空间之前,这不是问题,因为只有特权进程(具有CAP_SETGID功能的进程)才能调用setgroups(2)。但是,随着用户名称空间的引入,无特权的进程就有可能创建用户具有所有特权的新名称空间。然后,这允许以前没有特权的用户删除组,从而获得他们以前没有的文件访问权限。添加了/ proc / [pid] / setgroups文件来解决此安全问题,方法是拒绝无特权进程使用setgroups(2)删除组的任何途径。

Unmapped user and group IDs

在许多地方,未映射的用户ID(组ID)可能会暴露给用户空间。例如,在为该命名空间定义了用户ID映射之前,新用户命名空间中的第一个进程可以调用getuid(2)。在大多数情况下,未映射的用户ID会转换为溢出用户ID(组ID);溢出用户ID(组ID)的默认值为65534。请参见proc(5)中对/ proc / sys / kernel / overflowuid和/ proc / sys / kernel / overflowgid的描述。

以这种方式映射未映射ID的情况包括返回用户ID(getuid(2),getgid(2)等)的系统调用,通过UNIX域套接字传递的凭证,stat(2)返回的凭证,waitid( 2),以及System V IPC" ctl" IPC_STAT操作,/ proc / [pid] / status和/ proc / sysvipc / *中的文件公开的凭据,通过soirnfo_t中的si_uid字段返回的凭据(带有信号(请参阅sigaction(2)),写入流程记帐文件的凭据(请参阅acct(5))以及随POSIX消息队列通知返回的凭据(请参见mq_notify(3))。

在一种值得注意的情况下,未映射的用户ID和组ID不会转换为相应的溢出ID值。当查看第二个字段没有映射的uid_map或gid_map文件时,该字段显示为4294967295(-1为无符号整数)。

Accessing files

为了确定非特权进程访问文件时的权限,实际上将进程凭证(UID,GID)和文件凭证映射回它们在初始用户名称空间中的内容,然后进行比较以确定该进程的权限在文件上。使用凭证加上权限掩码可访问性模型的其他对象也是如此,例如System V IPC对象

Operation of file-related capabilities

在对其他用户或组拥有的文件执行操作时,某些功能允许进程绕过各种由内核强制执行的限制。这些功能是:CAP_CHOWNCAP_DAC_OVERRIDECAP_DAC_READ_SEARCH,CAP_FOWNER和CAP_FSETID。

在用户名称空间内,如果进程具有与文件相关的功能,则这些功能允许进程绕过规则,这意味着:

*
该过程在其用户名称空间中具有相关的有效能力;和
*
文件的用户ID和组ID在用户名称空间中都具有有效的映射。

CAP_FOWNER功能在某种程度上受到了特殊对待:只要至少文件的用户ID在用户名称空间中具有映射(即文件的组ID不需要有效的映射),它就允许进程绕过相应的规则。

Set-user-ID and set-group-ID programs

当用户名称空间中的进程执行set-user-ID(set-group-ID)程序时,名称空间中该进程的有效用户(组)ID会更改为为该用户的用户(组)ID映射的值。文件。但是,如果文件的用户或组ID在名称空间中没有映射,则将静默忽略set-user-ID(set-group-ID)位:执行新程序,但进程的有效用户(组)ID保持不变。 (这反映了执行驻留在使用MS_NOSUID标志挂载的文件系统上的set-user-ID或set-group-ID程序的语义,如mount(2)中所述。)

Miscellaneous

当进程的用户ID和组ID通过UNIX域套接字传递到另一个用户名称空间中的进程时(请参见unix(7)中对SCM_CREDENTIALS的描述),它们将根据接收进程的用户和组转换为相应的值。 ID映射。

出版信息

这个页面是Linux手册页项目5.08版的一部分。有关项目的说明、有关报告错误的信息以及此页面的最新版本,请访问https://www.kernel.org/doc/man-pages/

遵循规范

命名空间是特定于Linux的功能。

示例

下面的程序旨在允许试验用户名称空间以及其他类型的名称空间。它创建命令行选项指定的名称空间,然后在这些名称空间内执行命令。程序内部的comment和usage()函数提供了对该程序的完整说明。以下shell会话演示了其用法。

首先,我们看一下运行时环境:

$ uname -rs     # Need Linux 3.8 or later
Linux 3.8.0
$ id -u         # Running as unprivileged user
1000
$ id -g
1000

现在,在新的用户(-U),安装(-m)和PID(-p)命名空间中启动新的Shell,并将用户ID(-M)和组ID(-G)1000映射到用户命名空间中的0:

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

外壳具有PID 1,因为它是新PID名称空间中的第一个进程:

bash$ echo $$
1

挂载新的/ proc文件系统并列出在新的PID名称空间中可见的所有进程,这表明外壳程序看不到PID名称空间之外的任何进程:

bash$ mount -t proc proc /proc
bash$ ps ax
  PID TTY      STAT   TIME COMMAND
    1 pts/3    S      0:00 bash
   22 pts/3    R+     0:00 ps ax

在用户名称空间内,外壳程序具有用户ID和组ID 0,以及一整套允许的有效功能:

bash$ cat /proc/$$/status | egrep 'ha[UG]id'
Uid:    0       0       0       0
Gid:    0       0       0       0
bash$ cat /proc/$$/status | egrep 'haCap(Prm|Inh|Eff)'
CapInh: 0000000000000000
CapPrm: 0000001fffffffff
CapEff: 0000001fffffffff

Program source

/* userns_child_exec.c

   Licensed under GNU General Public License v2 or later

   Create a child process that executes a shell command in new
   namespace(s); allow UID and GID mappings to be specified when
   creating a user namespace.
*/
#define _GNU_SOURCE
#include <sched.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <signal.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <limits.h>
#include <errno.h>

/* A simple error-handling function: print an error message based
   on the value in aqerrnoaq and terminate the calling process */

#define errExit(msg)    do { perror(msg); exit(EXIT_FAILURE); \
                        } while (0)

struct child_args {
    char **argv;        /* Command to be executed by child, with args */
    int    pipe_fd[2];  /* Pipe used to synchronize parent and child */
};

static int verbose;

static void
usage(char *pname)
{
    fprintf(stderr, "Usage: %s [options] cmd [arg...]\n\n", pname);
    fprintf(stderr, "Create a child process that executes a shell "
            "command in a new user namespace,\n"
            "and possibly also other new namespace(s).\n\n");
    fprintf(stderr, "Options can be:\n\n");
#define fpe(str) fprintf(stderr, "    %s", str);
    fpe("-i          New IPC namespace\n");
    fpe("-m          New mount namespace\n");
    fpe("-n          New network namespace\n");
    fpe("-p          New PID namespace\n");
    fpe("-u          New UTS namespace\n");
    fpe("-U          New user namespace\n");
    fpe("-M uid_map  Specify UID map for user namespace\n");
    fpe("-G gid_map  Specify GID map for user namespace\n");
    fpe("-z          Map useraqs UID and GID to 0 in user namespace\n");
    fpe("            (equivalent to: -M aq0 <uid> 1aq -G aq0 <gid> 1aq)\n");
    fpe("-v          Display verbose messages\n");
    fpe("\n");
    fpe("If -z, -M, or -G is specified, -U is required.\n");
    fpe("It is not permitted to specify both -z and either -M or -G.\n");
    fpe("\n");
    fpe("Map strings for -M and -G consist of records of the form:\n");
    fpe("\n");
    fpe("    ID-inside-ns   ID-outside-ns   len\n");
    fpe("\n");
    fpe("A map string can contain multiple records, separated"
        " by commas;\n");
    fpe("the commas are replaced by newlines before writing"
        " to map files.\n");

    exit(EXIT_FAILURE);
}

/* Update the mapping file aqmap_fileaq, with the value provided in
   aqmappingaq, a string that defines a UID or GID mapping. A UID or
   GID mapping consists of one or more newline-delimited records
   of the form:

       ID_inside-ns    ID-outside-ns   length

   Requiring the user to supply a string that contains newlines is
   of course inconvenient for command-line use. Thus, we permit the
   use of commas to delimit records in this string, and replace them
   with newlines before writing the string to the file. */

static void
update_map(char *mapping, char *map_file)
{
    int fd, j;
    size_t map_len;     /* Length of aqmappingaq */

    /* Replace commas in mapping string with newlines */

    map_len = strlen(mapping);
    for (j = 0; j < map_len; j++)
        if (mapping[j] == aq,aq)
            mapping[j] = aq\naq;

    fd = open(map_file, O_RDWR);
    if (fd == -1) {
        fprintf(stderr, "ERROR: open %s: %s\n", map_file,
                strerror(errno));
        exit(EXIT_FAILURE);
    }

    if (write(fd, mapping, map_len) != map_len) {
        fprintf(stderr, "ERROR: write %s: %s\n", map_file,
                strerror(errno));
        exit(EXIT_FAILURE);
    }

    close(fd);
}

/* Linux 3.19 made a change in the handling of setgroups(2) and the
   aqgid_mapaq file to address a security issue. The issue allowed
   *unprivileged* users to employ user namespaces in order to drop
   The upshot of the 3.19 changes is that in order to update the
   aqgid_mapsaq file, use of the setgroups() system call in this
   user namespace must first be disabled by writing "deny" to one of
   the /proc/PID/setgroups files for this namespace.  That is the
   purpose of the following function. */

static void
proc_setgroups_write(pid_t child_pid, char *str)
{
    char setgroups_path[PATH_MAX];
    int fd;

    snprintf(setgroups_path, PATH_MAX, "/proc/%ld/setgroups",
            (long) child_pid);

    fd = open(setgroups_path, O_RDWR);
    if (fd == -1) {

        /* We may be on a system that doesnaqt support
           /proc/PID/setgroups. In that case, the file wonaqt exist,
           and the system wonaqt impose the restrictions that Linux 3.19
           added. Thataqs fine: we donaqt need to do anything in order
           to permit aqgid_mapaq to be updated.

           However, if the error from open() was something other than
           the ENOENT error that is expected for that case,  let the
           user know. */

        if (errno != ENOENT)
            fprintf(stderr, "ERROR: open %s: %s\n", setgroups_path,
                strerror(errno));
        return;
    }

    if (write(fd, str, strlen(str)) == -1)
        fprintf(stderr, "ERROR: write %s: %s\n", setgroups_path,
            strerror(errno));

    close(fd);
}

static int              /* Start function for cloned child */
childFunc(void *arg)
{
    struct child_args *args = (struct child_args *) arg;
    char ch;

    /* Wait until the parent has updated the UID and GID mappings.
       See the comment in main(). We wait for end of file on a
       pipe that will be closed by the parent process once it has
       updated the mappings. */

    close(args->pipe_fd[1]);    /* Close our descriptor for the write
                                   end of the pipe so that we see EOF
                                   when parent closes its descriptor */
    if (read(args->pipe_fd[0], &ch, 1) != 0) {
        fprintf(stderr,
                "Failure in child: read from pipe returned != 0\n");
        exit(EXIT_FAILURE);
    }

    close(args->pipe_fd[0]);

    /* Execute a shell command */

    printf("About to exec %s\n", args->argv[0]);
    execvp(args->argv[0], args->argv);
    errExit("execvp");
}

#define STACK_SIZE (1024 * 1024)

static char child_stack[STACK_SIZE];    /* Space for childaqs stack */

int
main(int argc, char *argv[])
{
    int flags, opt, map_zero;
    pid_t child_pid;
    struct child_args args;
    char *uid_map, *gid_map;
    const int MAP_BUF_SIZE = 100;
    char map_buf[MAP_BUF_SIZE];
    char map_path[PATH_MAX];

    /* Parse command-line options. The initial aq+aq character in
       the final getopt() argument prevents GNU-style permutation
       of command-line options. Thataqs useful, since sometimes
       the aqcommandaq to be executed by this program itself
       has command-line options. We donaqt want getopt() to treat
       those as options to this program. */

    flags = 0;
    verbose = 0;
    gid_map = NULL;
    uid_map = NULL;
    map_zero = 0;
    while ((opt = getopt(argc, argv, "+imnpuUM:G:zv")) != -1) {
        switch (opt) {
        case aqiaq: flags |= CLONE_NEWIPC;        break;
        case aqmaq: flags |= CLONE_NEWNS;         break;
        case aqnaq: flags |= CLONE_NEWNET;        break;
        case aqpaq: flags |= CLONE_NEWPID;        break;
        case aquaq: flags |= CLONE_NEWUTS;        break;
        case aqvaq: verbose = 1;                  break;
        case aqzaq: map_zero = 1;                 break;
        case aqMaq: uid_map = optarg;             break;
        case aqGaq: gid_map = optarg;             break;
        case aqUaq: flags |= CLONE_NEWUSER;       break;
        default:  usage(argv[0]);
        }
    }

    /* -M or -G without -U is nonsensical */

    if (((uid_map != NULL || gid_map != NULL || map_zero) &&
                !(flags & CLONE_NEWUSER)) ||
            (map_zero && (uid_map != NULL || gid_map != NULL)))
        usage(argv[0]);

    args.argv = &argv[optind];

    /* We use a pipe to synchronize the parent and child, in order to
       ensure that the parent sets the UID and GID maps before the child
       calls execve(). This ensures that the child maintains its
       capabilities during the execve() in the common case where we
       want to map the childaqs effective user ID to 0 in the new user
       namespace. Without this synchronization, the child would lose
       its capabilities if it performed an execve() with nonzero
       user IDs (see the capabilities(7) man page for details of the
       transformation of a processaqs capabilities during execve()). */

    if (pipe(args.pipe_fd) == -1)
        errExit("pipe");

    /* Create the child in new namespace(s) */

    child_pid = clone(childFunc, child_stack + STACK_SIZE,
                      flags | SIGCHLD, &args);
    if (child_pid == -1)
        errExit("clone");

    /* Parent falls through to here */

    if (verbose)
        printf("%s: PID of child created by clone() is %ld\n",
                argv[0], (long) child_pid);

    /* Update the UID and GID maps in the child */

    if (uid_map != NULL || map_zero) {
        snprintf(map_path, PATH_MAX, "/proc/%ld/uid_map",
                (long) child_pid);
        if (map_zero) {
            snprintf(map_buf, MAP_BUF_SIZE, "0 %ld 1", (long) getuid());
            uid_map = map_buf;
        }
        update_map(uid_map, map_path);
    }

    if (gid_map != NULL || map_zero) {
        proc_setgroups_write(child_pid, "deny");

        snprintf(map_path, PATH_MAX, "/proc/%ld/gid_map",
                (long) child_pid);
        if (map_zero) {
            snprintf(map_buf, MAP_BUF_SIZE, "0 %ld 1", (long) getgid());
            gid_map = map_buf;
        }
        update_map(gid_map, map_path);
    }

    /* Close the write end of the pipe, to signal to the child that we
       have updated the UID and GID maps */

    close(args.pipe_fd[1]);

    if (waitpid(child_pid, NULL, 0) == -1)      /* Wait for child */
        errExit("waitpid");

    if (verbose)
        printf("%s: terminating\n", argv[0]);

    exit(EXIT_SUCCESS);
}
日期:2019-08-20 18:02:04 来源:oir作者:oir