Commit message: Don't create our own temporary mount point for pivot_root

An attacker could pre-create /tmp/.bubblewrap-$UID and make it a
non-directory, non-symlink (in which case mounting our tmpfs would fail,
causing denial of service), or make it a symlink under their control
(potentially allowing bad things if the protected_symlinks sysctl is
not enabled).

Instead, temporarily mount the tmpfs on a directory that we are sure
exists and is not attacker-controlled. /tmp (the directory itself, not
a subdirectory) will do.

Fixes: #304
Bug-Debian: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=923557
Signed-off-by: Simon McVittie <smcv@debian.org>

Closes: #305
Approved by: cgwalters
CVE: CVE-2019-12439
CWE: CWE-20
Vulnerability description: bubblewrap.c in Bubblewrap before 0.3.3 misuses temporary directories in /tmp as a mount point. In some particular configurations (related to XDG_RUNTIME_DIR), a local attacker may abuse this flaw to prevent other users from executing bubblewrap or potentially execute code.

Project: bubblewrap
File: bubblewrap.c

acquire_privs (void)
{
  uid_t euid, new_fsuid;

  euid = geteuid ();

  /* Are we setuid ? */
  if (real_uid != euid)
    {
      if (euid != 0)
        die ("Unexpected setuid user %d, should be 0", euid);

      is_privileged = TRUE;
      /* We want to keep running as euid=0 until at the clone()
       * operation because doing so will make the user namespace be
       * owned by root, which makes it not ptrace:able by the user as
       * it otherwise would be. After that we will run fully as the
       * user, which is necessary e.g. to be able to read from a fuse
       * mount from the user.
       *
       * However, we don't want to accidentally mis-use euid=0 for
       * escalated filesystem access before the clone(), so we set
       * fsuid to the uid.
       */
      if (setfsuid (real_uid) < 0)
        die_with_error ("Unable to set fsuid");

      /* setfsuid can't properly report errors, check that it worked (as per manpage) */
      new_fsuid = setfsuid (-1);
      if (new_fsuid != real_uid)
        die ("Unable to set fsuid (was %d)", (int)new_fsuid);

      /* We never need capabilities after execve(), so lets drop everything from the bounding set */
      drop_cap_bounding_set (TRUE);

      /* Keep only the required capabilities for setup */
      set_required_caps ();
    }
  else if (real_uid != 0 && has_caps ())
    {
      /* We have some capabilities in the non-setuid case, which should not happen.
         Probably caused by the binary being setcap instead of setuid which we
         don't support anymore */
      die ("Unexpected capabilities but not setuid, old file caps config?");
    }
  else if (real_uid == 0)
    {
      /* If our uid is 0, default to inheriting all caps; the caller
       * can drop them via --cap-drop.  This is used by at least rpm-ostree.
       * Note this needs to happen before the argument parsing of --cap-drop.
       */
      struct __user_cap_header_struct hdr = { _LINUX_CAPABILITY_VERSION_3, 0 };
      struct __user_cap_data_struct data[2] = { { 0 } };

      if (capget (&hdr, data) < 0)
        die_with_error ("capget (for uid == 0) failed");

      requested_caps[0] = data[0].effective;
      requested_caps[1] = data[1].effective;
    }

  /* Else, we try unprivileged user namespaces */
}

Project: bubblewrap
File: bubblewrap.c

block_sigchild (void)
{
  sigset_t mask;
  int status;

  sigemptyset (&mask);
  sigaddset (&mask, SIGCHLD);

  if (sigprocmask (SIG_BLOCK, &mask, NULL) == -1)
    die_with_error ("sigprocmask");

  /* Reap any outstanding zombies that we may have inherited */
  while (waitpid (-1, &status, WNOHANG) > 0)
    ;
}

Project: bubblewrap
File: bubblewrap.c

close_ops_fd (void)
{
  SetupOp *op;

  for (op = ops; op != NULL; op = op->next)
    {
      if (op->fd != -1)
        {
          (void) close (op->fd);
          op->fd = -1;
        }
    }
}

Project: bubblewrap
File: bubblewrap.c

do_init (int event_fd, pid_t initial_pid, struct sock_fprog *seccomp_prog)
{
  int initial_exit_status = 1;
  LockFile *lock;

  for (lock = lock_files; lock != NULL; lock = lock->next)
    {
      int fd = open (lock->path, O_RDONLY | O_CLOEXEC);
      if (fd == -1)
        die_with_error ("Unable to open lock file %s", lock->path);

      struct flock l = {
        .l_type = F_RDLCK,
        .l_whence = SEEK_SET,
        .l_start = 0,
        .l_len = 0
      };

      if (fcntl (fd, F_SETLK, &l) < 0)
        die_with_error ("Unable to lock file %s", lock->path);

      /* Keep fd open to hang on to lock */
      lock->fd = fd;
    }

  /* Optionally bind our lifecycle to that of the caller */
  handle_die_with_parent ();

  if (seccomp_prog != NULL &&
      prctl (PR_SET_SECCOMP, SECCOMP_MODE_FILTER, seccomp_prog) != 0)
    die_with_error ("prctl(PR_SET_SECCOMP)");

  while (TRUE)
    {
      pid_t child;
      int status;

      child = wait (&status);
      if (child == initial_pid && event_fd != -1)
        {
          uint64_t val;
          int res UNUSED;

          initial_exit_status = propagate_exit_status (status);

          val = initial_exit_status + 1;
          res = write (event_fd, &val, 8);
          /* Ignore res, if e.g. the parent died and closed event_fd
             we don't want to error out here */
        }

      if (child == -1 && errno != EINTR)
        {
          if (errno != ECHILD)
            die_with_error ("init wait()");
          break;
        }
    }

  /* Close FDs. */
  for (lock = lock_files; lock != NULL; lock = lock->next)
    {
      if (lock->fd >= 0)
        {
          close (lock->fd);
          lock->fd = -1;
        }
    }

  return initial_exit_status;
}

