Temp
diff --git a/buildstream/_platform/unix.py b/buildstream/_platform/unix.py
index 6d7b463..7c3a805 100644
--- a/buildstream/_platform/unix.py
+++ b/buildstream/_platform/unix.py
@@ -22,7 +22,7 @@
 
 from .._artifactcache.tarcache import TarCache
 from .._exceptions import PlatformError
-from ..sandbox import SandboxChroot
+from ..sandbox import SandboxChroot, SandboxUserChroot
 
 from . import Platform
 
@@ -35,12 +35,14 @@
         self._artifact_cache = TarCache(context)
 
         # Not necessarily 100% reliable, but we want to fail early.
-        if os.geteuid() != 0:
-            raise PlatformError("Root privileges are required to run without bubblewrap.")
+        # if os.geteuid() != 0:
+        #     raise PlatformError("Root privileges are required to run without bubblewrap.")
 
     @property
     def artifactcache(self):
         return self._artifact_cache
 
     def create_sandbox(self, *args, **kwargs):
-        return SandboxChroot(*args, **kwargs)
+        # We can optionally create a SandboxUserChroot
+        return SandboxUserChroot(*args, **kwargs)
+        # return SandboxChroot(*args, **kwargs)
diff --git a/buildstream/sandbox/__init__.py b/buildstream/sandbox/__init__.py
index 7ee871c..c55e53e 100644
--- a/buildstream/sandbox/__init__.py
+++ b/buildstream/sandbox/__init__.py
@@ -19,5 +19,6 @@
 #        Tristan Maat <tristan.maat@codethink.co.uk>
 
 from .sandbox import Sandbox, SandboxFlags
+from ._sandboxuserchroot import SandboxUserChroot
 from ._sandboxchroot import SandboxChroot
 from ._sandboxbwrap import SandboxBwrap
diff --git a/buildstream/sandbox/_sandboxchroot.py b/buildstream/sandbox/_sandboxchroot.py
index 584f0e1..8ac0ba4 100644
--- a/buildstream/sandbox/_sandboxchroot.py
+++ b/buildstream/sandbox/_sandboxchroot.py
@@ -91,24 +91,12 @@
 
         return status
 
-    # chroot()
+    # popen()
     #
-    # A helper function to chroot into the rootfs.
+    # A helper function to create and manage a subprocess. We mimic
+    # subprocess.Popen's interface here.
     #
-    # Args:
-    #    rootfs (str): The path of the sysroot to chroot into
-    #    command (list): The command to execute in the chroot env
-    #    stdin (file): The stdin
-    #    stdout (file): The stdout
-    #    stderr (file): The stderr
-    #    cwd (str): The current working directory
-    #    env (dict): The environment variables to use while executing the command
-    #    flags (:class:`SandboxFlags`): The flags to enable on the sandbox
-    #
-    # Returns:
-    #    (int): The exit code of the executed command
-    #
-    def chroot(self, rootfs, command, stdin, stdout, stderr, cwd, env, flags):
+    def popen(self, command, **kwargs):
         def kill_proc():
             if process:
                 # First attempt to gracefully terminate
@@ -128,55 +116,76 @@
             group_id = os.getpgid(process.pid)
             os.killpg(group_id, signal.SIGCONT)
 
+        with _signals.suspendable(suspend_proc, resume_proc), \
+             _signals.terminator(kill_proc):
+            process = subprocess.Popen(command, **kwargs)
+
+            # Wait for the child process to finish, ensuring that
+            # a SIGINT has exactly the effect the user probably
+            # expects (i.e. let the child process handle it).
+            try:
+                while True:
+                    try:
+                        _, status = os.waitpid(process.pid, 0)
+                        # If the process exits due to a signal, we
+                        # brutally murder it to avoid zombies
+                        if not os.WIFEXITED(status):
+                            utils._kill_process_tree(process.pid)
+
+                    # Unlike in the bwrap case, here only the main
+                    # process seems to receive the SIGINT. We pass
+                    # on the signal to the child and then continue
+                    # to wait.
+                    except KeyboardInterrupt:
+                        process.send_signal(signal.SIGINT)
+                        continue
+
+                    break
+            # If we can't find the process, it has already died of
+            # its own accord, and therefore we don't need to check
+            # or kill anything.
+            except psutil.NoSuchProcess:
+                pass
+
+            # Return the exit code - see the documentation for
+            # os.WEXITSTATUS to see why this is required.
+            if os.WIFEXITED(status):
+                code = os.WEXITSTATUS(status)
+            else:
+                code = -1
+
+        return code
+
+    # chroot()
+    #
+    # A helper function to chroot into the rootfs.
+    #
+    # Args:
+    #    rootfs (str): The path of the sysroot to chroot into
+    #    command (list): The command to execute in the chroot env
+    #    stdin (file): The stdin
+    #    stdout (file): The stdout
+    #    stderr (file): The stderr
+    #    cwd (str): The current working directory
+    #    env (dict): The environment variables to use while executing the command
+    #    flags (:class:`SandboxFlags`): The flags to enable on the sandbox
+    #
+    # Returns:
+    #    (int): The exit code of the executed command
+    #
+    def chroot(self, rootfs, command, stdin, stdout, stderr, cwd, env, flags):
         try:
