在非 root 用户命名空间中,在 nosuid、nodev 文件系统上,为什么绑定挂载成功但重新挂载失败?

在非 root 用户命名空间中,在 nosuid、nodev 文件系统上,为什么绑定挂载成功但重新挂载失败?

在 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.cLinux内核源代码:

/*
 * 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;
}

相关内容