Project: bubblewrap
File: bubblewrap.c

drop_all_caps (bool keep_requested_caps)
{
  struct __user_cap_header_struct hdr = { _LINUX_CAPABILITY_VERSION_3, 0 };
  struct __user_cap_data_struct data[2] = { { 0 } };

  if (keep_requested_caps)
    {
      /* Avoid calling capset() unless we need to; currently
       * systemd-nspawn at least is known to install a seccomp
       * policy denying capset() for dubious reasons.
       * <https://github.com/projectatomic/bubblewrap/pull/122>
       */
      if (!opt_cap_add_or_drop_used && real_uid == 0)
        {
          assert (!is_privileged);
          return;
        }
      data[0].effective = requested_caps[0];
      data[0].permitted = requested_caps[0];
      data[0].inheritable = requested_caps[0];
      data[1].effective = requested_caps[1];
      data[1].permitted = requested_caps[1];
      data[1].inheritable = requested_caps[1];
    }

  if (capset (&hdr, data) < 0)
    {
      /* While the above logic ensures we don't call capset() for the primary
       * process unless configured to do so, we still try to drop privileges for
       * the init process unconditionally. Since due to the systemd seccomp
       * filter that will fail, let's just ignore it.
       */
      if (errno == EPERM && real_uid == 0 && !is_privileged)
        return;
      else
        die_with_error ("capset failed");
    }
}

Project: bubblewrap
File: bubblewrap.c

drop_cap_bounding_set (bool drop_all)
{
  if (!drop_all)
    prctl_caps (requested_caps, TRUE, FALSE);
  else
    {
      uint32_t no_caps[2] = {0, 0};
      prctl_caps (no_caps, TRUE, FALSE);
    }
}

Project: bubblewrap
File: bubblewrap.c

drop_privs (bool keep_requested_caps)
{
  assert (!keep_requested_caps || !is_privileged);
  /* Drop root uid */
  if (getuid () == 0 && setuid (opt_sandbox_uid) < 0)
    die_with_error ("unable to drop root uid");

  drop_all_caps (keep_requested_caps);
}

Project: bubblewrap
File: bubblewrap.c

dump_info (int fd, const char *output, bool exit_on_error)
{
  size_t len = strlen (output);
  if (write_to_fd (fd, output, len))
    {
      if (exit_on_error)
        die_with_error ("Write to info_fd");
    }
}

Project: bubblewrap
File: bubblewrap.c

handle_die_with_parent (void)
{
  if (opt_die_with_parent && prctl (PR_SET_PDEATHSIG, SIGKILL, 0, 0, 0) != 0)
    die_with_error ("prctl");
}

Project: bubblewrap
File: bubblewrap.c

monitor_child (int event_fd, pid_t child_pid, int setup_finished_fd)
{
  int res;
  uint64_t val;
  ssize_t s;
  int signal_fd;
  sigset_t mask;
  struct pollfd fds[2];
  int num_fds;
  struct signalfd_siginfo fdsi;
  int dont_close[] = {-1, -1, -1, -1};
  int j = 0;
  int exitc;
  pid_t died_pid;
  int died_status;

  /* Close all extra fds in the monitoring process.
     Any passed in fds have been passed on to the child anyway. */
  if (event_fd != -1)
    dont_close[j++] = event_fd;
  if (opt_json_status_fd != -1)
    dont_close[j++] = opt_json_status_fd;
  if (setup_finished_fd != -1)
    dont_close[j++] = setup_finished_fd;
  assert (j < sizeof(dont_close)/sizeof(*dont_close));
  fdwalk (proc_fd, close_extra_fds, dont_close);

  sigemptyset (&mask);
  sigaddset (&mask, SIGCHLD);

  signal_fd = signalfd (-1, &mask, SFD_CLOEXEC | SFD_NONBLOCK);
  if (signal_fd == -1)
    die_with_error ("Can't create signalfd");

  num_fds = 1;
  fds[0].fd = signal_fd;
  fds[0].events = POLLIN;
  if (event_fd != -1)
    {
      fds[1].fd = event_fd;
      fds[1].events = POLLIN;
      num_fds++;
    }

  while (1)
    {
      fds[0].revents = fds[1].revents = 0;
      res = poll (fds, num_fds, -1);
      if (res == -1 && errno != EINTR)
        die_with_error ("poll");

      /* Always read from the eventfd first, if pid 2 died then pid 1 often
       * dies too, and we could race, reporting that first and we'd lose
       * the real exit status. */
      if (event_fd != -1)
        {
          s = read (event_fd, &val, 8);
          if (s == -1 && errno != EINTR && errno != EAGAIN)
            die_with_error ("read eventfd");
          else if (s == 8)
            {
              exitc = (int) val - 1;
              report_child_exit_status (exitc, setup_finished_fd);
              return exitc;
            }
        }

      /* We need to read the signal_fd, or it will keep polling as read,
       * however we ignore the details as we get them from waitpid
       * below anyway */
      s = read (signal_fd, &fdsi, sizeof (struct signalfd_siginfo));
      if (s == -1 && errno != EINTR && errno != EAGAIN)
        die_with_error ("read signalfd");

      /* We may actually get several sigchld compressed into one
         SIGCHLD, so we have to handle all of them. */
      while ((died_pid = waitpid (-1, &died_status, WNOHANG)) > 0)
        {
          /* We may be getting sigchild from other children too. For instance if
             someone created a child process, and then exec:ed bubblewrap. Ignore them */
          if (died_pid == child_pid)
            {
              exitc = propagate_exit_status (died_status);
              report_child_exit_status (exitc, setup_finished_fd);
              return exitc;
            }
        }
    }

  die ("Should not be reached");

  return 0;
}

Project: bubblewrap
File: bubblewrap.c

parse_args (int          *argcp,
            const char ***argvp)
{
  int total_parsed_argc = *argcp;

  parse_args_recurse (argcp, argvp, FALSE, &total_parsed_argc);
}

Project: bubblewrap
File: bubblewrap.c

parse_args_recurse (int          *argcp,
                    const char ***argvp,
                    bool          in_file,
                    int          *total_parsed_argc_p)
{
  SetupOp *op;
  int argc = *argcp;
  const char **argv = *argvp;
  /* I can't imagine a case where someone wants more than this.
   * If you do...you should be able to pass multiple files
   * via a single tmpfs and linking them there, etc.
   *
   * We're adding this hardening due to precedent from
   * http://googleprojectzero.blogspot.com/2014/08/the-poisoned-nul-byte-2014-edition.html
   *
   * I picked 9000 because the Internet told me to and it was hard to
   * resist.
   */
  static const uint32_t MAX_ARGS = 9000;

  if (*total_parsed_argc_p > MAX_ARGS)
    die ("Exceeded maximum number of arguments %u", MAX_ARGS);

  while (argc > 0)
    {
      const char *arg = argv[0];

      if (strcmp (arg, "--help") == 0)
        {
          usage (EXIT_SUCCESS, stdout);
        }
      else if (strcmp (arg, "--version") == 0)
        {
          print_version_and_exit ();
        }
      else if (strcmp (arg, "--args") == 0)
        {
          int the_fd;
          char *endptr;
          const char *p, *data_end;
          size_t data_len;
          cleanup_free const char **data_argv = NULL;
          const char **data_argv_copy;
          int data_argc;
          int i;

          if (in_file)
            die ("--args not supported in arguments file");

          if (argc < 2)
            die ("--args takes an argument");

          the_fd = strtol (argv[1], &endptr, 10);
          if (argv[1][0] == 0 || endptr[0] != 0 || the_fd < 0)
            die ("Invalid fd: %s", argv[1]);

          /* opt_args_data is essentially a recursive argv array, which we must
           * keep allocated until exit time, since its argv entries get used
           * by the other cases in parse_args_recurse() when we recurse. */
          opt_args_data = load_file_data (the_fd, &data_len);
          if (opt_args_data == NULL)
            die_with_error ("Can't read --args data");
          (void) close (the_fd);

          data_end = opt_args_data + data_len;
          data_argc = 0;

          p = opt_args_data;
          while (p != NULL && p < data_end)
            {
              data_argc++;
              (*total_parsed_argc_p)++;
              if (*total_parsed_argc_p > MAX_ARGS)
                die ("Exceeded maximum number of arguments %u", MAX_ARGS);
              p = memchr (p, 0, data_end - p);
              if (p != NULL)
                p++;
            }

          data_argv = xcalloc (sizeof (char *) * (data_argc + 1));

          i = 0;
          p = opt_args_data;
          while (p != NULL && p < data_end)
            {
              /* Note: load_file_data always adds a nul terminator, so this is safe
               * even for the last string. */
              data_argv[i++] = p;
              p = memchr (p, 0, data_end - p);
              if (p != NULL)
                p++;
            }

          data_argv_copy = data_argv; /* Don't change data_argv, we need to free it */
          parse_args_recurse (&data_argc, &data_argv_copy, TRUE, total_parsed_argc_p);

          argv += 1;
          argc -= 1;
        }
      else if (strcmp (arg, "--unshare-all") == 0)
        {
          /* Keep this in order with the older (legacy) --unshare arguments,
           * we use the --try variants of user and cgroup, since we want
           * to support systems/kernels without support for those.
           */
          opt_unshare_user_try = opt_unshare_ipc = opt_unshare_pid =
            opt_unshare_uts = opt_unshare_cgroup_try =
            opt_unshare_net = TRUE;
        }
      /* Begin here the older individual --unshare variants */
      else if (strcmp (arg, "--unshare-user") == 0)
        {
          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;
        }
      else if (strcmp (arg, "--unshare-pid") == 0)
        {
          opt_unshare_pid = TRUE;
        }
      else if (strcmp (arg, "--unshare-net") == 0)
        {
          opt_unshare_net = TRUE;
        }
      else if (strcmp (arg, "--unshare-uts") == 0)
        {
          opt_unshare_uts = TRUE;
        }
      else if (strcmp (arg, "--unshare-cgroup") == 0)
        {
          opt_unshare_cgroup = TRUE;
        }
      else if (strcmp (arg, "--unshare-cgroup-try") == 0)
        {
          opt_unshare_cgroup_try = TRUE;
        }
      /* Begin here the newer --share variants */
      else if (strcmp (arg, "--share-net") == 0)
        {
          opt_unshare_net = FALSE;
        }
      /* End --share variants, other arguments begin */
      else if (strcmp (arg, "--chdir") == 0)
        {
          if (argc < 2)
            die ("--chdir takes one argument");

          opt_chdir_path = argv[1];
          argv++;
          argc--;
        }
      else if (strcmp (arg, "--remount-ro") == 0)
        {
          if (argc < 2)
            die ("--remount-ro takes one argument");

          SetupOp *op = setup_op_new (SETUP_REMOUNT_RO_NO_RECURSIVE);
          op->dest = argv[1];

          argv++;
          argc--;
        }
      else if (strcmp(arg, "--bind") == 0 ||
               strcmp(arg, "--bind-try") == 0)
        {
          if (argc < 3)
            die ("%s takes two arguments", arg);

          op = setup_op_new (SETUP_BIND_MOUNT);
          op->source = argv[1];
          op->dest = argv[2];
          if (strcmp(arg, "--bind-try") == 0)
            op->flags = ALLOW_NOTEXIST;

          argv += 2;
          argc -= 2;
        }
      else if (strcmp(arg, "--ro-bind") == 0 ||
               strcmp(arg, "--ro-bind-try") == 0)
        {
          if (argc < 3)
            die ("%s takes two arguments", arg);

          op = setup_op_new (SETUP_RO_BIND_MOUNT);
          op->source = argv[1];
          op->dest = argv[2];
          if (strcmp(arg, "--ro-bind-try") == 0)
            op->flags = ALLOW_NOTEXIST;

          argv += 2;
          argc -= 2;
        }
      else if (strcmp (arg, "--dev-bind") == 0 ||
               strcmp (arg, "--dev-bind-try") == 0)
        {
          if (argc < 3)
            die ("%s takes two arguments", arg);

          op = setup_op_new (SETUP_DEV_BIND_MOUNT);
          op->source = argv[1];
          op->dest = argv[2];
          if (strcmp(arg, "--dev-bind-try") == 0)
            op->flags = ALLOW_NOTEXIST;

          argv += 2;
          argc -= 2;
        }
      else if (strcmp (arg, "--proc") == 0)
        {
          if (argc < 2)
            die ("--proc takes an argument");

          op = setup_op_new (SETUP_MOUNT_PROC);
          op->dest = argv[1];

          argv += 1;
          argc -= 1;
        }
      else if (strcmp (arg, "--exec-label") == 0)
        {
          if (argc < 2)
            die ("--exec-label takes an argument");
          opt_exec_label = argv[1];
          die_unless_label_valid (opt_exec_label);

          argv += 1;
          argc -= 1;
        }
      else if (strcmp (arg, "--file-label") == 0)
        {
          if (argc < 2)
            die ("--file-label takes an argument");
          opt_file_label = argv[1];
          die_unless_label_valid (opt_file_label);
          if (label_create_file (opt_file_label))
            die_with_error ("--file-label setup failed");

          argv += 1;
          argc -= 1;
        }
      else if (strcmp (arg, "--dev") == 0)
        {
          if (argc < 2)
            die ("--dev takes an argument");

          op = setup_op_new (SETUP_MOUNT_DEV);
          op->dest = argv[1];
          opt_needs_devpts = TRUE;

          argv += 1;
          argc -= 1;
        }
      else if (strcmp (arg, "--tmpfs") == 0)
        {
          if (argc < 2)
            die ("--tmpfs takes an argument");

          op = setup_op_new (SETUP_MOUNT_TMPFS);
          op->dest = argv[1];

          argv += 1;
          argc -= 1;
        }
      else if (strcmp (arg, "--mqueue") == 0)
        {
          if (argc < 2)
            die ("--mqueue takes an argument");

          op = setup_op_new (SETUP_MOUNT_MQUEUE);
          op->dest = argv[1];

          argv += 1;
          argc -= 1;
        }
      else if (strcmp (arg, "--dir") == 0)
        {
          if (argc < 2)
            die ("--dir takes an argument");

          op = setup_op_new (SETUP_MAKE_DIR);
          op->dest = argv[1];

          argv += 1;
          argc -= 1;
        }
      else if (strcmp (arg, "--file") == 0)
        {
          int file_fd;
          char *endptr;

          if (argc < 3)
            die ("--file takes two arguments");

          file_fd = strtol (argv[1], &endptr, 10);
          if (argv[1][0] == 0 || endptr[0] != 0 || file_fd < 0)
            die ("Invalid fd: %s", argv[1]);

          op = setup_op_new (SETUP_MAKE_FILE);
          op->fd = file_fd;
          op->dest = argv[2];

          argv += 2;
          argc -= 2;
        }
      else if (strcmp (arg, "--bind-data") == 0)
        {
          int file_fd;
          char *endptr;

          if (argc < 3)
            die ("--bind-data takes two arguments");

          file_fd = strtol (argv[1], &endptr, 10);
          if (argv[1][0] == 0 || endptr[0] != 0 || file_fd < 0)
            die ("Invalid fd: %s", argv[1]);

          op = setup_op_new (SETUP_MAKE_BIND_FILE);
          op->fd = file_fd;
          op->dest = argv[2];

          argv += 2;
          argc -= 2;
        }
      else if (strcmp (arg, "--ro-bind-data") == 0)
        {
          int file_fd;
          char *endptr;

          if (argc < 3)
            die ("--ro-bind-data takes two arguments");

          file_fd = strtol (argv[1], &endptr, 10);
          if (argv[1][0] == 0 || endptr[0] != 0 || file_fd < 0)
            die ("Invalid fd: %s", argv[1]);

          op = setup_op_new (SETUP_MAKE_RO_BIND_FILE);
          op->fd = file_fd;
          op->dest = argv[2];

          argv += 2;
          argc -= 2;
        }
      else if (strcmp (arg, "--symlink") == 0)
        {
          if (argc < 3)
            die ("--symlink takes two arguments");

          op = setup_op_new (SETUP_MAKE_SYMLINK);
          op->source = argv[1];
          op->dest = argv[2];

          argv += 2;
          argc -= 2;
        }
      else if (strcmp (arg, "--lock-file") == 0)
        {
          if (argc < 2)
            die ("--lock-file takes an argument");

          (void) lock_file_new (argv[1]);

          argv += 1;
          argc -= 1;
        }
      else if (strcmp (arg, "--sync-fd") == 0)
        {
          int the_fd;
          char *endptr;

          if (argc < 2)
            die ("--sync-fd takes an argument");

          the_fd = strtol (argv[1], &endptr, 10);
          if (argv[1][0] == 0 || endptr[0] != 0 || the_fd < 0)
            die ("Invalid fd: %s", argv[1]);

          opt_sync_fd = the_fd;

          argv += 1;
          argc -= 1;
        }
      else if (strcmp (arg, "--block-fd") == 0)
        {
          int the_fd;
          char *endptr;

          if (argc < 2)
            die ("--block-fd takes an argument");

          the_fd = strtol (argv[1], &endptr, 10);
          if (argv[1][0] == 0 || endptr[0] != 0 || the_fd < 0)
            die ("Invalid fd: %s", argv[1]);

          opt_block_fd = the_fd;

          argv += 1;
          argc -= 1;
        }
      else if (strcmp (arg, "--userns-block-fd") == 0)
        {
          int the_fd;
          char *endptr;

          if (argc < 2)
            die ("--userns-block-fd takes an argument");

          the_fd = strtol (argv[1], &endptr, 10);
          if (argv[1][0] == 0 || endptr[0] != 0 || the_fd < 0)
            die ("Invalid fd: %s", argv[1]);

          opt_userns_block_fd = the_fd;

          argv += 1;
          argc -= 1;
        }
      else if (strcmp (arg, "--info-fd") == 0)
        {
          int the_fd;
          char *endptr;

          if (argc < 2)
            die ("--info-fd takes an argument");

          the_fd = strtol (argv[1], &endptr, 10);
          if (argv[1][0] == 0 || endptr[0] != 0 || the_fd < 0)
            die ("Invalid fd: %s", argv[1]);

          opt_info_fd = the_fd;

          argv += 1;
          argc -= 1;
        }
      else if (strcmp (arg, "--json-status-fd") == 0)
        {
          int the_fd;
          char *endptr;

          if (argc < 2)
            die ("--json-status-fd takes an argument");

          the_fd = strtol (argv[1], &endptr, 10);
          if (argv[1][0] == 0 || endptr[0] != 0 || the_fd < 0)
            die ("Invalid fd: %s", argv[1]);

          opt_json_status_fd = the_fd;

          argv += 1;
          argc -= 1;
        }
      else if (strcmp (arg, "--seccomp") == 0)
        {
          int the_fd;
          char *endptr;

          if (argc < 2)
            die ("--seccomp takes an argument");

          the_fd = strtol (argv[1], &endptr, 10);
          if (argv[1][0] == 0 || endptr[0] != 0 || the_fd < 0)
            die ("Invalid fd: %s", argv[1]);

          opt_seccomp_fd = the_fd;

          argv += 1;
          argc -= 1;
        }
      else if (strcmp (arg, "--setenv") == 0)
        {
          if (argc < 3)
            die ("--setenv takes two arguments");

          xsetenv (argv[1], argv[2], 1);

          argv += 2;
          argc -= 2;
        }
      else if (strcmp (arg, "--unsetenv") == 0)
        {
          if (argc < 2)
            die ("--unsetenv takes an argument");

          xunsetenv (argv[1]);

          argv += 1;
          argc -= 1;
        }
      else if (strcmp (arg, "--uid") == 0)
        {
          int the_uid;
          char *endptr;

          if (argc < 2)
            die ("--uid takes an argument");

          the_uid = strtol (argv[1], &endptr, 10);
          if (argv[1][0] == 0 || endptr[0] != 0 || the_uid < 0)
            die ("Invalid uid: %s", argv[1]);

          opt_sandbox_uid = the_uid;

          argv += 1;
          argc -= 1;
        }
      else if (strcmp (arg, "--gid") == 0)
        {
          int the_gid;
          char *endptr;

          if (argc < 2)
            die ("--gid takes an argument");

          the_gid = strtol (argv[1], &endptr, 10);
          if (argv[1][0] == 0 || endptr[0] != 0 || the_gid < 0)
            die ("Invalid gid: %s", argv[1]);

          opt_sandbox_gid = the_gid;

          argv += 1;
          argc -= 1;
        }
      else if (strcmp (arg, "--hostname") == 0)
        {
          if (argc < 2)
            die ("--hostname takes an argument");

          op = setup_op_new (SETUP_SET_HOSTNAME);
          op->dest = argv[1];
          op->flags = NO_CREATE_DEST;

          opt_sandbox_hostname = argv[1];

          argv += 1;
          argc -= 1;
        }
      else if (strcmp (arg, "--new-session") == 0)
        {
          opt_new_session = TRUE;
        }
      else if (strcmp (arg, "--die-with-parent") == 0)
        {
          opt_die_with_parent = TRUE;
        }
      else if (strcmp (arg, "--as-pid-1") == 0)
        {
          opt_as_pid_1 = TRUE;
        }
      else if (strcmp (arg, "--cap-add") == 0)
        {
          cap_value_t cap;
          if (argc < 2)
            die ("--cap-add takes an argument");

          opt_cap_add_or_drop_used = TRUE;

          if (strcasecmp (argv[1], "ALL") == 0)
            {
              requested_caps[0] = requested_caps[1] = 0xFFFFFFFF;
            }
          else
            {
              if (cap_from_name (argv[1], &cap) < 0)
                die ("unknown cap: %s", argv[1]);

              if (cap < 32)
                requested_caps[0] |= CAP_TO_MASK_0 (cap);
              else
                requested_caps[1] |= CAP_TO_MASK_1 (cap - 32);
            }

          argv += 1;
          argc -= 1;
        }
      else if (strcmp (arg, "--cap-drop") == 0)
        {
          cap_value_t cap;
          if (argc < 2)
            die ("--cap-drop takes an argument");

          opt_cap_add_or_drop_used = TRUE;

          if (strcasecmp (argv[1], "ALL") == 0)
            {
              requested_caps[0] = requested_caps[1] = 0;
            }
          else
            {
              if (cap_from_name (argv[1], &cap) < 0)
                die ("unknown cap: %s", argv[1]);

              if (cap < 32)
                requested_caps[0] &= ~CAP_TO_MASK_0 (cap);
              else
                requested_caps[1] &= ~CAP_TO_MASK_1 (cap - 32);
            }

          argv += 1;
          argc -= 1;
        }
      else if (strcmp (arg, "--") == 0)
        {
          argv += 1;
          argc -= 1;
          break;
        }
      else if (*arg == '-')
        {
          die ("Unknown option %s", arg);
        }
      else
        {
          break;
        }

      argv++;
      argc--;
    }

  *argcp = argc;
  *argvp = argv;
}

