为什么设备节点的绑定安装会在 tmpfs 的根目录中与 EACCES 中断?

R..*_*ICE 4 linux mount namespace bind-mount

设置容器/沙箱的一个常见场景是希望在新的 tmpfs 中创建一组最小的设备节点(而不是暴露主机/dev),我知道的唯一(非特权)方法是通过绑定安装想要的进去。我正在使用(内部unshare -mc --keep-caps)的命令是:

mkdir /tmp/x
mount -t tmpfs none /tmp/x
touch /tmp/x/null
mount -o bind /dev/null /tmp/x/null
Run Code Online (Sandbox Code Playgroud)

打算将支架移动到/dev. 但是,即使在执行移动之前,运行也会echo > /tmp/x/null产生“权限被拒绝”错误(EACCES )。

然而,如果我另外执行:

mkdir /tmp/x/y
touch /tmp/x/y/null
mount -o bind /dev/null /tmp/x/y/null
echo > /tmp/x/y/null
Run Code Online (Sandbox Code Playgroud)

写入成功,因为它应该。我已经解决了很多问题,但找不到应该发生这种情况的根本原因或原因。可以通过将绑定安装的节点放在一个子目录中并将它们的符号链接放在将成为 new 的文件系统的顶层来解决它/dev,但这似乎不是必需的。

这是怎么回事?对此有合理的解释吗?还是某些访问控制逻辑出错了?

Dan*_*ver 5

嗯,这似乎是一个非常有趣的效果,这是三种机制结合在一起的结果。

第一个(微不足道的)点是,当您将某些内容重定向到文件时,shell 会打开目标文件,并带有O_CREAT选项以确保该文件不存在时将被创建。

要考虑的第二件事是它/tmp/x是一个tmpfs挂载点,而它/tmp/x/y是一个普通目录。鉴于您tmpfs在没有选项的情况下进行挂载,挂载点的权限会自动更改,以便它成为全球可写的并且有一个粘性位(1777,这是 的常用权限集/tmp,所以这感觉像是一个理智的默认值),而 的权限/tmp/x/y是可能0755(取决于您的umask)。

最后,难题的第三部分是您设置用户命名空间的方式:您指示unshare(1)将主机用户的 UID/GID 映射到新命名空间中的相同 UID/GID。这是新命名空间中的唯一映射,因此尝试在父/子命名空间之间转换任何其他 UID 将导致所谓的溢出 UID,默认情况下为65534nobody用户(请参阅user_namespaces(7),部分Unmapped user and group IDs)。这使得/dev/null(及其绑定安装)由nobody子用户命名空间内部拥有(因为在子用户命名空间中没有主机root用户的映射):

$ ls -l /dev/null
crw-rw-rw- 1 nobody nobody 1, 3 Nov 25 21:54 /dev/null
Run Code Online (Sandbox Code Playgroud)

将所有事实结合在一起,我们得出以下echo > /tmp/x/null结论:尝试使用O_CREAT选项打开一个现有文件,而该文件驻留在世界可写的粘性目录中并且由 拥有nobody,他不是包含它的目录的所有者。

现在,一个openat(2)字一个字地仔细阅读:

EACCES

指定O_CREAT时,启用protected_fifos或protected_regular sysctl,文件已存在且为FIFO或普通文件,文件所有者既不是当前用户也不是包含目录的所有者,包含目录是两个世界- 或组可写和粘性。具体参见proc(5)中/proc/sys/fs/protected_fifos和/proc/sys/fs/protected_regular的说明。

这不是很棒吗?这看起来很像我们的情况...除了手册页介绍普通文件和 FIFO 而没有介绍设备节点的事实。

好吧,让我们来看看实际实现 this代码。我们可以看到,本质上,它首先检查必须成功的异常情况(第一个if),然后如果粘性目录是全局可写的(第二个if,第一个条件),它只会拒绝任何其他情况的访问:

$ ls -l /dev/null
crw-rw-rw- 1 nobody nobody 1, 3 Nov 25 21:54 /dev/null
Run Code Online (Sandbox Code Playgroud)

因此,如果目标文件是一个字符设备(不是常规文件或 FIFO),O_CREAT当该文件位于全局可写粘性目录中时,内核仍然拒绝打开它。

为了证明我找到的原因是正确的,我们可以检查在以下任何一种情况下问题是否消失:

  • mount tmpfswith -o mode=777— 这不会使挂载点有粘性位;
  • 打开/tmp/x/nullO_WRONLY,但没有O_CREAT选项(为了测试这一点,编写一个调用open("/tmp/x/null", O_WRONLY | O_CREAT)和的程序open("/tmp/x/null", O_WRONLY),然后编译并运行它strace -e trace=openat以查看每次调用的返回值)。

我不知道是否这种行为应被视为一个内核错误或没有,但对于文档openat(2)显然并不涵盖所有在这个系统调用的情况下,居然失败EACCES

  • 该功能是在 https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=30aba6656f61ed44cba445a3c0d38b296fa9e8f5 中添加的。在我看来,它就像一个错误:提交消息没有提到任何关于其他类型文件的内容,它有效地为它们强制启用了“保护”模式。 (2认同)