![在非 root 用户命名空间中,在 nosuid、nodev 文件系统上,为什么绑定挂载成功但重新挂载失败?](https://linux22.com/image/15568/%E5%9C%A8%E9%9D%9E%20root%20%E7%94%A8%E6%88%B7%E5%91%BD%E5%90%8D%E7%A9%BA%E9%97%B4%E4%B8%AD%EF%BC%8C%E5%9C%A8%20nosuid%E3%80%81nodev%20%E6%96%87%E4%BB%B6%E7%B3%BB%E7%BB%9F%E4%B8%8A%EF%BC%8C%E4%B8%BA%E4%BB%80%E4%B9%88%E7%BB%91%E5%AE%9A%E6%8C%82%E8%BD%BD%E6%88%90%E5%8A%9F%E4%BD%86%E9%87%8D%E6%96%B0%E6%8C%82%E8%BD%BD%E5%A4%B1%E8%B4%A5%EF%BC%9F.png)
在 Linux 用户命名空间中,作为非 root,我将 mount 绑定/tmp/foo
到其自身。这样就成功了。
然后,我尝试重新安装/tmp/foo
为只读。如果/tmp
使用nosuid
或挂载nodev
,则重新挂载失败。否则,重新挂载成功。
是否有某种原因导致nosuid
和/或nodev
阻止重新安装成功?这种行为是否记录在某处?我很困惑,因为我希望绑定安装和重新安装要么都成功,要么都失败。
以下是重现绑定安装和重新安装的代码:
#define _GNU_SOURCE /* unshare */
#include <errno.h> /* errno */
#include <sched.h> /* unshare */
#include <stdio.h> /* printf */
#include <string.h> /* strerror */
#include <sys/mount.h> /* mount */
#include <unistd.h> /* getuid */
int main() {
printf ( "getuid %d\n", getuid() );
int rv = unshare ( CLONE_NEWNS | CLONE_NEWPID | CLONE_NEWUSER );
printf ( "unshare %2d %s\n", rv, strerror(errno) );
rv = mount ( "/tmp/foo", "/tmp/foo", 0, MS_BIND | MS_REC, 0 ),
printf ( "mount %2d %s\n", rv, strerror(errno) );
rv = mount ( "/tmp/foo", "/tmp/foo", 0,
MS_BIND | MS_REMOUNT | MS_RDONLY, 0 ),
printf ( "remount %2d %s\n", rv, strerror(errno) );
return 0;
}
示例输出:
$ mkdir -p /tmp/foo
$ mount | grep /tmp
tmpfs on /tmp type tmpfs (rw,nosuid,nodev,relatime,inode64)
$ gcc test.c && ./a.out
getuid 1000
unshare 0 No error information
mount 0 No error information
remount -1 Operation not permitted
$ uname -a
Linux hostname 5.12.12_1 #1 SMP 1624132767 x86_64 GNU/Linux
然而,如果/tmp
挂载时既没有nosuid
也没有nodev
,那么绑定挂载和重新挂载都会成功,如下所示:
$ mkdir -p /tmp/foo
$ mount | grep /tmp
tmpfs on /tmp type tmpfs (rw,relatime,inode64)
$ gcc test.c && ./a.out
getuid 1000
unshare 0 No error information
mount 0 No error information
remount 0 No error information
答案1
我已经找到答案了。
从下面的内核源代码摘录中可以看出,如果已经设置了任何标志 NODEV、NOSUID、NOEXEC 和/或 ATIME,我将需要在第二次调用中保留(即继续设置)它们mount()
。
来自fs/namespace.c
Linux内核源代码:
/*
* Handle reconfiguration of the mountpoint only without alteration of the
* superblock it refers to. This is triggered by specifying MS_REMOUNT|MS_BIND
* to mount(2).
*/
static int do_reconfigure_mnt(struct path *path, unsigned int mnt_flags)
{
struct super_block *sb = path->mnt->mnt_sb;
struct mount *mnt = real_mount(path->mnt);
int ret;
if (!check_mnt(mnt))
return -EINVAL;
if (path->dentry != mnt->mnt.mnt_root)
return -EINVAL;
if (!can_change_locked_flags(mnt, mnt_flags))
return -EPERM;
down_write(&sb->s_umount);
ret = change_mount_ro_state(mnt, mnt_flags);
if (ret == 0)
set_mount_attributes(mnt, mnt_flags);
up_write(&sb->s_umount);
mnt_warn_timestamp_expiry(path, &mnt->mnt);
return ret;
}
static bool can_change_locked_flags(struct mount *mnt, unsigned int mnt_flags)
{
unsigned int fl = mnt->mnt.mnt_flags;
if ((fl & MNT_LOCK_READONLY) &&
!(mnt_flags & MNT_READONLY))
return false;
if ((fl & MNT_LOCK_NODEV) &&
!(mnt_flags & MNT_NODEV))
return false;
if ((fl & MNT_LOCK_NOSUID) &&
!(mnt_flags & MNT_NOSUID))
return false;
if ((fl & MNT_LOCK_NOEXEC) &&
!(mnt_flags & MNT_NOEXEC))
return false;
if ((fl & MNT_LOCK_ATIME) &&
((fl & MNT_ATIME_MASK) != (mnt_flags & MNT_ATIME_MASK)))
return false;
return true;
}