Project: bubblewrap
File: bubblewrap.c

prctl_caps (uint32_t *caps, bool do_cap_bounding, bool do_set_ambient)
{
  unsigned long cap;

  /* We ignore both EINVAL and EPERM, as we are actually relying
   * on PR_SET_NO_NEW_PRIVS to ensure the right capabilities are
   * available.  EPERM in particular can happen with old, buggy
   * kernels.  See:
   *  https://github.com/projectatomic/bubblewrap/pull/175#issuecomment-278051373
   *  https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/commit/security/commoncap.c?id=160da84dbb39443fdade7151bc63a88f8e953077
   */
  for (cap = 0; cap <= CAP_LAST_CAP; cap++)
    {
      bool keep = FALSE;
      if (cap < 32)
        {
          if (CAP_TO_MASK_0 (cap) & caps[0])
            keep = TRUE;
        }
      else
        {
          if (CAP_TO_MASK_1 (cap) & caps[1])
            keep = TRUE;
        }

      if (keep && do_set_ambient)
        {
#ifdef PR_CAP_AMBIENT
          int res = prctl (PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, cap, 0, 0);
          if (res == -1 && !(errno == EINVAL || errno == EPERM))
            die_with_error ("Adding ambient capability %ld", cap);
#else
          /* We ignore the EINVAL that results from not having PR_CAP_AMBIENT
           * in the current kernel at runtime, so also ignore not having it
           * in the current kernel headers at compile-time */
#endif
        }

      if (!keep && do_cap_bounding)
        {
          int res = prctl (PR_CAPBSET_DROP, cap, 0, 0, 0);
          if (res == -1 && !(errno == EINVAL || errno == EPERM))
            die_with_error ("Dropping capability %ld from bounds", cap);
        }
    }
}

