Qual código impede os loops do namespace de montagem? Em um caso mais complexo envolvendo propagação de montagem

2

Informações básicas

Os seguintes comandos retornam um erro:

# touch /tmp/a
# mount --bind /proc/self/ns/mnt /tmp/a
mount: /tmp/a: wrong fs type, bad option, bad superblock on /proc/self/ns/mnt, missing codepage or helper program, or other error.

Isso ocorre porque o código do kernel (veja extratos abaixo) previne um loop simples de namespace de montagem. Os comentários do código explicam porque isso não é permitido. O tempo de vida de um namespace de montagem é rastreado por uma contagem de referência simples. Se você tem um loop onde os espaços de nomes de montagem A e B fazem referência ao outro, então A e B sempre terão pelo menos uma referência, e eles nunca serão liberados. A memória alocada seria perdida até você reiniciar o sistema inteiro.

Para comparação, o kernel permite o seguinte, que não é um loop:

# unshare -m
# echo $$
8456
# kill -STOP $$
[1]+  Stopped                 unshare -m

# touch /tmp/a
# mount --bind /proc/8456/ns/mnt /tmp/a
#
# umount /tmp/a  # cleanup
#

Pergunta

Onde o código do kernel faz distinção entre os dois casos a seguir?

Se eu tentar criar um loop usando a propagação de montagem, ele falhará:

# mount --make-shared /tmp
# unshare -m --propagation shared
# echo $$
8456
# kill -STOP $$
[1]+  Stopped                 unshare -m

# mount --bind /proc/8456/ns/mnt /tmp/a
mount: /tmp/a: wrong fs type, bad option, bad superblock on /proc/9061/ns/mnt, missing codepage or helper program, or other error.

Mas se eu remover a propagação de montagem, nenhum loop será criado e será bem-sucedido:

# unshare -m --propagation private
# echo $$
8456
# kill -STOP $$
[1]+  Stopped                 unshare -m

# mount --bind /proc/8456/ns/mnt /tmp/a
# 
# umount /tmp/a  # cleanup    

Código do kernel que manipula o caso mais simples

link

static bool mnt_ns_loop(struct dentry *dentry)
{
    /* Could bind mounting the mount namespace inode cause a
     * mount namespace loop?
     */
    struct mnt_namespace *mnt_ns;
    if (!is_mnt_ns_file(dentry))
        return false;

    mnt_ns = to_mnt_ns(get_proc_ns(dentry->d_inode));
    return current->nsproxy->mnt_ns->seq >= mnt_ns->seq;
}

...

    err = -EINVAL;
    if (mnt_ns_loop(old_path.dentry))
        goto out;

...

 * Assign a sequence number so we can detect when we attempt to bind
 * mount a reference to an older mount namespace into the current
 * mount namespace, preventing reference counting loops.  A 64bit
 * number incrementing at 10Ghz will take 12,427 years to wrap which
 * is effectively never, so we can ignore the possibility.
 */
static atomic64_t mnt_ns_seq = ATOMIC64_INIT(1);

static struct mnt_namespace *alloc_mnt_ns(struct user_namespace *user_ns)
    
por sourcejedi 07.10.2018 / 02:43

1 resposta

0

É porque propagate_one() chama copy_tree() sem CL_COPY_MNT_NS_FILE . Nesse caso, se a raiz da árvore for uma montagem de um arquivo NS, copy_tree() falhará com o erro EINVAL . O termo "um arquivo NS" significa um dos arquivos /proc/*/ns/mnt .

Lendo mais, noto que se a raiz da árvore não é um arquivo NS, mas uma das montagens filho é, ela é excluída da propagação (da mesma forma que uma montagem não localizável é).

Veja commit 4ce5d2b1a8fd, vfs: não copie montagens de bind de /proc/<pid>/ns/mnt entre namespaces

Exemplo de um arquivo NS sendo silenciosamente ignorado durante a propagação

# mount --make-shared /tmp
# cd /tmp
# mkdir private_mnt
# mount --bind private_mnt private_mnt
# mount --make-private private_mnt
# touch private_mnt/child_ns
# unshare --mount=private_mnt/child_ns --propagation=shared ls -l /proc/self/ns/mnt
lrwxrwxrwx. 1 root root 0 Oct  7 18:25 /proc/self/ns/mnt -> 'mnt:[4026532807]'
# findmnt | grep /tmp
├─/tmp                                tmpfs                             tmpfs           ...
│ ├─/tmp/private_mnt                  tmpfs[/private_mnt]               tmpfs           ...
│ │ └─/tmp/private_mnt/child_ns       nsfs[mnt:[4026532807]]            nsfs            ...

Vamos criar uma montagem normal para comparação

# mkdir private_mnt/child_mnt
# mount --bind private_mnt/child_mnt private_mnt/child_mnt

Agora tente propagar tudo. (Crie uma montagem de ligação recursiva de private_mnt dentro de /tmp . /tmp é uma montagem compartilhada).

# mkdir shared_mnt
# mount --rbind private_mnt shared_mnt
# findmnt | grep /tmp/shared_mnt
│ └─/tmp/shared_mnt                   tmpfs[/private_mnt]               tmpfs           ...
│   ├─/tmp/shared_mnt/child_ns        nsfs[mnt:[4026532809]]            nsfs            ...
│   └─/tmp/shared_mnt/child_mnt       tmpfs[/private_mnt/child_mnt]     tmpfs           ...
# nsenter --mount=/tmp/private_mnt/child_ns findmnt|grep /tmp/shared_mnt
│ └─/tmp/shared_mnt                   tmpfs[/private_mnt]               tmpfs           ...
│   └─/tmp/shared_mnt/child_mnt       tmpfs[/private_mnt/child_mnt]     tmpfs           ...

Código do kernel

Aqui estão os extratos da versão atual do código, que foi adicionado no commit vinculado acima.

link

static int propagate_one(struct mount *m)
{
...
    /* Notice when we are propagating across user namespaces */
    if (m->mnt_ns->user_ns != user_ns)
        type |= CL_UNPRIVILEGED;
    child = copy_tree(last_source, last_source->mnt.mnt_root, type);
    if (IS_ERR(child))
        return PTR_ERR(child);

link

struct mount *copy_tree(struct mount *mnt, struct dentry *dentry,
                    int flag)
{
    struct mount *res, *p, *q, *r, *parent;

    if (!(flag & CL_COPY_UNBINDABLE) && IS_MNT_UNBINDABLE(mnt))
        return ERR_PTR(-EINVAL);

    if (!(flag & CL_COPY_MNT_NS_FILE) && is_mnt_ns_file(dentry))
        return ERR_PTR(-EINVAL);
    
por 07.10.2018 / 19:13