101 lines
3.4 KiB
Python
101 lines
3.4 KiB
Python
|
"""
|
||
|
Wrapper around a subset of the subprocess module,
|
||
|
that uses bwrap (bubblewrap) when it is available.
|
||
|
|
||
|
Instead of importing subprocess, other modules should use this as follows:
|
||
|
|
||
|
from . import subprocess
|
||
|
"""
|
||
|
|
||
|
import os
|
||
|
import shutil
|
||
|
import subprocess
|
||
|
import tempfile
|
||
|
from typing import List, Optional
|
||
|
|
||
|
|
||
|
__all__ = ['PIPE', 'run', 'CalledProcessError']
|
||
|
PIPE = subprocess.PIPE
|
||
|
CalledProcessError = subprocess.CalledProcessError
|
||
|
|
||
|
|
||
|
def _get_bwrap_path() -> str:
|
||
|
bwrap_path = '/usr/bin/bwrap'
|
||
|
if os.path.isfile(bwrap_path):
|
||
|
if os.access(bwrap_path, os.X_OK):
|
||
|
return bwrap_path
|
||
|
|
||
|
raise RuntimeError("Unable to find bwrap") # pragma: no cover
|
||
|
|
||
|
|
||
|
# pylint: disable=bad-whitespace
|
||
|
def _get_bwrap_args(tempdir: str,
|
||
|
input_filename: str,
|
||
|
output_filename: Optional[str] = None) -> List[str]:
|
||
|
cwd = os.getcwd()
|
||
|
|
||
|
# XXX: use --ro-bind-try once all supported platforms
|
||
|
# have a bubblewrap recent enough to support it.
|
||
|
ro_bind_dirs = ['/usr', '/lib', '/lib64', '/bin', '/sbin', cwd]
|
||
|
ro_bind_args = []
|
||
|
for bind_dir in ro_bind_dirs:
|
||
|
if os.path.isdir(bind_dir): # pragma: no cover
|
||
|
ro_bind_args.extend(['--ro-bind', bind_dir, bind_dir])
|
||
|
|
||
|
args = ro_bind_args + \
|
||
|
['--dev', '/dev',
|
||
|
'--chdir', cwd,
|
||
|
'--unshare-all',
|
||
|
'--new-session',
|
||
|
# XXX: enable --die-with-parent once all supported platforms have
|
||
|
# a bubblewrap recent enough to support it.
|
||
|
# '--die-with-parent',
|
||
|
]
|
||
|
|
||
|
if output_filename:
|
||
|
# Mount an empty temporary directory where the sandboxed
|
||
|
# process will create its output file
|
||
|
output_dirname = os.path.dirname(os.path.abspath(output_filename))
|
||
|
args.extend(['--bind', tempdir, output_dirname])
|
||
|
|
||
|
absolute_input_filename = os.path.abspath(input_filename)
|
||
|
args.extend(['--ro-bind', absolute_input_filename, absolute_input_filename])
|
||
|
|
||
|
return args
|
||
|
|
||
|
|
||
|
# pylint: disable=bad-whitespace
|
||
|
def run(args: List[str],
|
||
|
input_filename: str,
|
||
|
output_filename: Optional[str] = None,
|
||
|
**kwargs) -> subprocess.CompletedProcess:
|
||
|
"""Wrapper around `subprocess.run`, that uses bwrap (bubblewrap) if it
|
||
|
is available.
|
||
|
|
||
|
Extra supported keyword arguments:
|
||
|
|
||
|
- `input_filename`, made available read-only in the sandbox
|
||
|
- `output_filename`, where the file created by the sandboxed process
|
||
|
is copied upon successful completion; an empty temporary directory
|
||
|
is made visible as the parent directory of this file in the sandbox.
|
||
|
Optional: one valid use case is to invoke an external process
|
||
|
to inspect metadata present in a file.
|
||
|
"""
|
||
|
try:
|
||
|
bwrap_path = _get_bwrap_path()
|
||
|
except RuntimeError: # pragma: no cover
|
||
|
# bubblewrap is not installed ⇒ short-circuit
|
||
|
return subprocess.run(args, **kwargs)
|
||
|
|
||
|
with tempfile.TemporaryDirectory() as tempdir:
|
||
|
prefix_args = [bwrap_path] + \
|
||
|
_get_bwrap_args(input_filename=input_filename,
|
||
|
output_filename=output_filename,
|
||
|
tempdir=tempdir)
|
||
|
completed_process = subprocess.run(prefix_args + args, **kwargs)
|
||
|
if output_filename and completed_process.returncode == 0:
|
||
|
shutil.copy(os.path.join(tempdir, os.path.basename(output_filename)),
|
||
|
output_filename)
|
||
|
|
||
|
return completed_process
|