Project: bubblewrap
File: bubblewrap.c

privileged_op (int         privileged_op_socket,
               uint32_t    op,
               uint32_t    flags,
               const char *arg1,
               const char *arg2)
{
  if (privileged_op_socket != -1)
    {
      uint32_t buffer[2048];  /* 8k, but is int32 to guarantee nice alignment */
      PrivSepOp *op_buffer = (PrivSepOp *) buffer;
      size_t buffer_size = sizeof (PrivSepOp);
      uint32_t arg1_offset = 0, arg2_offset = 0;

      /* We're unprivileged, send this request to the privileged part */

      if (arg1 != NULL)
        {
          arg1_offset = buffer_size;
          buffer_size += strlen (arg1) + 1;
        }
      if (arg2 != NULL)
        {
          arg2_offset = buffer_size;
          buffer_size += strlen (arg2) + 1;
        }

      if (buffer_size >= sizeof (buffer))
        die ("privilege separation operation to large");

      op_buffer->op = op;
      op_buffer->flags = flags;
      op_buffer->arg1_offset = arg1_offset;
      op_buffer->arg2_offset = arg2_offset;
      if (arg1 != NULL)
        strcpy ((char *) buffer + arg1_offset, arg1);
      if (arg2 != NULL)
        strcpy ((char *) buffer + arg2_offset, arg2);

      if (write (privileged_op_socket, buffer, buffer_size) != buffer_size)
        die ("Can't write to privileged_op_socket");

      if (read (privileged_op_socket, buffer, 1) != 1)
        die ("Can't read from privileged_op_socket");

      return;
    }

  /*
   * This runs a privileged request for the unprivileged setup
   * code. Note that since the setup code is unprivileged it is not as
   * trusted, so we need to verify that all requests only affect the
   * child namespace as set up by the privileged parts of the setup,
   * and that all the code is very careful about handling input.
   *
   * This means:
   *  * Bind mounts are safe, since we always use filesystem namespace. They
   *     must be recursive though, as otherwise you can use a non-recursive bind
   *     mount to access an otherwise over-mounted mountpoint.
   *  * Mounting proc, tmpfs, mqueue, devpts in the child namespace is assumed to
   *    be safe.
   *  * Remounting RO (even non-recursive) is safe because it decreases privileges.
   *  * sethostname() is safe only if we set up a UTS namespace
   */
  switch (op)
    {
    case PRIV_SEP_OP_DONE:
      break;

    case PRIV_SEP_OP_REMOUNT_RO_NO_RECURSIVE:
      if (bind_mount (proc_fd, NULL, arg2, BIND_READONLY) != 0)
        die_with_error ("Can't remount readonly on %s", arg2);
      break;

    case PRIV_SEP_OP_BIND_MOUNT:
      /* We always bind directories recursively, otherwise this would let us
         access files that are otherwise covered on the host */
      if (bind_mount (proc_fd, arg1, arg2, BIND_RECURSIVE | flags) != 0)
        die_with_error ("Can't bind mount %s on %s", arg1, arg2);
      break;

    case PRIV_SEP_OP_PROC_MOUNT:
      if (mount ("proc", arg1, "proc", MS_NOSUID | MS_NOEXEC | MS_NODEV, NULL) != 0)
        die_with_error ("Can't mount proc on %s", arg1);
      break;

    case PRIV_SEP_OP_TMPFS_MOUNT:
      {
        cleanup_free char *opt = label_mount ("mode=0755", opt_file_label);
        if (mount ("tmpfs", arg1, "tmpfs", MS_NOSUID | MS_NODEV, opt) != 0)
          die_with_error ("Can't mount tmpfs on %s", arg1);
        break;
      }

    case PRIV_SEP_OP_DEVPTS_MOUNT:
      if (mount ("devpts", arg1, "devpts", MS_NOSUID | MS_NOEXEC,
                 "newinstance,ptmxmode=0666,mode=620") != 0)
        die_with_error ("Can't mount devpts on %s", arg1);
      break;

    case PRIV_SEP_OP_MQUEUE_MOUNT:
      if (mount ("mqueue", arg1, "mqueue", 0, NULL) != 0)
        die_with_error ("Can't mount mqueue on %s", arg1);
      break;

    case PRIV_SEP_OP_SET_HOSTNAME:
      /* This is checked at the start, but lets verify it here in case
         something manages to send hacked priv-sep operation requests. */
      if (!opt_unshare_uts)
        die ("Refusing to set hostname in original namespace");
      if (sethostname (arg1, strlen(arg1)) != 0)
        die_with_error ("Can't set hostname to %s", arg1);
      break;

    default:
      die ("Unexpected privileged op %d", op);
    }
}