-            with _signals.suspendable(suspend_proc, resume_proc), _signals.terminator(kill_proc):
-                process = subprocess.Popen(
-                    command,
-                    close_fds=True,
-                    cwd=os.path.join(rootfs, cwd.lstrip(os.sep)),
-                    env=env,
-                    stdin=stdin,
-                    stdout=stdout,
-                    stderr=stderr,
-                    # If you try to put gtk dialogs here Tristan (either)
-                    # will personally scald you
-                    preexec_fn=lambda: (os.chroot(rootfs), os.chdir(cwd)),
-                    start_new_session=flags & SandboxFlags.INTERACTIVE
-                )
-
-                # Wait for the child process to finish, ensuring that
-                # a SIGINT has exactly the effect the user probably
-                # expects (i.e. let the child process handle it).
-                try:
-                    while True:
-                        try:
-                            _, status = os.waitpid(process.pid, 0)
-                            # If the process exits due to a signal, we
-                            # brutally murder it to avoid zombies
-                            if not os.WIFEXITED(status):
-                                utils._kill_process_tree(process.pid)
-
-                        # Unlike in the bwrap case, here only the main
-                        # process seems to receive the SIGINT. We pass
-                        # on the signal to the child and then continue
-                        # to wait.
-                        except KeyboardInterrupt:
-                            process.send_signal(signal.SIGINT)
-                            continue
-
-                        break
-                # If we can't find the process, it has already died of
-                # its own accord, and therefore we don't need to check
-                # or kill anything.
-                except psutil.NoSuchProcess:
-                    pass
-
-                # Return the exit code - see the documentation for
-                # os.WEXITSTATUS to see why this is required.
-                if os.WIFEXITED(status):
-                    code = os.WEXITSTATUS(status)
-                else:
-                    code = -1
+            return self.popen(command,
+                              close_fds=True,
+                              cwd=os.path.join(rootfs, cwd.lstrip(os.sep)),
+                              env=env,
+                              stdin=stdin,
+                              stdout=stdout,
+                              stderr=stderr,
+                              # If you try to put gtk dialogs here Tristan (either)
+                              # will personally scald you
+                              preexec_fn=lambda: (os.chroot(rootfs), os.chdir(cwd)),
+                              start_new_session=flags & SandboxFlags.INTERACTIVE)
 
         except subprocess.SubprocessError as e:
             # Exceptions in preexec_fn are simply reported as
@@ -189,8 +198,6 @@
             else:
                 raise SandboxError('Could not run command {}: {}'.format(command, e)) from e
 
-        return code
-
     # create_devices()
     #
     # Create the nodes in /dev/ usually required for builds (null,
diff --git a/buildstream/sandbox/_sandboxuserchroot.py b/buildstream/sandbox/_sandboxuserchroot.py
new file mode 100644
index 0000000..6e98e8d
--- /dev/null
+++ b/buildstream/sandbox/_sandboxuserchroot.py
@@ -0,0 +1,158 @@
+import os
+import pwd
+import sys
+import stat
+import subprocess
+from pathlib import Path
+from contextlib import ExitStack, contextmanager
+
+from .. import utils
+from . import SandboxFlags
+from ._mount import MountMap
+from .._exceptions import SandboxError
+from ._sandboxchroot import SandboxChroot
+from .._message import Message, MessageType
+
+
+# The sandbox directory needs to fulfill a few criteria:
+#   - Its parents must be owned by root
+#   - It and its children must be owned by the user defined in the
+#     configuration file
+#   - Neither it nor its parents must be more permissive than 755
+#   - It cannot be in a directory mounted into the sandbox (duh)
+#
+# If we allow the user to specify this location (we probably should),
+# those criteria would be nice to check for before sandbox execution.
+# Although userchroot itself checks for some, the error messages are
+# not particularly helpful.
+#
+SANDBOX_DIR = '/usr/local/sandboxes'
+
+
+def assert_userchroot_configuration():
+    configured = False
+    user = pwd.getpwuid(os.getuid())[0]
+    userchroot = utils.get_host_tool('userchroot')
+    config = Path(userchroot).parents[1].joinpath('etc/userchroot.conf')
+
+    if config.exists():
+        with open(config, 'r') as configf:
+            for line in configf:
+                if line.rstrip() == '{}:{}'.format(user, SANDBOX_DIR):
+                    configured = True
+                    break
+
+    if not configured:
+        raise SandboxError("'userchroot' is not configured correctly. "
+                           "Please add '{}:{}' to '{}'"
+                           .format(user, SANDBOX_DIR, config))
+
+
+class SandboxUserChroot(SandboxChroot):
+    def run(self, command, flags, *, cwd=None, env=None):
+        # Ensure sandbox default configuration
+        if cwd is None:
+            cwd = self._get_work_directory() or '/'
+
+        if env is None:
+            env = self._get_environment()
+
+        if isinstance(command, str):
+            command = [command]
+
+        stdout, stderr = self._get_output()
+
+        # Create the mount map, this will tell us where
+        # each mount point needs to be mounted from and to
+        self.mount_map = MountMap(self, True)
+
+        # Make sure userchroot is configured correctly
+        assert_userchroot_configuration()
+
+        with ExitStack() as stack:
+            # Create sysroot
+            try:
+                os.makedirs(SANDBOX_DIR, exist_ok=True)
+                rootfs = stack.enter_context(utils._tempdir(dir=SANDBOX_DIR))
+            except PermissionError as e:
+                raise SandboxError('Could not create sysroot in {}: {}'
+                                   .format(SANDBOX_DIR, e)) from e
+
+            stack.enter_context(self.stage_sysroot(rootfs, flags, stdout, stderr))
+
+            # Chroot!
+            if flags & SandboxFlags.INTERACTIVE:
+                stdin = sys.stdin
+            else:
+                stdin = stack.enter_context(open(os.devnull, 'r'))
+
+            status = self.chroot(rootfs, command, stdin, stdout, stderr,
+                                 cwd, env, flags)
+        return status
+
+    def chroot(self, rootfs, command, stdin, stdout, stderr, cwd, env,
+               flags):
+        # Create a script in the root directory of the sysroot to
+        # execute the given commands.
+        script = "\n".join(["#!/bin/sh"] + command)
+        scriptpath = os.path.join(rootfs, 'buildstream-run.sh')
+
+        with open(scriptpath, 'w') as scriptfile:
+            scriptfile.write(script)
+        perms = os.stat(scriptpath).st_mode
+        os.chmod(scriptpath, perms & stat.S_IXUSR)
+
+        # Execute the script with userchroot
+        try:
+            command = [utils.get_host_tool('userchroot'),
+                       rootfs,
+                       '--install-devices',
+                       '/buildstream-run.sh']
+            return self.popen(command,
+                              env=env,
+                              stdin=stdin,
+                              stdout=stdout,
+                              stderr=stderr,
+                              cwd=os.path.join(rootfs, cwd.lstrip(os.sep)),
+                              start_new_session=flags & SandboxFlags.INTERACTIVE)
+
+        except subprocess.SubprocessError as e:
+            raise SandboxError('Could not run command {}: {}'.format(command, e)) from e
+
+    # mount_dirs()
+    #
+    # Since we aren't root we can't arbitrarily mount directories. Yet
+    # we *require* our FUSE filesystem for at least some operations.
+    #
+    # FUSE can be mounted by users, therefore this mount function
+    # attempts to safely mount our FUSE system on top of a copy of our
+    # sandbox files.
+    #
+    # This *does* mean that this platform is significantly slower than
+    # others, unfortunately...
+    #
+    @contextmanager
+    def stage_sysroot(self, rootfs, flags, stdout, stderr):
+        def mount(d):
+            overrides = self._get_mount_sources()
+
+            if d in overrides:
+                src = overrides[d]
+            else:
+                src = self.mount_map.get_mount_source(d)
+
+            dst = os.path.join(rootfs, d.lstrip(os.sep))
+
+            self.info('Mounting {} to {}'.format(src, dst))
+
+        with self.mount_map.mounted(rootfs):
+            yield
+
+            mount('/')
+
+            for mark in self._get_marked_directories():
+                mount(mark['directory'])
+
+    def info(self, message):
+        msg = Message('sandbox', MessageType.INFO, message)
+        self._get_context()._message(msg)