summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndrew Leeming <a_p_leeming@hotmail.com>2016-10-06 13:22:43 +0100
committerGitHub <noreply@github.com>2016-10-06 13:22:43 +0100
commit0ba8e54c70f7aa96c89df9879545397ef118b3b6 (patch)
tree0385fbf2fb70843e40f884cc236c02351acc49cc
parentdd86a9c5ad513f801923e869b61ab3afcc4b7f77 (diff)
parente3cb6f63a891941a7bf13b764bc65abf47db9227 (diff)
downloadsandboxlib-0ba8e54c70f7aa96c89df9879545397ef118b3b6.tar.gz
Merge pull request #24 from CodethinkLabs/tiagogomes/unit-test-fixes
Tiagogomes/unit test fixes
-rw-r--r--HACKING.rst3
-rw-r--r--README.rst12
-rw-r--r--sandboxlib/__init__.py6
-rw-r--r--sandboxlib/chroot.py23
-rw-r--r--sandboxlib/linux_user_chroot.py2
-rw-r--r--tests/test_all.py122
6 files changed, 129 insertions, 39 deletions
diff --git a/HACKING.rst b/HACKING.rst
index 9144885..e771db0 100644
--- a/HACKING.rst
+++ b/HACKING.rst
@@ -40,6 +40,9 @@ Running the automated test suite
Use ``tox``. You'll need 'py.test', 'tox' and their dependencies available.
+You will also need to have installed the C library static libraries for
+-static linking (glibc-static package in Fedora).
+
Note that a lot of the tests will be skipped or fail if you don't run as
'root', because some of the sandboxing backends only work when you are the
'root' user. The test suite could handle this better than it does.
diff --git a/README.rst b/README.rst
index ced547f..bb241c3 100644
--- a/README.rst
+++ b/README.rst
@@ -140,25 +140,23 @@ kexec() system call).
.. _seccomp: http://man7.org/linux/man-pages/man2/seccomp.2.html
-xdg-app (GNOME Application Sandboxing)
+flatpak (GNOME Application Sandboxing)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-The xdg-app_ project started from a desire in the GNOME_ desktop project to
+The flatpak_ project started from a desire in the GNOME_ desktop project to
allow running 3rd-party applications with some isolation from the host system.
Mobile platforms like Android and iOS have been doing this for some time
already.
It implements sandboxing mainly using the 'namespaces' feature of Linux. Find
-out more about `the project <https://wiki.gnome.org/Projects/SandboxedApps>`_
-and `how the sandboxing is implemented
-<https://wiki.gnome.org/Projects/SandboxedApps/Sandbox>`_.
+out more about `the project <http://flatpak.org/>`_.
-xdg-app_ is for a specific use case of desktop application sandboxing, so it
+flatpak_ is for a specific use case of desktop application sandboxing, so it
doesn't make sense for sandboxlib to wrap it. Use it directly if it suits your
purpose!
.. _GNOME: https://www.gnome.org/
-.. _xdg-app: https://github.com/alexlarsson/xdg-app
+.. _flatpak: https://github.com/flatpak/flatpak
Containerisation
----------------
diff --git a/sandboxlib/__init__.py b/sandboxlib/__init__.py
index beaf7b2..1418225 100644
--- a/sandboxlib/__init__.py
+++ b/sandboxlib/__init__.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2015 Codethink Limited
+# Copyright (C) 2015, 2016 Codethink Limited
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -195,7 +195,7 @@ def validate_extra_mounts(extra_mounts):
for mount_entry in extra_mounts:
if mount_entry[1] is None:
raise AssertionError(
- "Mount point empty in mount entry %s" % mount_entry)
+ "Mount point empty in mount entry %s" % str(mount_entry))
if len(mount_entry) == 3:
full_mount_entry = list(mount_entry) + ['']
@@ -203,7 +203,7 @@ def validate_extra_mounts(extra_mounts):
full_mount_entry = list(mount_entry)
else:
raise AssertionError(
- "Invalid mount entry in 'extra_mounts': %s" % mount_entry)
+ "Invalid mount entry in 'extra_mounts': %s" % str(mount_entry))
# Convert all the entries to strings to prevent type errors later
# on. None is special cased to the empty string, as str(None) is
diff --git a/sandboxlib/chroot.py b/sandboxlib/chroot.py
index c0d13f8..9f5ed23 100644
--- a/sandboxlib/chroot.py
+++ b/sandboxlib/chroot.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2015 Codethink Limited
+# Copyright (C) 2015-2016 Codethink Limited
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -33,6 +33,7 @@ that the sandbox contains a shell and we do some hack like running
'''
+import sys
import contextlib
import multiprocessing
import os
@@ -101,7 +102,10 @@ def mount(source, path, mount_type, mount_options):
# We depend on the host system's 'mount' program here, which is a
# little sad. It's possible to call the libc's mount() function
# directly from Python using the 'ctypes' library, and perhaps we
- # should do that instead.
+ # should do that instead. The 'mount' requires that a source is
+ # given even for the special filesystems (e.g. proc, tmpfs), so we
+ # use the mount type as the source if the latter is not explicitly
+ # given.
def is_none(value):
return value in (None, 'none', '')
@@ -112,6 +116,8 @@ def mount(source, path, mount_type, mount_options):
argv.extend(('-o', mount_options))
if not is_none(source):
argv.append(source)
+ else:
+ argv.append(mount_type)
argv.append(path)
exit, out, err = sandboxlib._run_command(
@@ -220,13 +226,16 @@ def run_sandbox(command, cwd=None, env=None,
with mount_all(filesystem_root, extra_mounts):
- # Awful hack to ensure string-escape is loaded:
+ # Awful hack to ensure string-escape/unicode-escape are loaded:
#
# this ensures that when propagating an exception back from
- # the child process in a chroot, the required string-escape
- # python module is already in memory and no attempt to
- # lazy load it in the chroot is made.
- unused = "Some Text".encode('string-escape')
+ # the child process in a chroot, the required string-escape/
+ # unicode-escape python modules are already in memory and no
+ # attempt to lazy load them in the chroot is made.
+ if sys.version_info.major == 2:
+ unused = "Some Text".encode('string-escape')
+ elif sys.version_info.major == 3:
+ unused = "Some Text".encode('unicode-escape')
process = multiprocessing.Process(
target=run_command_in_chroot,
diff --git a/sandboxlib/linux_user_chroot.py b/sandboxlib/linux_user_chroot.py
index 979d5d5..cab5344 100644
--- a/sandboxlib/linux_user_chroot.py
+++ b/sandboxlib/linux_user_chroot.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2015 Codethink Limited
+# Copyright (C) 2015-2016 Codethink Limited
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
diff --git a/tests/test_all.py b/tests/test_all.py
index eed2f79..eed6d26 100644
--- a/tests/test_all.py
+++ b/tests/test_all.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2015 Codethink Limited
+# Copyright (C) 2015-2016 Codethink Limited
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -40,7 +40,7 @@ def test_no_output(sandboxlib_executor):
'''Test ignoring of stderr/stdout.
We could use run_sandbox_with_redirection() and not get the 'err' and 'out'
- paramemter at all, but we may as well test that they are indeed None.
+ parameters at all, but we may as well test that they are indeed None.
'''
exit, out, err = sandboxlib_executor.run_sandbox(
@@ -50,14 +50,39 @@ def test_no_output(sandboxlib_executor):
assert out is None
assert err is None
-
-def test_stdout(sandboxlib_executor):
+def test_output(sandboxlib_executor):
exit, out, err = sandboxlib_executor.run_sandbox(['echo', 'xyzzy'])
assert exit == 0
assert out.decode('unicode-escape') == 'xyzzy\n'
assert err.decode('unicode-escape') == ''
+ exit, out, err = sandboxlib_executor.run_sandbox(
+ ['sh', '-c', 'echo xyzzy >&2; exit 11'])
+
+ assert exit == 11
+ assert out.decode('unicode-escape') == ''
+ assert err.decode('unicode-escape') == 'xyzzy\n'
+
+def test_output_redirection(sandboxlib_executor, tmpdir):
+ outlog_fp = str(tmpdir.join('outlog.txt'))
+ errlog_fp = str(tmpdir.join('errlog.txt'))
+ with open(outlog_fp, 'w') as outlog, open(errlog_fp, 'w') as errlog:
+ exit = sandboxlib_executor.run_sandbox_with_redirection(
+ ['sh', '-c', 'echo abcde; echo xyzzy >&2'],
+ stdout=outlog, stderr=errlog)
+
+ with open(outlog_fp) as outlog, open(errlog_fp) as errlog:
+ assert outlog.read() == 'abcde\n'
+ assert errlog.read() == 'xyzzy\n'
+
+ with open(outlog_fp, 'w') as outlog, open(errlog_fp, 'w') as errlog:
+ exit = sandboxlib_executor.run_sandbox_with_redirection(
+ ['sh', '-c', 'echo abcde; echo xyzzy >&2'],
+ stdout=outlog, stderr=sandboxlib.STDOUT)
+
+ with open(outlog_fp) as outlog:
+ assert outlog.read() == 'abcde\nxyzzy\n'
def test_current_working_directory(sandboxlib_executor, tmpdir):
exit, out, err = sandboxlib_executor.run_sandbox(
@@ -67,6 +92,33 @@ def test_current_working_directory(sandboxlib_executor, tmpdir):
assert out.decode('unicode-escape') == '%s\n' % str(tmpdir)
assert err.decode('unicode-escape') == ''
+def test_environment(sandboxlib_executor):
+ exit, out, err = sandboxlib_executor.run_sandbox(
+ ['env'], env={'foo': 'bar'})
+
+ assert exit == 0
+ assert out.decode('unicode-escape') == 'foo=bar\n'
+ assert err.decode('unicode-escape') == ''
+
+def test_isolated_mounts(sandboxlib_executor):
+ if sandboxlib_executor == sandboxlib.chroot:
+ pytest.skip('chroot backend does not support mounts isolation')
+ elif sandboxlib_executor == sandboxlib.linux_user_chroot:
+ # linux-user-chroot always creates a new mount namespace
+ pass
+
+def test_isolated_network(sandboxlib_executor):
+ if sandboxlib_executor == sandboxlib.chroot:
+ pytest.skip('chroot backend does not support network isolation')
+
+ exit, out, err = sandboxlib_executor.run_sandbox(
+ ['sh', '-c', 'cat /proc/net/dev | sed 1,2d | cut -f1 -d:'],
+ network='isolated')
+
+ assert exit == 0
+ assert out.decode('unicode-escape').strip() == 'lo'
+ assert err.decode('unicode-escape') == ''
+
class TestMounts(object):
@pytest.fixture()
@@ -83,7 +135,7 @@ class TestMounts(object):
def test_mount_proc(self, sandboxlib_executor, mounts_test_sandbox):
exit, out, err = sandboxlib_executor.run_sandbox(
- ['test-file-or-directory-exists', '/proc'],
+ ['/bin/test-file-or-directory-exists', '/proc'],
filesystem_root=str(mounts_test_sandbox),
extra_mounts=[(None, '/proc', 'proc')])
@@ -93,7 +145,7 @@ class TestMounts(object):
def test_mount_tmpfs(self, sandboxlib_executor, mounts_test_sandbox):
exit, out, err = sandboxlib_executor.run_sandbox(
- ['test-file-or-directory-exists', '/dev/shm'],
+ ['/bin/test-file-or-directory-exists', '/dev/shm'],
filesystem_root=str(mounts_test_sandbox),
extra_mounts=[(None, '/dev/shm', 'tmpfs')])
@@ -101,6 +153,21 @@ class TestMounts(object):
assert out.decode('unicode-escape') == "/dev/shm exists"
assert exit == 0
+ def test_invalid_mount_specs(self, sandboxlib_executor):
+ with pytest.raises(AssertionError) as excinfo:
+ exit, out, err = sandboxlib_executor.run_sandbox(
+ ['true'], extra_mounts=[('proc', None, 'tmpfs')])
+
+ assert excinfo.value.message == (
+ "Mount point empty in mount entry ('proc', None, 'tmpfs')")
+
+ with pytest.raises(AssertionError) as excinfo:
+ exit, out, err = sandboxlib_executor.run_sandbox(
+ ['true'], extra_mounts=[('proc', 'tmpfs')])
+
+ assert excinfo.value.message == (
+ "Invalid mount entry in 'extra_mounts': ('proc', 'tmpfs')")
+
class TestWriteablePaths(object):
@pytest.fixture()
@@ -114,7 +181,7 @@ class TestWriteablePaths(object):
bin_path.join('test-file-is-writable').chmod(0o755)
data_path = sandbox_path.mkdir('data')
- data_path.mkdir('1')
+ data_path = data_path.mkdir('1')
data_path.join('canary').write("Please don't overwrite me.")
return sandbox_path
@@ -125,7 +192,7 @@ class TestWriteablePaths(object):
pytest.xfail("chroot backend doesn't support read-only paths.")
exit, out, err = sandboxlib_executor.run_sandbox(
- ['test-file-is-writable', '/data/1/canary'],
+ ['/bin/test-file-is-writable', '/data/1/canary'],
filesystem_root=str(writable_paths_test_sandbox),
filesystem_writable_paths='none')
@@ -140,7 +207,7 @@ class TestWriteablePaths(object):
pytest.xfail("chroot backend doesn't support read-only paths.")
exit, out, err = sandboxlib_executor.run_sandbox(
- ['test-file-is-writable', '/data/1/canary'],
+ ['/bin/test-file-is-writable', '/data/1/canary'],
filesystem_root=str(writable_paths_test_sandbox),
filesystem_writable_paths=['/data/1'])
@@ -152,7 +219,7 @@ class TestWriteablePaths(object):
def test_all_writable(self, sandboxlib_executor,
writable_paths_test_sandbox):
exit, out, err = sandboxlib_executor.run_sandbox(
- ['test-file-is-writable', '/data/1/canary'],
+ ['/bin/test-file-is-writable', '/data/1/canary'],
filesystem_root=str(writable_paths_test_sandbox),
filesystem_writable_paths='all')
@@ -167,12 +234,9 @@ class TestWriteablePaths(object):
pytest.xfail("chroot backend doesn't support read-only paths.")
exit, out, err = sandboxlib_executor.run_sandbox(
- ['test-file-is-writable', '/data/1/canary'],
+ ['/bin/test-file-is-writable', '/data/1/canary'],
filesystem_root=str(writable_paths_test_sandbox),
- filesystem_writable_paths='none',
- extra_mounts=[
- (None, '/data', 'tmpfs')
- ])
+ filesystem_writable_paths='none')
assert err.decode('unicode-escape') == ''
assert out.decode('unicode-escape') == \
@@ -185,12 +249,9 @@ class TestWriteablePaths(object):
pytest.xfail("chroot backend doesn't support read-only paths.")
exit, out, err = sandboxlib_executor.run_sandbox(
- ['test-file-is-writable', '/data/1/canary'],
+ ['/bin/test-file-is-writable', '/data/1/canary'],
filesystem_root=str(writable_paths_test_sandbox),
- filesystem_writable_paths=['/data'],
- extra_mounts=[
- (None, '/data', 'tmpfs')
- ])
+ filesystem_writable_paths=['/data'])
assert err.decode('unicode-escape') == ''
assert out.decode('unicode-escape') == \
@@ -201,8 +262,18 @@ class TestWriteablePaths(object):
def test_executor_for_platform():
'''Simple test of backend autodetection.'''
executor = sandboxlib.executor_for_platform()
- test_stdout(executor)
+ test_output(executor)
+def test_sandboxlib_backend_env_var(sandboxlib_executor):
+ executor_name = sandboxlib_executor.__name__.split('.')[-1]
+ os.environ["SANDBOXLIB_BACKEND"] = executor_name
+ executor = sandboxlib.executor_for_platform()
+ assert executor == sandboxlib_executor
+
+def test_sandboxlib_backend_env_var_unknown_executor():
+ executor = sandboxlib.executor_for_platform()
+ os.environ["SANDBOXLIB_BACKEND"] = "unknown"
+ assert executor == sandboxlib.executor_for_platform()
def test_degrade_config_for_capabilities(sandboxlib_executor):
'''Simple test of adjusting configuration for a given backend.'''
@@ -214,3 +285,12 @@ def test_degrade_config_for_capabilities(sandboxlib_executor):
out_config = sandboxlib_executor.degrade_config_for_capabilities(
in_config, warn=True)
+
+ if sandboxlib_executor == sandboxlib.chroot:
+ assert out_config == {
+ 'mounts': 'undefined',
+ 'network': 'undefined',
+ 'filesystem_writable_paths': 'all'
+ }
+ elif sandboxlib_executor == sandboxlib.linux_user_chroot:
+ assert out_config == in_config