Project: bubblewrap
File: bubblewrap.c

propagate_exit_status (int status)
{
  if (WIFEXITED (status))
    return WEXITSTATUS (status);

  /* The process died of a signal, we can't really report that, but we
   * can at least be bash-compatible. The bash manpage says:
   *   The return value of a simple command is its
   *   exit status, or 128+n if the command is
   *   terminated by signal n.
   */
  if (WIFSIGNALED (status))
    return 128 + WTERMSIG (status);

  /* Weird? */
  return 255;
}

Project: bubblewrap
File: bubblewrap.c

resolve_symlinks_in_ops (void)
{
  SetupOp *op;

  for (op = ops; op != NULL; op = op->next)
    {
      const char *old_source;

      switch (op->type)
        {
        case SETUP_RO_BIND_MOUNT:
        case SETUP_DEV_BIND_MOUNT:
        case SETUP_BIND_MOUNT:
          old_source = op->source;
          op->source = realpath (old_source, NULL);
          if (op->source == NULL)
            {
              if (op->flags & ALLOW_NOTEXIST && errno == ENOENT)
                op->source = old_source;
              else
                die_with_error("Can't find source path %s", old_source);
            }
          break;
        default:
          break;
        }
    }
}

Project: bubblewrap
File: bubblewrap.c

