diff options
| author | jvoisin | 2019-10-12 16:13:49 -0700 |
|---|---|---|
| committer | jvoisin | 2019-10-12 16:13:49 -0700 |
| commit | 5f0b3beb46d09af26107fe5f80e63ddccb127a59 (patch) | |
| tree | f3d46e6e9dac60daa304d212bed62b17c019f7eb /libmat2/bubblewrap.py | |
| parent | 3cef7fe7fc81c1495a461a8594b1df69467536ea (diff) | |
Add a way to disable the sandbox
Due to bubblewrap's pickiness, mat2 can now be run
without a sandbox, even if bubblewrap is installed.
Diffstat (limited to 'libmat2/bubblewrap.py')
| -rw-r--r-- | libmat2/bubblewrap.py | 113 |
1 files changed, 113 insertions, 0 deletions
diff --git a/libmat2/bubblewrap.py b/libmat2/bubblewrap.py new file mode 100644 index 0000000..fb6fc9d --- /dev/null +++ b/libmat2/bubblewrap.py | |||
| @@ -0,0 +1,113 @@ | |||
| 1 | """ | ||
| 2 | Wrapper around a subset of the subprocess module, | ||
| 3 | that uses bwrap (bubblewrap) when it is available. | ||
| 4 | |||
| 5 | Instead of importing subprocess, other modules should use this as follows: | ||
| 6 | |||
| 7 | from . import subprocess | ||
| 8 | """ | ||
| 9 | |||
| 10 | import os | ||
| 11 | import shutil | ||
| 12 | import subprocess | ||
| 13 | import tempfile | ||
| 14 | from typing import List, Optional | ||
| 15 | |||
| 16 | |||
| 17 | __all__ = ['PIPE', 'run', 'CalledProcessError'] | ||
| 18 | PIPE = subprocess.PIPE | ||
| 19 | CalledProcessError = subprocess.CalledProcessError | ||
| 20 | |||
| 21 | |||
| 22 | def _get_bwrap_path() -> str: | ||
| 23 | bwrap_path = '/usr/bin/bwrap' | ||
| 24 | if os.path.isfile(bwrap_path): | ||
| 25 | if os.access(bwrap_path, os.X_OK): | ||
| 26 | return bwrap_path | ||
| 27 | |||
| 28 | raise RuntimeError("Unable to find bwrap") # pragma: no cover | ||
| 29 | |||
| 30 | |||
| 31 | # pylint: disable=bad-whitespace | ||
| 32 | def _get_bwrap_args(tempdir: str, | ||
| 33 | input_filename: str, | ||
| 34 | output_filename: Optional[str] = None) -> List[str]: | ||
| 35 | ro_bind_args = [] | ||
| 36 | cwd = os.getcwd() | ||
| 37 | |||
| 38 | # XXX: use --ro-bind-try once all supported platforms | ||
| 39 | # have a bubblewrap recent enough to support it. | ||
| 40 | ro_bind_dirs = ['/usr', '/lib', '/lib64', '/bin', '/sbin', cwd] | ||
| 41 | for bind_dir in ro_bind_dirs: | ||
| 42 | if os.path.isdir(bind_dir): # pragma: no cover | ||
| 43 | ro_bind_args.extend(['--ro-bind', bind_dir, bind_dir]) | ||
| 44 | |||
| 45 | ro_bind_files = ['/etc/ld.so.cache'] | ||
| 46 | for bind_file in ro_bind_files: | ||
| 47 | if os.path.isfile(bind_file): # pragma: no cover | ||
| 48 | ro_bind_args.extend(['--ro-bind', bind_file, bind_file]) | ||
| 49 | |||
| 50 | args = ro_bind_args + \ | ||
| 51 | ['--dev', '/dev', | ||
| 52 | '--proc', '/proc', | ||
| 53 | '--chdir', cwd, | ||
| 54 | '--tmpfs', '/tmp', | ||
| 55 | '--unshare-user-try', | ||
| 56 | '--unshare-ipc', | ||
| 57 | '--unshare-pid', | ||
| 58 | '--unshare-net', | ||
| 59 | '--unshare-uts', | ||
| 60 | '--unshare-cgroup-try', | ||
| 61 | '--new-session', | ||
| 62 | '--cap-drop', 'all', | ||
| 63 | # XXX: enable --die-with-parent once all supported platforms have | ||
| 64 | # a bubblewrap recent enough to support it. | ||
| 65 | # '--die-with-parent', | ||
| 66 | ] | ||
| 67 | |||
| 68 | if output_filename: | ||
| 69 | # Mount an empty temporary directory where the sandboxed | ||
| 70 | # process will create its output file | ||
| 71 | output_dirname = os.path.dirname(os.path.abspath(output_filename)) | ||
| 72 | args.extend(['--bind', tempdir, output_dirname]) | ||
| 73 | |||
| 74 | absolute_input_filename = os.path.abspath(input_filename) | ||
| 75 | args.extend(['--ro-bind', absolute_input_filename, absolute_input_filename]) | ||
| 76 | |||
| 77 | return args | ||
| 78 | |||
| 79 | |||
| 80 | # pylint: disable=bad-whitespace | ||
| 81 | def run(args: List[str], | ||
| 82 | input_filename: str, | ||
| 83 | output_filename: Optional[str] = None, | ||
| 84 | **kwargs) -> subprocess.CompletedProcess: | ||
| 85 | """Wrapper around `subprocess.run`, that uses bwrap (bubblewrap) if it | ||
| 86 | is available. | ||
| 87 | |||
| 88 | Extra supported keyword arguments: | ||
| 89 | |||
| 90 | - `input_filename`, made available read-only in the sandbox | ||
| 91 | - `output_filename`, where the file created by the sandboxed process | ||
| 92 | is copied upon successful completion; an empty temporary directory | ||
| 93 | is made visible as the parent directory of this file in the sandbox. | ||
| 94 | Optional: one valid use case is to invoke an external process | ||
| 95 | to inspect metadata present in a file. | ||
| 96 | """ | ||
| 97 | try: | ||
| 98 | bwrap_path = _get_bwrap_path() | ||
| 99 | except RuntimeError: # pragma: no cover | ||
| 100 | # bubblewrap is not installed ⇒ short-circuit | ||
| 101 | return subprocess.run(args, **kwargs) | ||
| 102 | |||
| 103 | with tempfile.TemporaryDirectory() as tempdir: | ||
| 104 | prefix_args = [bwrap_path] + \ | ||
| 105 | _get_bwrap_args(input_filename=input_filename, | ||
| 106 | output_filename=output_filename, | ||
| 107 | tempdir=tempdir) | ||
| 108 | completed_process = subprocess.run(prefix_args + args, **kwargs) | ||
| 109 | if output_filename and completed_process.returncode == 0: | ||
| 110 | shutil.copy(os.path.join(tempdir, os.path.basename(output_filename)), | ||
| 111 | output_filename) | ||
| 112 | |||
| 113 | return completed_process | ||
