2

In some of my build scripts I've been using mount namespaces as a mechanism to safely mount without ever leaving these mounts behind when the script terminates. Unshared mount points are implicitly unmounted when the last process in that namespace exits.

My scripts usually include a stansa such as this:

#!/bin/bash
self_ns=$(ls -lh /proc/self/ns/mnt)
init_ns=$(ls -lh /proc/$PPID/ns/mnt)
if [ "${self_ns#*mnt:}" = "${init_ns#*mnt:}" ] ; then
    unshare --mount $0 "$@"
    exit $?
fi

While this has worked fine for me for some time I've recently run into a problem on a jenkins build server.

I believe the issue there is that the build script itself is being executed inside a jenkins chroot environment. So when the script executes unshare --mount ..., it fails with the error:

unshare: cannot change root filesystem propagation: Invalid argument

Unfortunately I really don't understand this restriction or how to get round it. When I try a chroot on the command line I can't replicate this error. I don't know what the jenkins plugin has done to cause this.

The most important thing is that these mount points are removed on exit every time without fail.

  • I note it succeeds in entering a new mount namespace. But then it tries to do the equivalent of mount --make-rprivate /. Which is something you want. Or at least you want mount --make-rslave /. The exact thing unshare does here is controlled by the --propagation option. – sourcejedi Nov 26 '19 at 15:58
  • I looked at the list of EINVAL errors in man 2 mount, but I don't think any of those explain your problem. – sourcejedi Nov 26 '19 at 16:15
  • @sourcejedi its odd under jenkings chroot, the unshare itself fails. But I haven't been able to construct a chroot environment that does the same from the command line. – Philip Couling Nov 26 '19 at 16:16
  • 1
    I get the same error if I chroot to an lxc's rootfs directory installation and unshare --mount bash. If I first bind mount (--private) this directory elsewhere and then do the chroot there, unshare --mount then works. I don't know what this means, but I hope this can help find the cause or a workaround (adding a bind mount in the pipeline). – A.B Nov 26 '19 at 21:01
  • @A.B wow. For some reason I can mount --bind --make-private / /mnt Then chroot /mnt unshare --mount bash -c 'echo hello' but not unshare --mount bash -c 'echo hello'. Okay that's a viable work around, whatever the cause was. – Philip Couling Nov 28 '19 at 11:39
1

Based on A.B's comment I have found a workaround:

A.B wrote:

I get the same error if I chroot to an lxc's rootfs directory installation and unshare --mount bash. If I first bind mount (--private) this directory elsewhere and then do the chroot there, unshare --mount then works. I don't know what this means, but I hope this can help find the cause or a workaround (adding a bind mount in the pipeline).

Based on this I found that this does not work:

unshare --mount bash -c 'echo hello'

But this works:

mount --bind --make-private / /mnt 
chroot /mnt unshare --mount bash -c 'echo hello'
umount /mnt
  • hmm. i don't think you want to use mount --bind? I think the command you run will not have access to /proc, for example ps will not work. Does your workaround still work if you use mount --rbind instead? – sourcejedi Nov 28 '19 at 17:39
  • Interesting question although patching proc sys dev and tmp is simple enough. I can try with rbind but the principle is sound either way. – Philip Couling Nov 28 '19 at 19:51
1

The cause of the problem is that unshare tries to set the mount propagation flags of the root directory, which can only be done for mount points. The root directory of the Jenkins chroot environment is not a mount point.

For example:

$ unshare -rm mount --make-rprivate /opt
mount: /opt: not mount point or bad option.

A complete reproduction:

#!/bin/bash
try() {
  mount -t tmpfs t /mnt
  mkdir /mnt/t
  for i in /bin /lib* /sbin /usr /home /proc
  do
    mkdir "/mnt/t$i"
    mount --rbind "$i" "/mnt/t$i"
  done
  chroot /mnt/t unshare -m echo OK
}
export -f try
unshare -rm bash -c try

A simple workaround, which makes no mounts outside the mount namespace. It escapes the chroot to set the mount propagation, and assumes the mount command is availble outside the chroot:

unshare --propagation unchanged -m sh -c \
'nsenter --mount=/proc/self/ns/mnt mount --make-rslave /; echo Do some mounts'

Or convert the chroot into a pivot_root environment:

#define _GNU_SOURCE
#include <errno.h>
#include <error.h>
#include <fcntl.h>
#include <sched.h>
#include <stdio.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <unistd.h>

static int pivot_root(const char *new_root, const char *put_old){
    return syscall(SYS_pivot_root, new_root, put_old);
}

int main(int argc, char **argv) {

    if (unshare(CLONE_NEWNS))
        error(1, errno, "unshare");

    int root = open("/", O_DIRECTORY | O_PATH | O_CLOEXEC);
    if (root < 0) error(1, errno, "open /");

    int ns = open("/proc/self/ns/mnt", O_RDONLY | O_CLOEXEC);
    if (ns < 0) error(1, errno, "open mount namespace");

    if (setns(ns, CLONE_NEWNS))
        error(1, errno, "setns");

    if (fchdir(root))
        error(1, errno, "fchdir");

    if (mount("/", "/", 0, MS_REC|MS_SLAVE, 0))
        error(1, errno, "mount --make-rslave");

    if (mount(".", "proc", 0, MS_REC|MS_BIND, 0))
        error(1, errno, "mount --rbind");

    if (chdir("proc"))
        error(1, errno, "chdir");

    if (pivot_root(".", "proc"))
        error(1, errno, "pivot_root");

    if (umount2("proc", MNT_DETACH))
        error(1, errno, "umount");

    execvp(argv[1], argv + 1);
    error(1, errno, "exec");
}
  • Thanks +1. That's really useful and may suggest my workaround was the right corse of action. – Philip Couling Apr 30 '20 at 7:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.