set_ambient_capabilities (void)
{
  if (is_privileged)
    return;
  prctl_caps (requested_caps, FALSE, TRUE);
}

Project: bubblewrap
File: bubblewrap.c

switch_to_user_with_privs (void)
{
  /* If we're in a new user namespace, we got back the bounding set, clear it again */
  if (opt_unshare_user)
    drop_cap_bounding_set (FALSE);

  if (!is_privileged)
    return;

  /* Tell kernel not clear capabilities when later dropping root uid */
  if (prctl (PR_SET_KEEPCAPS, 1, 0, 0, 0) < 0)
    die_with_error ("prctl(PR_SET_KEEPCAPS) failed");

  if (setuid (opt_sandbox_uid) < 0)
    die_with_error ("unable to drop root uid");

  /* Regain effective required capabilities from permitted */
  set_required_caps ();
}

Project: bubblewrap
File: bubblewrap.c

usage (int ecode, FILE *out)
{
  fprintf (out, "usage: %s [OPTIONS...] [--] COMMAND [ARGS...]\n\n", argv0);

  fprintf (out,
           "    --help                       Print this help\n"
           "    --version                    Print version\n"
           "    --args FD                    Parse NUL-separated args from FD\n"
           "    --unshare-all                Unshare every namespace we support by default\n"
           "    --share-net                  Retain the network namespace (can only combine with --unshare-all)\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"
           "    --unshare-uts                Create new uts namespace\n"
           "    --unshare-cgroup             Create new cgroup namespace\n"
           "    --unshare-cgroup-try         Create new cgroup namespace if possible else continue by skipping it\n"
           "    --uid UID                    Custom uid in the sandbox (requires --unshare-user)\n"
           "    --gid GID                    Custom gid in the sandbox (requires --unshare-user)\n"
           "    --hostname NAME              Custom hostname in the sandbox (requires --unshare-uts)\n"
           "    --chdir DIR                  Change directory to DIR\n"
           "    --setenv VAR VALUE           Set an environment variable\n"
           "    --unsetenv VAR               Unset an environment variable\n"
           "    --lock-file DEST             Take a lock on DEST while sandbox is running\n"
           "    --sync-fd FD                 Keep this fd open while sandbox is running\n"
           "    --bind SRC DEST              Bind mount the host path SRC on DEST\n"
           "    --bind-try SRC DEST          Equal to --bind but ignores non-existent SRC\n"
           "    --dev-bind SRC DEST          Bind mount the host path SRC on DEST, allowing device access\n"
           "    --dev-bind-try SRC DEST      Equal to --dev-bind but ignores non-existent SRC\n"
           "    --ro-bind SRC DEST           Bind mount the host path SRC readonly on DEST\n"
           "    --ro-bind-try SRC DEST       Equal to --ro-bind but ignores non-existent SRC\n"
           "    --remount-ro DEST            Remount DEST as readonly; does not recursively remount\n"
           "    --exec-label LABEL           Exec label for the sandbox\n"
           "    --file-label LABEL           File label for temporary sandbox content\n"
           "    --proc DEST                  Mount new procfs on DEST\n"
           "    --dev DEST                   Mount new dev on DEST\n"
           "    --tmpfs DEST                 Mount new tmpfs on DEST\n"
           "    --mqueue DEST                Mount new mqueue on DEST\n"
           "    --dir DEST                   Create dir at DEST\n"
           "    --file FD DEST               Copy from FD to destination DEST\n"
           "    --bind-data FD DEST          Copy from FD to file which is bind-mounted on DEST\n"
           "    --ro-bind-data FD DEST       Copy from FD to file which is readonly bind-mounted on DEST\n"
           "    --symlink SRC DEST           Create symlink at DEST with target SRC\n"
           "    --seccomp FD                 Load and use seccomp rules from FD\n"
           "    --block-fd FD                Block on FD until some data to read is available\n"
           "    --userns-block-fd FD         Block on FD until the user namespace is ready\n"
           "    --info-fd FD                 Write information about the running container to FD\n"
           "    --json-status-fd FD          Write container status to FD as multiple JSON documents\n"
           "    --new-session                Create a new terminal session\n"
           "    --die-with-parent            Kills with SIGKILL child process (COMMAND) when bwrap or bwrap's parent dies.\n"
           "    --as-pid-1                   Do not install a reaper process with PID=1\n"
           "    --cap-add CAP                Add cap CAP when running as privileged user\n"
           "    --cap-drop CAP               Drop cap CAP when running as privileged user\n"
          );
  exit (ecode);
}

