• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import os
2import sys
3import tempfile
4import operator
5import functools
6import itertools
7import re
8import contextlib
9import pickle
10import textwrap
11import builtins
12
13import pkg_resources
14from distutils.errors import DistutilsError
15from pkg_resources import working_set
16
17if sys.platform.startswith('java'):
18    import org.python.modules.posix.PosixModule as _os
19else:
20    _os = sys.modules[os.name]
21try:
22    _file = file
23except NameError:
24    _file = None
25_open = open
26
27
28__all__ = [
29    "AbstractSandbox",
30    "DirectorySandbox",
31    "SandboxViolation",
32    "run_setup",
33]
34
35
36def _execfile(filename, globals, locals=None):
37    """
38    Python 3 implementation of execfile.
39    """
40    mode = 'rb'
41    with open(filename, mode) as stream:
42        script = stream.read()
43    if locals is None:
44        locals = globals
45    code = compile(script, filename, 'exec')
46    exec(code, globals, locals)
47
48
49@contextlib.contextmanager
50def save_argv(repl=None):
51    saved = sys.argv[:]
52    if repl is not None:
53        sys.argv[:] = repl
54    try:
55        yield saved
56    finally:
57        sys.argv[:] = saved
58
59
60@contextlib.contextmanager
61def save_path():
62    saved = sys.path[:]
63    try:
64        yield saved
65    finally:
66        sys.path[:] = saved
67
68
69@contextlib.contextmanager
70def override_temp(replacement):
71    """
72    Monkey-patch tempfile.tempdir with replacement, ensuring it exists
73    """
74    os.makedirs(replacement, exist_ok=True)
75
76    saved = tempfile.tempdir
77
78    tempfile.tempdir = replacement
79
80    try:
81        yield
82    finally:
83        tempfile.tempdir = saved
84
85
86@contextlib.contextmanager
87def pushd(target):
88    saved = os.getcwd()
89    os.chdir(target)
90    try:
91        yield saved
92    finally:
93        os.chdir(saved)
94
95
96class UnpickleableException(Exception):
97    """
98    An exception representing another Exception that could not be pickled.
99    """
100
101    @staticmethod
102    def dump(type, exc):
103        """
104        Always return a dumped (pickled) type and exc. If exc can't be pickled,
105        wrap it in UnpickleableException first.
106        """
107        try:
108            return pickle.dumps(type), pickle.dumps(exc)
109        except Exception:
110            # get UnpickleableException inside the sandbox
111            from setuptools.sandbox import UnpickleableException as cls
112
113            return cls.dump(cls, cls(repr(exc)))
114
115
116class ExceptionSaver:
117    """
118    A Context Manager that will save an exception, serialized, and restore it
119    later.
120    """
121
122    def __enter__(self):
123        return self
124
125    def __exit__(self, type, exc, tb):
126        if not exc:
127            return
128
129        # dump the exception
130        self._saved = UnpickleableException.dump(type, exc)
131        self._tb = tb
132
133        # suppress the exception
134        return True
135
136    def resume(self):
137        "restore and re-raise any exception"
138
139        if '_saved' not in vars(self):
140            return
141
142        type, exc = map(pickle.loads, self._saved)
143        raise exc.with_traceback(self._tb)
144
145
146@contextlib.contextmanager
147def save_modules():
148    """
149    Context in which imported modules are saved.
150
151    Translates exceptions internal to the context into the equivalent exception
152    outside the context.
153    """
154    saved = sys.modules.copy()
155    with ExceptionSaver() as saved_exc:
156        yield saved
157
158    sys.modules.update(saved)
159    # remove any modules imported since
160    del_modules = (
161        mod_name
162        for mod_name in sys.modules
163        if mod_name not in saved
164        # exclude any encodings modules. See #285
165        and not mod_name.startswith('encodings.')
166    )
167    _clear_modules(del_modules)
168
169    saved_exc.resume()
170
171
172def _clear_modules(module_names):
173    for mod_name in list(module_names):
174        del sys.modules[mod_name]
175
176
177@contextlib.contextmanager
178def save_pkg_resources_state():
179    saved = pkg_resources.__getstate__()
180    try:
181        yield saved
182    finally:
183        pkg_resources.__setstate__(saved)
184
185
186@contextlib.contextmanager
187def setup_context(setup_dir):
188    temp_dir = os.path.join(setup_dir, 'temp')
189    with save_pkg_resources_state():
190        with save_modules():
191            with save_path():
192                hide_setuptools()
193                with save_argv():
194                    with override_temp(temp_dir):
195                        with pushd(setup_dir):
196                            # ensure setuptools commands are available
197                            __import__('setuptools')
198                            yield
199
200
201_MODULES_TO_HIDE = {
202    'setuptools',
203    'distutils',
204    'pkg_resources',
205    'Cython',
206    '_distutils_hack',
207}
208
209
210def _needs_hiding(mod_name):
211    """
212    >>> _needs_hiding('setuptools')
213    True
214    >>> _needs_hiding('pkg_resources')
215    True
216    >>> _needs_hiding('setuptools_plugin')
217    False
218    >>> _needs_hiding('setuptools.__init__')
219    True
220    >>> _needs_hiding('distutils')
221    True
222    >>> _needs_hiding('os')
223    False
224    >>> _needs_hiding('Cython')
225    True
226    """
227    base_module = mod_name.split('.', 1)[0]
228    return base_module in _MODULES_TO_HIDE
229
230
231def hide_setuptools():
232    """
233    Remove references to setuptools' modules from sys.modules to allow the
234    invocation to import the most appropriate setuptools. This technique is
235    necessary to avoid issues such as #315 where setuptools upgrading itself
236    would fail to find a function declared in the metadata.
237    """
238    _distutils_hack = sys.modules.get('_distutils_hack', None)
239    if _distutils_hack is not None:
240        _distutils_hack.remove_shim()
241
242    modules = filter(_needs_hiding, sys.modules)
243    _clear_modules(modules)
244
245
246def run_setup(setup_script, args):
247    """Run a distutils setup script, sandboxed in its directory"""
248    setup_dir = os.path.abspath(os.path.dirname(setup_script))
249    with setup_context(setup_dir):
250        try:
251            sys.argv[:] = [setup_script] + list(args)
252            sys.path.insert(0, setup_dir)
253            # reset to include setup dir, w/clean callback list
254            working_set.__init__()
255            working_set.callbacks.append(lambda dist: dist.activate())
256
257            with DirectorySandbox(setup_dir):
258                ns = dict(__file__=setup_script, __name__='__main__')
259                _execfile(setup_script, ns)
260        except SystemExit as v:
261            if v.args and v.args[0]:
262                raise
263            # Normal exit, just return
264
265
266class AbstractSandbox:
267    """Wrap 'os' module and 'open()' builtin for virtualizing setup scripts"""
268
269    _active = False
270
271    def __init__(self):
272        self._attrs = [
273            name
274            for name in dir(_os)
275            if not name.startswith('_') and hasattr(self, name)
276        ]
277
278    def _copy(self, source):
279        for name in self._attrs:
280            setattr(os, name, getattr(source, name))
281
282    def __enter__(self):
283        self._copy(self)
284        if _file:
285            builtins.file = self._file
286        builtins.open = self._open
287        self._active = True
288
289    def __exit__(self, exc_type, exc_value, traceback):
290        self._active = False
291        if _file:
292            builtins.file = _file
293        builtins.open = _open
294        self._copy(_os)
295
296    def run(self, func):
297        """Run 'func' under os sandboxing"""
298        with self:
299            return func()
300
301    def _mk_dual_path_wrapper(name):
302        original = getattr(_os, name)
303
304        def wrap(self, src, dst, *args, **kw):
305            if self._active:
306                src, dst = self._remap_pair(name, src, dst, *args, **kw)
307            return original(src, dst, *args, **kw)
308
309        return wrap
310
311    for name in ["rename", "link", "symlink"]:
312        if hasattr(_os, name):
313            locals()[name] = _mk_dual_path_wrapper(name)
314
315    def _mk_single_path_wrapper(name, original=None):
316        original = original or getattr(_os, name)
317
318        def wrap(self, path, *args, **kw):
319            if self._active:
320                path = self._remap_input(name, path, *args, **kw)
321            return original(path, *args, **kw)
322
323        return wrap
324
325    if _file:
326        _file = _mk_single_path_wrapper('file', _file)
327    _open = _mk_single_path_wrapper('open', _open)
328    for name in [
329        "stat",
330        "listdir",
331        "chdir",
332        "open",
333        "chmod",
334        "chown",
335        "mkdir",
336        "remove",
337        "unlink",
338        "rmdir",
339        "utime",
340        "lchown",
341        "chroot",
342        "lstat",
343        "startfile",
344        "mkfifo",
345        "mknod",
346        "pathconf",
347        "access",
348    ]:
349        if hasattr(_os, name):
350            locals()[name] = _mk_single_path_wrapper(name)
351
352    def _mk_single_with_return(name):
353        original = getattr(_os, name)
354
355        def wrap(self, path, *args, **kw):
356            if self._active:
357                path = self._remap_input(name, path, *args, **kw)
358                return self._remap_output(name, original(path, *args, **kw))
359            return original(path, *args, **kw)
360
361        return wrap
362
363    for name in ['readlink', 'tempnam']:
364        if hasattr(_os, name):
365            locals()[name] = _mk_single_with_return(name)
366
367    def _mk_query(name):
368        original = getattr(_os, name)
369
370        def wrap(self, *args, **kw):
371            retval = original(*args, **kw)
372            if self._active:
373                return self._remap_output(name, retval)
374            return retval
375
376        return wrap
377
378    for name in ['getcwd', 'tmpnam']:
379        if hasattr(_os, name):
380            locals()[name] = _mk_query(name)
381
382    def _validate_path(self, path):
383        """Called to remap or validate any path, whether input or output"""
384        return path
385
386    def _remap_input(self, operation, path, *args, **kw):
387        """Called for path inputs"""
388        return self._validate_path(path)
389
390    def _remap_output(self, operation, path):
391        """Called for path outputs"""
392        return self._validate_path(path)
393
394    def _remap_pair(self, operation, src, dst, *args, **kw):
395        """Called for path pairs like rename, link, and symlink operations"""
396        return (
397            self._remap_input(operation + '-from', src, *args, **kw),
398            self._remap_input(operation + '-to', dst, *args, **kw),
399        )
400
401
402if hasattr(os, 'devnull'):
403    _EXCEPTIONS = [os.devnull]
404else:
405    _EXCEPTIONS = []
406
407
408class DirectorySandbox(AbstractSandbox):
409    """Restrict operations to a single subdirectory - pseudo-chroot"""
410
411    write_ops = dict.fromkeys(
412        [
413            "open",
414            "chmod",
415            "chown",
416            "mkdir",
417            "remove",
418            "unlink",
419            "rmdir",
420            "utime",
421            "lchown",
422            "chroot",
423            "mkfifo",
424            "mknod",
425            "tempnam",
426        ]
427    )
428
429    _exception_patterns = []
430    "exempt writing to paths that match the pattern"
431
432    def __init__(self, sandbox, exceptions=_EXCEPTIONS):
433        self._sandbox = os.path.normcase(os.path.realpath(sandbox))
434        self._prefix = os.path.join(self._sandbox, '')
435        self._exceptions = [
436            os.path.normcase(os.path.realpath(path)) for path in exceptions
437        ]
438        AbstractSandbox.__init__(self)
439
440    def _violation(self, operation, *args, **kw):
441        from setuptools.sandbox import SandboxViolation
442
443        raise SandboxViolation(operation, args, kw)
444
445    if _file:
446
447        def _file(self, path, mode='r', *args, **kw):
448            if mode not in ('r', 'rt', 'rb', 'rU', 'U') and not self._ok(path):
449                self._violation("file", path, mode, *args, **kw)
450            return _file(path, mode, *args, **kw)
451
452    def _open(self, path, mode='r', *args, **kw):
453        if mode not in ('r', 'rt', 'rb', 'rU', 'U') and not self._ok(path):
454            self._violation("open", path, mode, *args, **kw)
455        return _open(path, mode, *args, **kw)
456
457    def tmpnam(self):
458        self._violation("tmpnam")
459
460    def _ok(self, path):
461        active = self._active
462        try:
463            self._active = False
464            realpath = os.path.normcase(os.path.realpath(path))
465            return (
466                self._exempted(realpath)
467                or realpath == self._sandbox
468                or realpath.startswith(self._prefix)
469            )
470        finally:
471            self._active = active
472
473    def _exempted(self, filepath):
474        start_matches = (
475            filepath.startswith(exception) for exception in self._exceptions
476        )
477        pattern_matches = (
478            re.match(pattern, filepath) for pattern in self._exception_patterns
479        )
480        candidates = itertools.chain(start_matches, pattern_matches)
481        return any(candidates)
482
483    def _remap_input(self, operation, path, *args, **kw):
484        """Called for path inputs"""
485        if operation in self.write_ops and not self._ok(path):
486            self._violation(operation, os.path.realpath(path), *args, **kw)
487        return path
488
489    def _remap_pair(self, operation, src, dst, *args, **kw):
490        """Called for path pairs like rename, link, and symlink operations"""
491        if not self._ok(src) or not self._ok(dst):
492            self._violation(operation, src, dst, *args, **kw)
493        return (src, dst)
494
495    def open(self, file, flags, mode=0o777, *args, **kw):
496        """Called for low-level os.open()"""
497        if flags & WRITE_FLAGS and not self._ok(file):
498            self._violation("os.open", file, flags, mode, *args, **kw)
499        return _os.open(file, flags, mode, *args, **kw)
500
501
502WRITE_FLAGS = functools.reduce(
503    operator.or_,
504    [
505        getattr(_os, a, 0)
506        for a in "O_WRONLY O_RDWR O_APPEND O_CREAT O_TRUNC O_TEMPORARY".split()
507    ],
508)
509
510
511class SandboxViolation(DistutilsError):
512    """A setup script attempted to modify the filesystem outside the sandbox"""
513
514    tmpl = textwrap.dedent(
515        """
516        SandboxViolation: {cmd}{args!r} {kwargs}
517
518        The package setup script has attempted to modify files on your system
519        that are not within the EasyInstall build area, and has been aborted.
520
521        This package cannot be safely installed by EasyInstall, and may not
522        support alternate installation locations even if you run its setup
523        script by hand.  Please inform the package's author and the EasyInstall
524        maintainers to find out if a fix or workaround is available.
525        """
526    ).lstrip()
527
528    def __str__(self):
529        cmd, args, kwargs = self.args
530        return self.tmpl.format(**locals())
531