root以外のユーザーの名前空間、nosuid、nodevファイルシステムでは、バインドマウントは成功しますが、再マウントが失敗するのはなぜですか?

root以外のユーザーの名前空間、nosuid、nodevファイルシステムでは、バインドマウントは成功しますが、再マウントが失敗するのはなぜですか?

Linuxユーザーの名前空間は、rootではなくユーザーとして/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フラグがすでに設定されている場合は、2番目の呼び出しでそのフラグを維持(つまり設定を継続)する必要があります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;
}

関連情報