Package: bubblewrap
Version: 0~git160513-2
Severity: normal
Tags: patch
Forwarded: https://github.com/projectatomic/bubblewrap/pull/71

"bwrap --ro-bind / / --unshare-user --uid 2 --gid 3 /bin/sh" runs the
wrapped shell in its own user namespace with uid 2 and gid 3, as expected.

Similarly, "bwrap --ro-bind / / --dev /dev /bin/sh" runs the wrapped
shell with a minimal /dev, as expected.

However, combining "--dev /dev" with "--unshare-user" fails on Debian
kernels with their default configuration (kernel.unprivileged_userns_clone=0);
in particular, this breaks flatpak-builder. Alex Larsson has proposed
patches upstream for this issue, which I have applied to the embedded
copy of bubblewrap in flatpak 0.6.0-2.

Patches are attached, and also available in git here:

    ssh://git.debian.org/git/users/smcv/bubblewrap.git

among several others.

Ideally, I would like to copy that git repository into collab-maint,
and commit this sort of thing directly as a co-maintainer.

Regards,
    S
>From d8cbc3d8e3f43e56f43285f6fe4cb2c923f794cc Mon Sep 17 00:00:00 2001
From: Simon McVittie <s...@debian.org>
Date: Sat, 21 May 2016 17:31:10 +0100
Subject: [PATCH 6/7] Add patches from upstream bug 71 to make --dev coexist
 with --unshare-user

---
 debian/changelog                                   |   2 +
 debian/patches/Add-unshare-user-try.patch          |  57 ++++++
 ...tuid-no-unprivileged-user-namespaces-work.patch | 197 +++++++++++++++++++++
 debian/patches/series                              |   2 +
 4 files changed, 258 insertions(+)
 create mode 100644 debian/patches/Add-unshare-user-try.patch
 create mode 100644 debian/patches/Make-setuid-no-unprivileged-user-namespaces-work.patch
 create mode 100644 debian/patches/series

diff --git a/debian/changelog b/debian/changelog
index 00aad10..77fece6 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -7,6 +7,8 @@ bubblewrap (0~git160513-3) UNRELEASED; urgency=medium
   * Increase Priority to optional, because this tool is likely to be
     depended on by gnome-software (via Flatpak) in future
   * debian/gbp.conf: add DEP-14-style git-buildpackage configuration
+  * Add patches from upstream bug 71 to make --dev coexist with
+    --unshare-user
 
  -- Simon McVittie <s...@debian.org>  Sat, 21 May 2016 15:10:56 +0100
 
