有时候,Linux® 中的文件系统是一个相当简单的树。进程可以对本身执行 chroot(),使其文件系统树的根成为系统文件系统根的一个子目录。在树中的任何节点上,可以用来自新设备的树覆盖文件系统。
在 2000 年,Al Viro 为 Linux 引入了绑定挂载和文件系统名称空间:
- 绑定挂载(bind mount)允许从任何其他位置访问任何文件或目录。
- 文件系统名称空间(filesystem namespace)是与不同进程相关联的完全独立的文件系统树。
clone(2)Linux 手册页指出:clone(2) 系统调用创建一个新的子进程,它让子进程可以与执行调用的进程共享它的执行上下文的某些部分,比如内存空间、文件描述符表和信号处理函数表。细节参见 。
在执行 clone(2) 时,进程请求它当前的文件系统树的拷贝(更多信息见 );在此之后,新进程就拥有与原进程的文件系统树相同的拷贝。在建立拷贝之后,在这两个树中的任何挂载操作都不会影响另一个拷贝。
尽管每个进程使用单独的文件系统名称空间在理论上非常有意义,但是在实践中,完全隔离它们会造成较大的限制性。进程克隆了系统的文件系统名称空间之后,已经运行的系统守护进程无法为这个用户自动挂载 CD-ROM,因为在原文件系统名称空间中执行的挂载无法影响用户的拷贝。
2006 年引入的挂载传播(mount propagation)解决了这个问题,挂载传播定义了挂载对象之间的关系。系统用这些关系决定任何挂载对象中的挂载事件如何传播到其他挂载对象:
- 如果两个挂载对象具有共享关系,那么一个挂载对象中的挂载事件会传播到另一个挂载对象,反之亦然。
- 如果两个挂载对象形成从属(slave)关系,那么一个挂载对象中的挂载事件会传播到另一个挂载对象,但是反过来不行;在这种关系中,从属对象是事件的接收者。
传播事件的挂载对象称为共享挂载(shared mount);接收挂载事件的挂载对象称为从属挂载(slave mount)。既不传播也不接收挂载事件的挂载对象称为私有挂载(private mount)。另一种特殊的挂载对象称为不可绑定的挂载(unbindable mount),它们与私有挂载相似,但是不允许执行绑定挂载。不可绑定的挂载对于快速增长挂载对象尤其有意义(后面会进一步讨论这个概念)。
在默认情况下,所有挂载都是私有的。可以用以下命令将挂载对象显式地标为共享挂载:
1
| mount --make-shared <mount-object>
|
例如,如果 / 上的挂载必须是共享的,那么执行以下命令:
从共享挂载克隆的挂载对象也是共享的挂载;它们相互传播挂载事件。
通过执行以下命令,可以显式地将一个共享挂载转换为从属挂载:
1
| mount --make-slave <shared-mount-object>
|
从从属挂载克隆的挂载对象也是从属的挂载,它也从属于原来的从属挂载的主挂载对象。
通过执行以下命令,可以将挂载对象标为私有的:
1
| mount --make-private <mount-object>
|
通过执行以下命令,可以将挂载对象标为不可绑定的:
1
| mount --make-unbindable <mount-object>
|
最后,这些设置都可以递归地应用,这意味着它们将应用于目标挂载之下的所有挂载。
例如:
将 / 之下的所有挂载转换为共享挂载。
每个登录专用的名称空间清单 1 给出一个 PAM(pluggable authentication module,可插入身份验证模块)的部分代码,它将除根用户之外的每个用户放在一个私有名称空间中。如果 /tmp/priv/USER 目录存在,那么这个目录将被绑定挂载在用户的私有名称空间中的 /tmp 上。
清单 1. 实现每个登录专用的名称空间的 PAM 代码片段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
| #define DIRNAMSZ 200
int handle_login(const char *user)
{
int ret = 0;
struct stat statbuf;
char dirnam[DIRNAMSZ];
if (strcmp(user, "root") == 0)
return PAM_SUCCESS;
ret = unshare(CLONE_NEWNS);
if (ret) {
mysyslog(LOG_ERR, "failed to unshare mounts for %s\n", user);
return PAM_SESSION_ERR;
}
snprintf(dirnam, DIRNAMSZ, "/tmp/priv/%s", user);
ret = stat(dirnam, &statbuf);
if (ret == 0 && S_ISDIR(statbuf.st_mode)) {
ret = mount(dirnam, "/tmp", "none", MS_BIND, NULL);
if (ret) {
mysyslog(LOG_ERR, "failed to mount tmp for %s\n", user);
return PAM_SESSION_ERR;
}
} else
mysyslog(LOG_INFO, "No private /tmp for user %s\n", user);
return PAM_SUCCESS;
}
int pam_sm_open_session(pam_handle_t *pamh, int flags, int argc,
const char **argv)
{
const char *PAM_user = NULL;
char *fnam;
int ret;
ret = pam_get_user(pamh, &PAM_user, NULL);
if (ret != PAM_SUCCESS) {
mysyslog(LOG_ERR, "PAM-NS: couldn't get user\n");
return PAM_SESSION_ERR;
}
return handle_login(PAM_user);
}
|
要想使用这个 PAM 模块,可以从后面的 一节下载完整的 pam_ns.c 文件和对应的 makefile。编译它并将生成的 pam_ns.so 文件复制到 /lib/security/ 中。然后在 /etc/pam.d/login 和 /etc/pam.d/sshd 中添加以下条目:
1
| session required pam_ns.so
|
最后,为用户 USER 创建一个私有的 tmp 目录。
1
2
3
4
| mkdir /tmp/priv
chmod 000 /tmp/priv
mkdir /tmp/priv/USER
chown -R USER /tmp/priv/USER
|
现在,以根用户身份在一个终端上登录,以 USER 用户身份在另一个终端上登录。作为 USER 执行以下命令:
1
2
| touch /tmp/ab
ls /tmp
|
注意,USER 的 /tmp 只包含新创建的文件。
接下来,在根用户的终端上列出 /tmp 的内容清单;注意,这里有其他文件,但是没有 /tmp/ab。这些 /tmp 目录实际上是单独的目录。要想在根用户的终端上访问 USER 的 /tmp 目录,应该执行:
这时会看到文件 ab。接下来,在根用户的终端上,在 /mnt 上挂载某些东西:
mount(8) 和 unshare(2)Linux 手册页指出:mount(8) 命令将某一设备上的文件系统附着到一个大文件树上。unshare(2) 系统调用允许进行调用的进程用原名称空间的拷贝替代所选资源的名称空间。参见 。
注意,在根用户的终端上,/dev 的内容出现在 /mnt 下面,但是在 USER 的终端上没有出现。这两个终端的挂载树是完全独立的。可以使用 mount(8) 命令获得与挂载传播相关的指令。在默认情况下,所有挂载都是私有的。所以,在作为 USER 登录之前,可以执行以下命令:
在此之后,挂载事件就会在后面的非共享名称空间之间传播。但是,在 USER 登录之后,将 /tmp/priv/USER 挂载到 /tmp 的事件不应该传播到父名称空间。为了解决这个问题,pam_ns.so 可以将它的文件系统标为从属对象,见清单 2。
清单 2. 将用户的名称空间标为从属对象的 PAM 模块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
| #define DIRNAMSZ 200
#ifndef MS_SLAVE
#define MS_SLAVE 1<<19
#endif
#ifndef MS_REC
#define MS_REC 0x4000
#endif
int handle_login(const char *user)
{
int ret = 0;
struct stat statbuf;
char dirnam[DIRNAMSZ];
if (strcmp(user, "root") == 0)
return PAM_SUCCESS;
ret = unshare(CLONE_NEWNS);
if (ret) {
mysyslog(LOG_ERR, "failed to unshare mounts for %s\n", user);
return PAM_SESSION_ERR;
}
ret = mount("", "/", "dontcare", MS_REC|MS_SLAVE, ""));
if (ret) {
mysyslog(LOG_ERR, "failed to mark / rslave for %s\n", user);
return PAM_SESSION_ERR;
}
snprintf(dirnam, DIRNAMSZ, "/tmp/priv/%s", user);
ret = stat(dirnam, &statbuf);
if (ret == 0 && S_ISDIR(statbuf.st_mode)) {
ret = mount(dirnam, "/tmp", "none", MS_BIND, NULL);
if (ret) {
mysyslog(LOG_ERR, "failed to mount tmp for %s\n", user);
return PAM_SESSION_ERR;
}
} else
mysyslog(LOG_INFO, "No private /tmp for user %s\n", user);
return PAM_SUCCESS;
}
|
|