Project: bubblewrap
File: bubblewrap.c

write_uid_gid_map (uid_t sandbox_uid,
                   uid_t parent_uid,
                   uid_t sandbox_gid,
                   uid_t parent_gid,
                   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_t old_fsuid = -1;

  if (pid == -1)
    dir = xstrdup ("self");
  else
    dir = xasprintf ("%d", pid);

  dir_fd = openat (proc_fd, dir, 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 %d 1\n"
                         "%d %d 1\n", overflow_uid, sandbox_uid, parent_uid);
  else
    uid_map = xasprintf ("%d %d 1\n", sandbox_uid, parent_uid);

  if (map_root && parent_gid != 0 && sandbox_gid != 0)
    gid_map = xasprintf ("0 %d 1\n"
                         "%d %d 1\n", overflow_gid, sandbox_gid, parent_gid);
  else
    gid_map = xasprintf ("%d %d 1\n", sandbox_gid, parent_gid);

  /* We have to be root to be allowed to write to the uid map
   * for setuid apps, so temporary set fsuid to 0 */
  if (is_privileged)
    old_fsuid = setfsuid (0);

  if (write_file_at (dir_fd, "uid_map", uid_map) != 0)
    die_with_error ("setting up uid map");

  if (deny_groups &&
      write_file_at (dir_fd, "setgroups", "deny\n") != 0)
    {
      /* If /proc/[pid]/setgroups does not exist, assume we are
       * running a linux kernel < 3.19, i.e. we live with the
       * vulnerability known as CVE-2014-8989 in older kernels
       * where setgroups does not exist.
       */
      if (errno != ENOENT)
        die_with_error ("error writing to setgroups");
    }

  if (write_file_at (dir_fd, "gid_map", gid_map) != 0)
    die_with_error ("setting up gid map");

  if (is_privileged)
    {
      setfsuid (old_fsuid);
      if (setfsuid (-1) != real_uid)
        die ("Unable to re-set fsuid");
    }
}