diff --git a/debian/patches/Add-unshare-user-try.patch b/debian/patches/Add-unshare-user-try.patch
new file mode 100644
index 0000000..1053ad7
--- /dev/null
+++ b/debian/patches/Add-unshare-user-try.patch
@@ -0,0 +1,57 @@
+From: Alexander Larsson <al...@redhat.com>
+Date: Fri, 20 May 2016 15:13:57 +0200
+Subject: Add --unshare-user-try
+
+This optionally enables user namespaces, but ignores it if its
+not supported by the kernel.
+
+Note: For this to make any sense, bwrap has to be setuid,
+because unprivileged use requires user namespaces.
+
+Bug: https://github.com/projectatomic/bubblewrap/pull/71
+---
+ bubblewrap.c | 10 ++++++++++
+ 1 file changed, 10 insertions(+)
+
+diff --git a/bubblewrap.c b/bubblewrap.c
+index 59407a7..6c4664e 100644
+--- a/bubblewrap.c
++++ b/bubblewrap.c
+@@ -148,6 +148,7 @@ usage (int ecode, FILE *out)
+            "    --version                    Print version\n"
+            "    --args FD                    Parse nul-separated args from FD\n"
+            "    --unshare-user               Create new user namespace (may be automatically implied if not setuid)\n"
++           "    --unshare-user-try           Create new user namespace if possible else continue by skipping it\n"
+            "    --unshare-ipc                Create new ipc namespace\n"
+            "    --unshare-pid                Create new pid namespace\n"
+            "    --unshare-net                Create new network namespace\n"
+@@ -840,6 +841,7 @@ read_priv_sec_op (int          read_socket,
+ 
+ char *opt_chdir_path = NULL;
+ bool opt_unshare_user = FALSE;
++bool opt_unshare_user_try = FALSE;
+ bool opt_unshare_pid = FALSE;
+ bool opt_unshare_ipc = FALSE;
+ bool opt_unshare_net = FALSE;
+@@ -955,6 +957,10 @@ parse_args_recurse (int    *argcp,
+         {
+           opt_unshare_user = TRUE;
+         }
++      else if (strcmp (arg, "--unshare-user-try") == 0)
++        {
++          opt_unshare_user_try = TRUE;
++        }
+       else if (strcmp (arg, "--unshare-ipc") == 0)
+         {
+           opt_unshare_ipc = TRUE;
+@@ -1327,6 +1333,10 @@ main (int    argc,
+   if (!is_privileged)
+     opt_unshare_user = TRUE;
+ 
++  if (opt_unshare_user_try &&
++      stat ("/proc/self/ns/user", &sbuf) == 0)
++    opt_unshare_user = TRUE;
++
+   if (argc == 0)
+     usage (EXIT_FAILURE, stderr);
+ 
diff --git a/debian/patches/Make-setuid-no-unprivileged-user-namespaces-work.patch b/debian/patches/Make-setuid-no-unprivileged-user-namespaces-work.patch
new file mode 100644
index 0000000..ec18f7e
--- /dev/null
+++ b/debian/patches/Make-setuid-no-unprivileged-user-namespaces-work.patch
@@ -0,0 +1,197 @@
+From: Alexander Larsson <al...@redhat.com>
+Date: Fri, 20 May 2016 14:59:43 +0200
+Subject: Make setuid + no-unprivileged user namespaces work
+
+On e.g. debian by default unprivileged namespaces are not allowed.
+Typically the setuid mode is then used. However, if /dev is mounted
+(and thus devpts) then we need to do some workaround in how we
+create the uid/gid maps so uid 0 is mapped while we mount devpts.
+
+Unfortunately the way we were working around that is by using an
+unprivileged unshare(NEWUSER) in the sandbox, which doesn't work.
+See https://github.com/flatpak/flatpak/issues/2 for details.
+
+We work around this by mapping uid/gid 0 + the user. However, since
+this is a privileged operation we need to do that in the parent
+namespace, and we need setuid/setgid rights.
+
+Bug: https://github.com/projectatomic/bubblewrap/pull/71
+---
+ Makefile.am  |  2 +-
+ bubblewrap.c | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++++--------
+ 2 files changed, 70 insertions(+), 11 deletions(-)
+
+diff --git a/Makefile.am b/Makefile.am
+index e4d250b..6e47b5f 100644
+--- a/Makefile.am
++++ b/Makefile.am
+@@ -14,7 +14,7 @@ if PRIV_MODE_SETUID
+ 	$(SUDO_BIN) chmod u+s $(DESTDIR)$(bindir)/bwrap
+ else
+ if PRIV_MODE_FILECAPS
+-	$(SUDO_BIN) setcap cap_sys_admin,cap_net_admin,cap_sys_chroot+ep $(DESTDIR)$(bindir)/bwrap
++	$(SUDO_BIN) setcap cap_sys_admin,cap_net_admin,cap_sys_chroot,cap_setuid,cap_setgid+ep $(DESTDIR)$(bindir)/bwrap
+ endif
+ endif
+ 
+diff --git a/bubblewrap.c b/bubblewrap.c
+index 1f9331a..59407a7 100644
+--- a/bubblewrap.c
++++ b/bubblewrap.c
+@@ -358,7 +358,7 @@ do_init (int event_fd, pid_t initial_pid)
+ }
+ 
+ /* low 32bit caps needed */
+-#define REQUIRED_CAPS_0 (CAP_TO_MASK (CAP_SYS_ADMIN) | CAP_TO_MASK (CAP_SYS_CHROOT) | CAP_TO_MASK (CAP_NET_ADMIN))
++#define REQUIRED_CAPS_0 (CAP_TO_MASK (CAP_SYS_ADMIN) | CAP_TO_MASK (CAP_SYS_CHROOT) | CAP_TO_MASK (CAP_SETUID) | CAP_TO_MASK (CAP_SETGID))
+ /* high 32bit caps needed */
+ #define REQUIRED_CAPS_1 0
+ 
+@@ -441,21 +441,44 @@ write_uid_gid_map (uid_t sandbox_uid,
+                    uid_t parent_uid,
+                    uid_t sandbox_gid,
+                    uid_t parent_gid,
+-                   bool  deny_groups)
++                   pid_t pid,
++                   bool  deny_groups,
++                   bool  map_root)
+ {
+   cleanup_free char *uid_map = NULL;
+   cleanup_free char *gid_map = NULL;
++  cleanup_free char *dir = NULL;
++  cleanup_fd int dir_fd = -1;
+ 
+-  uid_map = xasprintf ("%d %d 1\n", sandbox_uid, parent_uid);
+-  if (write_file_at (proc_fd, "self/uid_map", uid_map) != 0)
++  if (pid == -1)
++    dir = xstrdup ("self");
++  else
++    dir = xasprintf ("%d", pid);
++
++  dir_fd = openat (proc_fd, dir, O_RDONLY | O_PATH);
++  if (dir_fd < 0)
++    die_with_error ("open /proc/%s failed", dir);
++
++  if (map_root && parent_uid != 0 && sandbox_uid != 0)
++    uid_map = xasprintf ("0 0 1\n"
++                         "%d %d 1\n", sandbox_uid, parent_uid);
++  else
++    uid_map = xasprintf ("%d %d 1\n", sandbox_uid, parent_uid);
++
++  if (write_file_at (dir_fd, "uid_map", uid_map) != 0)
+     die_with_error ("setting up uid map");
+ 
+   if (deny_groups &&
+-      write_file_at (proc_fd, "self/setgroups", "deny\n") != 0)
++      write_file_at (dir_fd, "setgroups", "deny\n") != 0)
+     die_with_error ("error writing to setgroups");
+ 
+-  gid_map = xasprintf ("%d %d 1\n", sandbox_gid, parent_gid);
+-  if (write_file_at (proc_fd, "self/gid_map", gid_map) != 0)
++  if (map_root && parent_gid != 0 && sandbox_gid != 0)
++    gid_map = xasprintf ("0 0 1\n"
++                         "%d %d 1\n", sandbox_gid, parent_gid);
++  else
++    gid_map = xasprintf ("%d %d 1\n", sandbox_gid, parent_gid);
++
++  if (write_file_at (dir_fd, "gid_map", gid_map) != 0)
+     die_with_error ("setting up gid map");
+ }
+ 
+@@ -1269,10 +1292,13 @@ main (int    argc,
+   char *old_cwd = NULL;
+   pid_t pid;
+   int event_fd = -1;
++  int child_wait_fd = -1;
+   const char *new_cwd;
+   uid_t ns_uid;
+   gid_t ns_gid;
+   struct stat sbuf;
++  uint64_t val;
++  int res UNUSED;
+ 
+   /* Get the (optional) capabilities we need, drop root */
+   acquire_caps ();
+@@ -1374,6 +1400,10 @@ main (int    argc,
+     if (!stat ("/proc/self/ns/cgroup", &sbuf))
+       clone_flags |= CLONE_NEWCGROUP;
+ 
++  child_wait_fd = eventfd (0, EFD_CLOEXEC);
++  if (child_wait_fd == -1)
++    die_with_error ("eventfd()");
++
+   pid = raw_clone (clone_flags, NULL);
+   if (pid == -1)
+     {
+@@ -1388,23 +1418,52 @@ main (int    argc,
+       die_with_error ("Creating new namespace failed");
+     }
+ 
++  ns_uid = opt_sandbox_uid;
++  ns_gid = opt_sandbox_gid;
++
+   if (pid != 0)
+     {
++      if (is_privileged && opt_unshare_user)
++        {
++          /* Map the uid/gid 0 if opt_needs_devpts, as otherwise
++           * mounting it will fail.
++           * Due to this non-direct mapping we need to have set[ug]id
++           * caps in the parent namespaces, and thus we need to write
++           * the map in the parent namespace, not the child. */
++          write_uid_gid_map (ns_uid, uid,
++                             ns_gid, gid,
++                             pid, TRUE, opt_needs_devpts);
++        }
++
+       /* Initial launched process, wait for exec:ed command to exit */
+ 
+       /* We don't need any caps in the launcher, drop them immediately. */
+       drop_caps ();
++
++      /* Let child run */
++      val = 1;
++      res = write (child_wait_fd, &val, 8);
++      /* Ignore res, if e.g. the child died and closed child_wait_fd we don't want to error out here */
++      close (child_wait_fd);
++
+       monitor_child (event_fd);
+       exit (0); /* Should not be reached, but better safe... */
+     }
+ 
++  /* Wait for the parent to init uid/gid maps and drop caps */
++  res = read (child_wait_fd, &val, 8);
++  close (child_wait_fd);
++
+   if (opt_unshare_net && loopback_setup () != 0)
+     die ("Can't create loopback device");
+ 
+   ns_uid = opt_sandbox_uid;
+   ns_gid = opt_sandbox_gid;
+-  if (opt_unshare_user)
++  if (!is_privileged && opt_unshare_user)
+     {
++      /* In the unprivileged case we have to write the uid/gid maps in
++       * the child, because we have no caps in the parent */
++
+       if (opt_needs_devpts)
+         {
+           /* This is a bit hacky, but we need to first map the real uid/gid to
+@@ -1417,7 +1476,7 @@ main (int    argc,
+ 
+       write_uid_gid_map (ns_uid, uid,
+                          ns_gid, gid,
+-                         TRUE);
++                         -1, TRUE, FALSE);
+     }
+ 
+   old_umask = umask (0);
+@@ -1523,7 +1582,7 @@ main (int    argc,
+ 
+       write_uid_gid_map (opt_sandbox_uid, ns_uid,
+                          opt_sandbox_gid, ns_gid,
+-                         FALSE);
++                         -1, FALSE, FALSE);
+     }
+ 
+   /* Now make /newroot the real root */
diff --git a/debian/patches/series b/debian/patches/series
new file mode 100644
index 0000000..850bb4f
--- /dev/null
+++ b/debian/patches/series
@@ -0,0 +1,2 @@
+Make-setuid-no-unprivileged-user-namespaces-work.patch
+Add-unshare-user-try.patch
-- 
2.8.1

>From cb3042bd4ca549e9b6452bc9afdd65929f287f81 Mon Sep 17 00:00:00 2001
From: Simon McVittie <s...@debian.org>
Date: Sat, 21 May 2016 21:44:19 +0100
Subject: [PATCH 7/7] Add some simple autopkgtests

In particular, "userns" verifies that upstream bug 71 is fixed.
---
 debian/changelog        |  1 +
 debian/tests/basic      | 13 +++++++++++++
 debian/tests/control    |  5 +++++
 debian/tests/dev        | 36 ++++++++++++++++++++++++++++++++++++
 debian/tests/testlib.sh | 40 ++++++++++++++++++++++++++++++++++++++++
 debian/tests/userns     | 35 +++++++++++++++++++++++++++++++++++
 6 files changed, 130 insertions(+)
 create mode 100755 debian/tests/basic
 create mode 100644 debian/tests/control
 create mode 100755 debian/tests/dev
 create mode 100644 debian/tests/testlib.sh
 create mode 100755 debian/tests/userns

diff --git a/debian/changelog b/debian/changelog
index 77fece6..7435098 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -9,6 +9,7 @@ bubblewrap (0~git160513-3) UNRELEASED; urgency=medium
   * debian/gbp.conf: add DEP-14-style git-buildpackage configuration
   * Add patches from upstream bug 71 to make --dev coexist with
     --unshare-user
+  * Add some simple autopkgtests, including one for bug 71
 
  -- Simon McVittie <s...@debian.org>  Sat, 21 May 2016 15:10:56 +0100
 
diff --git a/debian/tests/basic b/debian/tests/basic
new file mode 100755
index 0000000..41d6597
--- /dev/null
+++ b/debian/tests/basic
@@ -0,0 +1,13 @@
+#!/usr/bin/bats
+# vim:set sw=4 sts=4 et ft=sh:
+
+set -e
+
+. debian/tests/testlib.sh
+
+@test "Basic usage" {
+    ret=0
+    run bwrap --ro-bind / / /usr/bin/id
+    is "$status" 0
+    is "$output" "$(id)"
+}
diff --git a/debian/tests/control b/debian/tests/control
new file mode 100644
index 0000000..9774a97
--- /dev/null
+++ b/debian/tests/control
@@ -0,0 +1,5 @@
+Tests:
+ userns
+Depends:
+ bats,
+ bubblewrap,
diff --git a/debian/tests/dev b/debian/tests/dev
new file mode 100755
index 0000000..f14b7e8
--- /dev/null
+++ b/debian/tests/dev
@@ -0,0 +1,36 @@
+#!/usr/bin/bats
+# vim:set sw=4 sts=4 et ft=sh:
+
+set -e
+
+. debian/tests/testlib.sh
+
+@test "Mount new minimal /dev" {
+    run bwrap --ro-bind / / --dev /dev /bin/sh -c 'echo /dev/*'
+
+    like " $output " " /dev/full "
+    like " $output " " /dev/null "
+    like " $output " " /dev/pts "
+    like " $output " " /dev/random "
+    like " $output " " /dev/shm "
+    like " $output " " /dev/stderr "
+    like " $output " " /dev/stdin "
+    like " $output " " /dev/stdout "
+    like " $output " " /dev/tty "
+    like " $output " " /dev/urandom "
+    like " $output " " /dev/zero "
+
+    # an arbitrary selection of devices not expected to be passed through
+    unlike " $output " " /dev/hda "
+    unlike " $output " " /dev/dsp "
+    unlike " $output " " /dev/fuse "
+    unlike " $output " " /dev/kmsg "
+    unlike " $output " " /dev/loop0 "
+    unlike " $output " " /dev/mem "
+    unlike " $output " " /dev/sda "
+    unlike " $output " " /dev/snd "
+    unlike " $output " " /dev/tty1 "
+    unlike " $output " " /dev/vda "
+
+    is "$status" 0
+}
diff --git a/debian/tests/testlib.sh b/debian/tests/testlib.sh
new file mode 100644
index 0000000..31946b0
--- /dev/null
+++ b/debian/tests/testlib.sh
@@ -0,0 +1,40 @@
+# vim:set sw=4 sts=4 et ft=sh:
+
+# is GOT EXPECTED
+# Assert that GOT == EXPECTED.
+# (Inspired by Perl's Test::More)
+is () {
+    if [ "x$1" = "x$2" ]; then
+        return 0
+    else
+        printf "# got:      %q\n" "$1"
+        printf "# expected: %q\n" "$2"
+        return 1
+    fi
+}
+
+# like GOT EREGEX
+# Assert that GOT matches EREGEX.
+# (Inspired by Perl's Test::More)
+like () {
+    if [[ $1 =~ $2 ]]; then
+        return 0
+    else
+        printf "# got:              %q\n" "$1"
+        printf "# should match ERE: %q\n" "$2"
+        return 1
+    fi
+}
+
+# unlike GOT EREGEX
+# Assert that GOT matches EREGEX.
+# (Inspired by Perl's Test::More)
+unlike () {
+    if [[ $1 =~ $2 ]]; then
+        printf "# got:                  %q\n" "$1"
+        printf "# should not match ERE: %q\n" "$2"
+        return 1
+    else
+        return 0
+    fi
+}
diff --git a/debian/tests/userns b/debian/tests/userns
new file mode 100755
index 0000000..c8862c6
--- /dev/null
+++ b/debian/tests/userns
@@ -0,0 +1,35 @@
+#!/usr/bin/bats
+# vim:set sw=4 sts=4 et ft=sh:
+
+set -e
+
+. debian/tests/testlib.sh
+
+@test "Unshare user ID" {
+    run bwrap --ro-bind / / --unshare-user --uid 2 --gid 3 /usr/bin/id -u
+    is "$status" 0
+    is "$output" 2
+    run bwrap --ro-bind / / --unshare-user --uid 2 --gid 3 /usr/bin/id -g
+    is "$status" 0
+    is "$output" 3
+    run bwrap --ro-bind / / --unshare-user --uid 2 --gid 3 /bin/sh -c 'ls -l /etc/passwd'
+    is "$status" 0
+    like "$output" " nobody nogroup "
+}
+
+@test "Combine new /dev with new user namespace (#71)" {
+    run bwrap --ro-bind / / --unshare-user --uid 2 --gid 3 --dev /dev /bin/sh -c 'echo /dev/*'
+
+    like " $output " " /dev/full "
+    unlike " $output " " /dev/tty1 "
+
+    is "$status" 0
+
+    run bwrap --ro-bind / / --unshare-user --uid 2 --gid 3 --dev /dev /usr/bin/id -u
+    is "$status" 0
+    is "$output" 2
+
+    run bwrap --ro-bind / / --unshare-user --uid 2 --gid 3 --dev /dev /usr/bin/id -g
+    is "$status" 0
+    is "$output" 3
+}
-- 
2.8.1

Reply via email to