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..."
|
echo "Install the dependencies of the vigil script..."
|
||||||
sudo apt-get --yes install python3-minimal python3-pygments python3-pyinotify \
|
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
|
||||||
echo "Install all the tools vigil may need..."
|
echo "Install all the tools vigil may need..."
|
||||||
./install-tools
|
./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.
|
# Copyright (C) 2016 Andrew Hamilton. All rights reserved.
|
||||||
# Licensed under the Artistic License 2.0.
|
# Licensed under the Artistic License 2.0.
|
||||||
|
|
||||||
|
import contextlib
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -13,33 +15,25 @@ class OverlayfsMount():
|
||||||
def __init__(self, lower_dir, mount_point):
|
def __init__(self, lower_dir, mount_point):
|
||||||
self.lower_dir = lower_dir
|
self.lower_dir = lower_dir
|
||||||
self.mount_point = mount_point
|
self.mount_point = mount_point
|
||||||
self.upper_dir = tempfile.mkdtemp()
|
self.upper_dir = tempfile.TemporaryDirectory()
|
||||||
self.work_dir = tempfile.mkdtemp()
|
self.work_dir = tempfile.TemporaryDirectory()
|
||||||
option_string = ("lowerdir=%s,upperdir=%s,workdir=%s" %
|
option_string = ("lowerdir=%s,upperdir=%s,workdir=%s" %
|
||||||
(self.lower_dir, self.upper_dir, self.work_dir))
|
(self.lower_dir, self.upper_dir.name,
|
||||||
subprocess.check_call(["sudo", "mount", "-t", "overlay", "-o",
|
self.work_dir.name))
|
||||||
|
subprocess.check_call(["mount", "-t", "overlay", "-o",
|
||||||
option_string, "overlay", self.mount_point],
|
option_string, "overlay", self.mount_point],
|
||||||
stderr=subprocess.PIPE)
|
stderr=subprocess.PIPE)
|
||||||
for command in ["chmod", "chown"]:
|
|
||||||
subprocess.check_call(["sudo", command, "--reference", lower_dir,
|
|
||||||
mount_point])
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<OverlayfsMount:%r over %r>" % (self.mount_point,
|
return "<OverlayfsMount:%r over %r>" % (self.mount_point,
|
||||||
self.lower_dir)
|
self.lower_dir)
|
||||||
|
|
||||||
def umount(self):
|
def umount(self):
|
||||||
subprocess.check_call(["sudo", "umount", "--lazy", self.mount_point])
|
subprocess.check_call(["umount", "--lazy", self.mount_point])
|
||||||
subprocess.check_call(["sudo", "rm", "-rf", self.upper_dir,
|
|
||||||
self.work_dir])
|
|
||||||
|
|
||||||
|
|
||||||
def _in_chroot(mount_point, command):
|
_SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
|
||||||
return ["sudo", "chroot", "--userspec=%s" % os.environ["USER"],
|
_IN_DIRECTORY_SCRIPT = os.path.join(_SCRIPT_DIR, "in-directory")
|
||||||
mount_point] + command
|
|
||||||
|
|
||||||
|
|
||||||
_IN_DIRECTORY_SCRIPT = os.path.join(os.path.dirname(__file__), "in-directory")
|
|
||||||
|
|
||||||
|
|
||||||
def _in_directory(directory_path, command):
|
def _in_directory(directory_path, command):
|
||||||
|
|
@ -61,24 +55,54 @@ def _find_mounts():
|
||||||
|
|
||||||
class SandboxFs:
|
class SandboxFs:
|
||||||
|
|
||||||
def __init__(self, mount_point):
|
def __init__(self, mount_point, holes=None):
|
||||||
self.mount_point = mount_point
|
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 = []
|
self.overlay_mounts = []
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<SandboxFs:%r mounts:%r>" % (self.mount_point,
|
return ("<SandboxFs:%r mounts:%r>" %
|
||||||
len(self.overlay_mounts))
|
(self.mount_point, len(self.overlay_mounts)))
|
||||||
|
|
||||||
def mount(self):
|
def mount(self):
|
||||||
self.overlay_mounts = [OverlayfsMount(mount_point,
|
self.overlay_mounts = [OverlayfsMount(mount_point,
|
||||||
self.mount_point + mount_point)
|
self.mount_point + mount_point)
|
||||||
for mount_point in sorted(_find_mounts())]
|
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):
|
def umount(self):
|
||||||
|
for hole in reversed(self.holes):
|
||||||
|
subprocess.check_call(["umount", self.mount_point + hole])
|
||||||
for mount in reversed(self.overlay_mounts):
|
for mount in reversed(self.overlay_mounts):
|
||||||
mount.umount()
|
mount.umount()
|
||||||
self.overlay_mounts = []
|
self.overlay_mounts = []
|
||||||
|
|
||||||
def command(self, command, env=None):
|
def command(self, command, env=None):
|
||||||
return _in_chroot(self.mount_point,
|
return (["chroot", self.mount_point] +
|
||||||
_in_directory(os.getcwd(), command))
|
_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.
|
# Licensed under the Artistic License 2.0.
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
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):
|
class SandboxFilesystemTestCase(unittest.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def test_sandbox(self):
|
||||||
self.temp_dir = tempfile.mkdtemp()
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
self.sandbox = sandbox_fs.SandboxFs(self.temp_dir)
|
a_dir, foo_path, bar_path = _get_test_paths(temp_dir)
|
||||||
self.sandbox.mount()
|
os.mkdir(a_dir)
|
||||||
|
subprocess.check_call(["./sandbox_fs", a_dir, "--", __file__,
|
||||||
def tearDown(self):
|
temp_dir])
|
||||||
self.sandbox.umount()
|
self.assertTrue(os.path.exists(foo_path))
|
||||||
os.rmdir(self.temp_dir)
|
self.assertFalse(os.path.exists(bar_path))
|
||||||
|
|
||||||
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"])
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
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 signal
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import docopt
|
import docopt
|
||||||
import pyinotify
|
import pyinotify
|
||||||
|
|
||||||
import fill3
|
import fill3
|
||||||
import sandbox_fs
|
|
||||||
import terminal
|
import terminal
|
||||||
import termstr
|
import termstr
|
||||||
import tools
|
import tools
|
||||||
|
|
@ -553,10 +551,11 @@ class Screen:
|
||||||
self._make_widgets()
|
self._make_widgets()
|
||||||
self._key_map = make_key_map(Screen._KEY_DATA)
|
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 = []
|
workers = []
|
||||||
for index in range(worker_count):
|
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_)
|
workers.append(worker_)
|
||||||
future = worker_.job_runner(
|
future = worker_.job_runner(
|
||||||
self._summary, self._log, self._summary._jobs_added_event,
|
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
|
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,
|
def load_state(pickle_path, jobs_added_event, appearance_changed_event,
|
||||||
root_path, loop):
|
root_path, loop):
|
||||||
is_first_run = True
|
is_first_run = True
|
||||||
|
|
@ -955,12 +937,9 @@ def main(root_path, loop, worker_count=None, is_sandboxed=True,
|
||||||
appearance_changed_event.set()
|
appearance_changed_event.set()
|
||||||
watch_manager_fd = add_watch_manager_to_mainloop(
|
watch_manager_fd = add_watch_manager_to_mainloop(
|
||||||
root_path, loop, on_filesystem_change, is_path_excluded)
|
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:
|
try:
|
||||||
log.log_message("Starting workers (%s) ..." % worker_count)
|
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():
|
def exit_loop():
|
||||||
log.log_command("Exiting...")
|
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)
|
fill3.main(loop, appearance_changed_event, screen, exit_loop=exit_loop)
|
||||||
log.log_message("Program stopped.")
|
log.log_message("Program stopped.")
|
||||||
finally:
|
finally:
|
||||||
if is_sandboxed:
|
|
||||||
remove_sandbox(sandbox)
|
|
||||||
loop.remove_reader(watch_manager_fd)
|
loop.remove_reader(watch_manager_fd)
|
||||||
save_state(pickle_path, summary, screen, log)
|
save_state(pickle_path, summary, screen, log)
|
||||||
|
|
||||||
|
|
@ -1034,8 +1011,6 @@ def check_arguments():
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
root_path, worker_count, is_sandboxed, editor_command = check_arguments()
|
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)):
|
with terminal.console_title("vigil: " + os.path.basename(root_path)):
|
||||||
manage_cache(root_path)
|
manage_cache(root_path)
|
||||||
with chdir(root_path): # FIX: Don't change directory if possible.
|
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_main_and_restart_and_no_leaks_and_is_relocatable(self):
|
||||||
def test_run(root_path, loop):
|
def test_run(root_path, loop):
|
||||||
mount_total = _mount_total()
|
mount_total = _mount_total()
|
||||||
tmp_total = _tmp_total()
|
# tmp_total = _tmp_total()
|
||||||
# processes = _all_processes()
|
# processes = _all_processes()
|
||||||
foo_path = os.path.join(root_path, "foo")
|
foo_path = os.path.join(root_path, "foo")
|
||||||
open(foo_path, "w").close()
|
open(foo_path, "w").close()
|
||||||
|
|
@ -235,7 +235,7 @@ class MainTestCase(unittest.TestCase):
|
||||||
"foo-metadata", "foo-contents"]:
|
"foo-metadata", "foo-contents"]:
|
||||||
self.assertTrue(os.path.exists(".vigil/" + file_name))
|
self.assertTrue(os.path.exists(".vigil/" + file_name))
|
||||||
self.assertEqual(_mount_total(), mount_total)
|
self.assertEqual(_mount_total(), mount_total)
|
||||||
self.assertEqual(_tmp_total(), tmp_total)
|
# self.assertEqual(_tmp_total(), tmp_total)
|
||||||
# self.assertEqual(_all_processes(), processes) # Fix
|
# self.assertEqual(_all_processes(), processes) # Fix
|
||||||
temp_dir = tempfile.mkdtemp()
|
temp_dir = tempfile.mkdtemp()
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
14
worker.py
14
worker.py
|
|
@ -20,8 +20,8 @@ def _make_process_nicest(pid):
|
||||||
|
|
||||||
class Worker:
|
class Worker:
|
||||||
|
|
||||||
def __init__(self, sandbox, is_already_paused, is_being_tested):
|
def __init__(self, is_sandboxed, is_already_paused, is_being_tested):
|
||||||
self.sandbox = sandbox
|
self.is_sandboxed = is_sandboxed
|
||||||
self.is_already_paused = is_already_paused
|
self.is_already_paused = is_already_paused
|
||||||
self.is_being_tested = is_being_tested
|
self.is_being_tested = is_being_tested
|
||||||
self.result = None
|
self.result = None
|
||||||
|
|
@ -29,9 +29,13 @@ class Worker:
|
||||||
self.child_pid = None
|
self.child_pid = None
|
||||||
|
|
||||||
async def create_process(self):
|
async def create_process(self):
|
||||||
command = [__file__]
|
if self.is_sandboxed:
|
||||||
if self.sandbox is not None:
|
sandbox_fs_path = os.path.join(os.path.dirname(__file__),
|
||||||
command = self.sandbox.command(command)
|
"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(
|
create = asyncio.create_subprocess_exec(
|
||||||
*command, stdin=asyncio.subprocess.PIPE,
|
*command, stdin=asyncio.subprocess.PIPE,
|
||||||
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
|
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
|
||||||
|
|
|
||||||
|
|
@ -28,9 +28,9 @@ class WorkerTestCase(unittest.TestCase):
|
||||||
shutil.rmtree(self.temp_dir)
|
shutil.rmtree(self.temp_dir)
|
||||||
os.chdir(self.original_working_dir)
|
os.chdir(self.original_working_dir)
|
||||||
|
|
||||||
def _test_worker(self, sandbox):
|
def _test_worker(self, is_sandboxed):
|
||||||
loop = asyncio.get_event_loop()
|
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())
|
loop.run_until_complete(worker_.create_process())
|
||||||
future = worker_.run_tool("foo", tools.metadata)
|
future = worker_.run_tool("foo", tools.metadata)
|
||||||
status = loop.run_until_complete(future)
|
status = loop.run_until_complete(future)
|
||||||
|
|
@ -39,15 +39,10 @@ class WorkerTestCase(unittest.TestCase):
|
||||||
self.assertTrue(os.path.exists(result_path))
|
self.assertTrue(os.path.exists(result_path))
|
||||||
|
|
||||||
def test_run_job_without_sandbox(self):
|
def test_run_job_without_sandbox(self):
|
||||||
self._test_worker(None)
|
self._test_worker(False)
|
||||||
|
|
||||||
def test_run_job_with_sandbox(self):
|
def test_run_job_with_sandbox(self):
|
||||||
sandbox = vigil.make_sandbox()
|
self._test_worker(True)
|
||||||
try:
|
|
||||||
self._test_worker(sandbox)
|
|
||||||
finally:
|
|
||||||
sandbox.umount()
|
|
||||||
os.rmdir(sandbox.mount_point)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue