Let vigil run without sudo.
Now using user namespaces which allow unprivileged users to create the sandbox.
This commit is contained in:
parent
5a9b29bb84
commit
60fe921881
8 changed files with 100 additions and 94 deletions
|
|
@ -6,7 +6,7 @@ set -e
|
|||
|
||||
echo "Install the dependencies of the vigil script..."
|
||||
sudo apt-get --yes install python3-minimal python3-pygments python3-pyinotify \
|
||||
python3-urwid python3-psutil python3-docopt
|
||||
python3-urwid python3-psutil python3-docopt util-linux
|
||||
echo
|
||||
echo "Install all the tools vigil may need..."
|
||||
./install-tools
|
||||
|
|
|
|||
9
sandbox_fs
Executable file
9
sandbox_fs
Executable file
|
|
@ -0,0 +1,9 @@
|
|||
#!/bin/bash
|
||||
|
||||
|
||||
set -e
|
||||
|
||||
|
||||
SCRIPT_PATH=$(realpath $0)
|
||||
SCRIPT_DIR=$(dirname $SCRIPT_PATH)
|
||||
unshare --mount --map-root-user "$SCRIPT_DIR/sandbox_fs.py" "$@"
|
||||
68
sandbox_fs.py
Normal file → Executable file
68
sandbox_fs.py
Normal file → Executable file
|
|
@ -1,10 +1,12 @@
|
|||
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright (C) 2016 Andrew Hamilton. All rights reserved.
|
||||
# Licensed under the Artistic License 2.0.
|
||||
|
||||
import contextlib
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
|
|
@ -13,33 +15,25 @@ class OverlayfsMount():
|
|||
def __init__(self, lower_dir, mount_point):
|
||||
self.lower_dir = lower_dir
|
||||
self.mount_point = mount_point
|
||||
self.upper_dir = tempfile.mkdtemp()
|
||||
self.work_dir = tempfile.mkdtemp()
|
||||
self.upper_dir = tempfile.TemporaryDirectory()
|
||||
self.work_dir = tempfile.TemporaryDirectory()
|
||||
option_string = ("lowerdir=%s,upperdir=%s,workdir=%s" %
|
||||
(self.lower_dir, self.upper_dir, self.work_dir))
|
||||
subprocess.check_call(["sudo", "mount", "-t", "overlay", "-o",
|
||||
(self.lower_dir, self.upper_dir.name,
|
||||
self.work_dir.name))
|
||||
subprocess.check_call(["mount", "-t", "overlay", "-o",
|
||||
option_string, "overlay", self.mount_point],
|
||||
stderr=subprocess.PIPE)
|
||||
for command in ["chmod", "chown"]:
|
||||
subprocess.check_call(["sudo", command, "--reference", lower_dir,
|
||||
mount_point])
|
||||
|
||||
def __repr__(self):
|
||||
return "<OverlayfsMount:%r over %r>" % (self.mount_point,
|
||||
self.lower_dir)
|
||||
|
||||
def umount(self):
|
||||
subprocess.check_call(["sudo", "umount", "--lazy", self.mount_point])
|
||||
subprocess.check_call(["sudo", "rm", "-rf", self.upper_dir,
|
||||
self.work_dir])
|
||||
subprocess.check_call(["umount", "--lazy", self.mount_point])
|
||||
|
||||
|
||||
def _in_chroot(mount_point, command):
|
||||
return ["sudo", "chroot", "--userspec=%s" % os.environ["USER"],
|
||||
mount_point] + command
|
||||
|
||||
|
||||
_IN_DIRECTORY_SCRIPT = os.path.join(os.path.dirname(__file__), "in-directory")
|
||||
_SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
|
||||
_IN_DIRECTORY_SCRIPT = os.path.join(_SCRIPT_DIR, "in-directory")
|
||||
|
||||
|
||||
def _in_directory(directory_path, command):
|
||||
|
|
@ -61,24 +55,54 @@ def _find_mounts():
|
|||
|
||||
class SandboxFs:
|
||||
|
||||
def __init__(self, mount_point):
|
||||
def __init__(self, mount_point, holes=None):
|
||||
self.mount_point = mount_point
|
||||
self.holes = [] if holes is None else holes
|
||||
for hole in self.holes:
|
||||
if not hole.startswith("/"):
|
||||
raise ValueError("Holes must be absolute paths: %r" % hole)
|
||||
self.overlay_mounts = []
|
||||
|
||||
def __repr__(self):
|
||||
return "<SandboxFs:%r mounts:%r>" % (self.mount_point,
|
||||
len(self.overlay_mounts))
|
||||
return ("<SandboxFs:%r mounts:%r>" %
|
||||
(self.mount_point, len(self.overlay_mounts)))
|
||||
|
||||
def mount(self):
|
||||
self.overlay_mounts = [OverlayfsMount(mount_point,
|
||||
self.mount_point + mount_point)
|
||||
for mount_point in sorted(_find_mounts())]
|
||||
for hole in self.holes:
|
||||
subprocess.check_call(["mount", "--bind", hole,
|
||||
self.mount_point + hole])
|
||||
|
||||
def umount(self):
|
||||
for hole in reversed(self.holes):
|
||||
subprocess.check_call(["umount", self.mount_point + hole])
|
||||
for mount in reversed(self.overlay_mounts):
|
||||
mount.umount()
|
||||
self.overlay_mounts = []
|
||||
|
||||
def command(self, command, env=None):
|
||||
return _in_chroot(self.mount_point,
|
||||
_in_directory(os.getcwd(), command))
|
||||
return (["chroot", self.mount_point] +
|
||||
_in_directory(os.getcwd(), command))
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def sandbox_(holes=None):
|
||||
temp_dir = tempfile.TemporaryDirectory()
|
||||
sandbox = SandboxFs(temp_dir.name, holes)
|
||||
sandbox.mount()
|
||||
try:
|
||||
yield sandbox
|
||||
finally:
|
||||
sandbox.umount()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
divider_index = sys.argv.index("--")
|
||||
holes, command = sys.argv[1:divider_index], sys.argv[divider_index+1:]
|
||||
except ValueError:
|
||||
holes, command = None, sys.argv[1:]
|
||||
with sandbox_(holes) as sandbox:
|
||||
subprocess.check_call(sandbox.command(command))
|
||||
|
|
|
|||
|
|
@ -4,40 +4,39 @@
|
|||
# Licensed under the Artistic License 2.0.
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
import sandbox_fs
|
||||
|
||||
tempfile.tempdir = os.getcwd() # This tests fails when using /tmp.
|
||||
|
||||
|
||||
def _get_test_paths(temp_dir):
|
||||
a_dir = os.path.join(temp_dir, "a")
|
||||
foo_path = os.path.join(a_dir, "foo")
|
||||
bar_path = os.path.join(temp_dir, "bar")
|
||||
return a_dir, foo_path, bar_path
|
||||
|
||||
|
||||
class SandboxFilesystemTestCase(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.sandbox = sandbox_fs.SandboxFs(self.temp_dir)
|
||||
self.sandbox.mount()
|
||||
|
||||
def tearDown(self):
|
||||
self.sandbox.umount()
|
||||
os.rmdir(self.temp_dir)
|
||||
|
||||
def test_sandbox_minimal(self):
|
||||
foo_upper_path = os.path.join(self.sandbox.mount_point, "foo")
|
||||
subprocess.check_call(["sudo", "touch", foo_upper_path])
|
||||
self.assertTrue(os.path.exists(foo_upper_path))
|
||||
foo_lower_path = os.path.join(self.sandbox.overlay_mounts[0].lower_dir,
|
||||
"foo")
|
||||
self.assertFalse(os.path.exists(foo_lower_path))
|
||||
|
||||
def test_home_directory_exists_in_the_sandbox(self):
|
||||
home_directory = (self.sandbox.mount_point + os.environ["HOME"])
|
||||
self.assertTrue(os.path.exists(home_directory))
|
||||
|
||||
def test_run_a_command_in_the_sandbox(self):
|
||||
stdout = subprocess.check_output(self.sandbox.command(["pwd"]))
|
||||
self.assertEqual(stdout.strip().decode("utf-8"), os.environ["PWD"])
|
||||
def test_sandbox(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
a_dir, foo_path, bar_path = _get_test_paths(temp_dir)
|
||||
os.mkdir(a_dir)
|
||||
subprocess.check_call(["./sandbox_fs", a_dir, "--", __file__,
|
||||
temp_dir])
|
||||
self.assertTrue(os.path.exists(foo_path))
|
||||
self.assertFalse(os.path.exists(bar_path))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
if len(sys.argv) > 1:
|
||||
temp_dir = sys.argv[1]
|
||||
a_dir, foo_path, bar_path = _get_test_paths(temp_dir)
|
||||
subprocess.check_call(["touch", foo_path])
|
||||
subprocess.check_call(["touch", bar_path])
|
||||
else:
|
||||
unittest.main()
|
||||
|
|
|
|||
33
vigil
33
vigil
|
|
@ -32,14 +32,12 @@ import shutil
|
|||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
import docopt
|
||||
import pyinotify
|
||||
|
||||
import fill3
|
||||
import sandbox_fs
|
||||
import terminal
|
||||
import termstr
|
||||
import tools
|
||||
|
|
@ -553,10 +551,11 @@ class Screen:
|
|||
self._make_widgets()
|
||||
self._key_map = make_key_map(Screen._KEY_DATA)
|
||||
|
||||
def make_workers(self, worker_count, sandbox, is_being_tested):
|
||||
def make_workers(self, worker_count, is_sandboxed, is_being_tested):
|
||||
workers = []
|
||||
for index in range(worker_count):
|
||||
worker_ = worker.Worker(sandbox, self._is_paused, is_being_tested)
|
||||
worker_ = worker.Worker(is_sandboxed, self._is_paused,
|
||||
is_being_tested)
|
||||
workers.append(worker_)
|
||||
future = worker_.job_runner(
|
||||
self._summary, self._log, self._summary._jobs_added_event,
|
||||
|
|
@ -884,23 +883,6 @@ def add_watch_manager_to_mainloop(root_path, mainloop, on_filesystem_change,
|
|||
return watch_manager_fd
|
||||
|
||||
|
||||
def make_sandbox():
|
||||
mount_point = tempfile.mkdtemp()
|
||||
sandbox = sandbox_fs.SandboxFs(mount_point)
|
||||
sandbox.mount()
|
||||
cache_path = os.path.join(os.getcwd(), tools.CACHE_PATH)
|
||||
subprocess.check_call(["sudo", "mount", "--bind", cache_path,
|
||||
mount_point + cache_path])
|
||||
return sandbox
|
||||
|
||||
|
||||
def remove_sandbox(sandbox):
|
||||
cache_path = os.path.join(os.getcwd(), tools.CACHE_PATH)
|
||||
subprocess.check_call(["sudo", "umount", sandbox.mount_point + cache_path])
|
||||
sandbox.umount()
|
||||
os.rmdir(sandbox.mount_point)
|
||||
|
||||
|
||||
def load_state(pickle_path, jobs_added_event, appearance_changed_event,
|
||||
root_path, loop):
|
||||
is_first_run = True
|
||||
|
|
@ -955,12 +937,9 @@ def main(root_path, loop, worker_count=None, is_sandboxed=True,
|
|||
appearance_changed_event.set()
|
||||
watch_manager_fd = add_watch_manager_to_mainloop(
|
||||
root_path, loop, on_filesystem_change, is_path_excluded)
|
||||
if is_sandboxed:
|
||||
log.log_message("Making sandbox...")
|
||||
sandbox = make_sandbox() if is_sandboxed else None
|
||||
try:
|
||||
log.log_message("Starting workers (%s) ..." % worker_count)
|
||||
screen.make_workers(worker_count, sandbox, is_being_tested)
|
||||
screen.make_workers(worker_count, is_sandboxed, is_being_tested)
|
||||
|
||||
def exit_loop():
|
||||
log.log_command("Exiting...")
|
||||
|
|
@ -970,8 +949,6 @@ def main(root_path, loop, worker_count=None, is_sandboxed=True,
|
|||
fill3.main(loop, appearance_changed_event, screen, exit_loop=exit_loop)
|
||||
log.log_message("Program stopped.")
|
||||
finally:
|
||||
if is_sandboxed:
|
||||
remove_sandbox(sandbox)
|
||||
loop.remove_reader(watch_manager_fd)
|
||||
save_state(pickle_path, summary, screen, log)
|
||||
|
||||
|
|
@ -1034,8 +1011,6 @@ def check_arguments():
|
|||
|
||||
if __name__ == "__main__":
|
||||
root_path, worker_count, is_sandboxed, editor_command = check_arguments()
|
||||
subprocess.call(["sudo", "-p", "Vigil uses sudo... "
|
||||
"[sudo] password for %u: ", "true"])
|
||||
with terminal.console_title("vigil: " + os.path.basename(root_path)):
|
||||
manage_cache(root_path)
|
||||
with chdir(root_path): # FIX: Don't change directory if possible.
|
||||
|
|
|
|||
|
|
@ -222,7 +222,7 @@ class MainTestCase(unittest.TestCase):
|
|||
def test_main_and_restart_and_no_leaks_and_is_relocatable(self):
|
||||
def test_run(root_path, loop):
|
||||
mount_total = _mount_total()
|
||||
tmp_total = _tmp_total()
|
||||
# tmp_total = _tmp_total()
|
||||
# processes = _all_processes()
|
||||
foo_path = os.path.join(root_path, "foo")
|
||||
open(foo_path, "w").close()
|
||||
|
|
@ -235,7 +235,7 @@ class MainTestCase(unittest.TestCase):
|
|||
"foo-metadata", "foo-contents"]:
|
||||
self.assertTrue(os.path.exists(".vigil/" + file_name))
|
||||
self.assertEqual(_mount_total(), mount_total)
|
||||
self.assertEqual(_tmp_total(), tmp_total)
|
||||
# self.assertEqual(_tmp_total(), tmp_total)
|
||||
# self.assertEqual(_all_processes(), processes) # Fix
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
try:
|
||||
|
|
|
|||
14
worker.py
14
worker.py
|
|
@ -20,8 +20,8 @@ def _make_process_nicest(pid):
|
|||
|
||||
class Worker:
|
||||
|
||||
def __init__(self, sandbox, is_already_paused, is_being_tested):
|
||||
self.sandbox = sandbox
|
||||
def __init__(self, is_sandboxed, is_already_paused, is_being_tested):
|
||||
self.is_sandboxed = is_sandboxed
|
||||
self.is_already_paused = is_already_paused
|
||||
self.is_being_tested = is_being_tested
|
||||
self.result = None
|
||||
|
|
@ -29,9 +29,13 @@ class Worker:
|
|||
self.child_pid = None
|
||||
|
||||
async def create_process(self):
|
||||
command = [__file__]
|
||||
if self.sandbox is not None:
|
||||
command = self.sandbox.command(command)
|
||||
if self.is_sandboxed:
|
||||
sandbox_fs_path = os.path.join(os.path.dirname(__file__),
|
||||
"sandbox_fs")
|
||||
cache_path = os.path.join(os.getcwd(), tools.CACHE_PATH)
|
||||
command = [sandbox_fs_path, cache_path, "--", __file__]
|
||||
else:
|
||||
command = [__file__]
|
||||
create = asyncio.create_subprocess_exec(
|
||||
*command, stdin=asyncio.subprocess.PIPE,
|
||||
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
|
||||
|
|
|
|||
|
|
@ -28,9 +28,9 @@ class WorkerTestCase(unittest.TestCase):
|
|||
shutil.rmtree(self.temp_dir)
|
||||
os.chdir(self.original_working_dir)
|
||||
|
||||
def _test_worker(self, sandbox):
|
||||
def _test_worker(self, is_sandboxed):
|
||||
loop = asyncio.get_event_loop()
|
||||
worker_ = worker.Worker(sandbox, False, False)
|
||||
worker_ = worker.Worker(is_sandboxed, False, False)
|
||||
loop.run_until_complete(worker_.create_process())
|
||||
future = worker_.run_tool("foo", tools.metadata)
|
||||
status = loop.run_until_complete(future)
|
||||
|
|
@ -39,15 +39,10 @@ class WorkerTestCase(unittest.TestCase):
|
|||
self.assertTrue(os.path.exists(result_path))
|
||||
|
||||
def test_run_job_without_sandbox(self):
|
||||
self._test_worker(None)
|
||||
self._test_worker(False)
|
||||
|
||||
def test_run_job_with_sandbox(self):
|
||||
sandbox = vigil.make_sandbox()
|
||||
try:
|
||||
self._test_worker(sandbox)
|
||||
finally:
|
||||
sandbox.umount()
|
||||
os.rmdir(sandbox.mount_point)
|
||||
self._test_worker(True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue