sandbox.py 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. import os
  2. import sys
  3. import tempfile
  4. import operator
  5. import functools
  6. import itertools
  7. import re
  8. import pkg_resources
  9. if os.name == "java":
  10. import org.python.modules.posix.PosixModule as _os
  11. else:
  12. _os = sys.modules[os.name]
  13. try:
  14. _file = file
  15. except NameError:
  16. _file = None
  17. _open = open
  18. from distutils.errors import DistutilsError
  19. from pkg_resources import working_set
  20. from setuptools.compat import builtins, execfile
  21. __all__ = [
  22. "AbstractSandbox", "DirectorySandbox", "SandboxViolation", "run_setup",
  23. ]
  24. def run_setup(setup_script, args):
  25. """Run a distutils setup script, sandboxed in its directory"""
  26. old_dir = os.getcwd()
  27. save_argv = sys.argv[:]
  28. save_path = sys.path[:]
  29. setup_dir = os.path.abspath(os.path.dirname(setup_script))
  30. temp_dir = os.path.join(setup_dir,'temp')
  31. if not os.path.isdir(temp_dir): os.makedirs(temp_dir)
  32. save_tmp = tempfile.tempdir
  33. save_modules = sys.modules.copy()
  34. pr_state = pkg_resources.__getstate__()
  35. try:
  36. tempfile.tempdir = temp_dir
  37. os.chdir(setup_dir)
  38. try:
  39. sys.argv[:] = [setup_script]+list(args)
  40. sys.path.insert(0, setup_dir)
  41. # reset to include setup dir, w/clean callback list
  42. working_set.__init__()
  43. working_set.callbacks.append(lambda dist:dist.activate())
  44. DirectorySandbox(setup_dir).run(
  45. lambda: execfile(
  46. "setup.py",
  47. {'__file__':setup_script, '__name__':'__main__'}
  48. )
  49. )
  50. except SystemExit:
  51. v = sys.exc_info()[1]
  52. if v.args and v.args[0]:
  53. raise
  54. # Normal exit, just return
  55. finally:
  56. pkg_resources.__setstate__(pr_state)
  57. sys.modules.update(save_modules)
  58. # remove any modules imported within the sandbox
  59. del_modules = [
  60. mod_name for mod_name in sys.modules
  61. if mod_name not in save_modules
  62. # exclude any encodings modules. See #285
  63. and not mod_name.startswith('encodings.')
  64. ]
  65. list(map(sys.modules.__delitem__, del_modules))
  66. os.chdir(old_dir)
  67. sys.path[:] = save_path
  68. sys.argv[:] = save_argv
  69. tempfile.tempdir = save_tmp
  70. class AbstractSandbox:
  71. """Wrap 'os' module and 'open()' builtin for virtualizing setup scripts"""
  72. _active = False
  73. def __init__(self):
  74. self._attrs = [
  75. name for name in dir(_os)
  76. if not name.startswith('_') and hasattr(self,name)
  77. ]
  78. def _copy(self, source):
  79. for name in self._attrs:
  80. setattr(os, name, getattr(source,name))
  81. def run(self, func):
  82. """Run 'func' under os sandboxing"""
  83. try:
  84. self._copy(self)
  85. if _file:
  86. builtins.file = self._file
  87. builtins.open = self._open
  88. self._active = True
  89. return func()
  90. finally:
  91. self._active = False
  92. if _file:
  93. builtins.file = _file
  94. builtins.open = _open
  95. self._copy(_os)
  96. def _mk_dual_path_wrapper(name):
  97. original = getattr(_os,name)
  98. def wrap(self,src,dst,*args,**kw):
  99. if self._active:
  100. src,dst = self._remap_pair(name,src,dst,*args,**kw)
  101. return original(src,dst,*args,**kw)
  102. return wrap
  103. for name in ["rename", "link", "symlink"]:
  104. if hasattr(_os,name): locals()[name] = _mk_dual_path_wrapper(name)
  105. def _mk_single_path_wrapper(name, original=None):
  106. original = original or getattr(_os,name)
  107. def wrap(self,path,*args,**kw):
  108. if self._active:
  109. path = self._remap_input(name,path,*args,**kw)
  110. return original(path,*args,**kw)
  111. return wrap
  112. if _file:
  113. _file = _mk_single_path_wrapper('file', _file)
  114. _open = _mk_single_path_wrapper('open', _open)
  115. for name in [
  116. "stat", "listdir", "chdir", "open", "chmod", "chown", "mkdir",
  117. "remove", "unlink", "rmdir", "utime", "lchown", "chroot", "lstat",
  118. "startfile", "mkfifo", "mknod", "pathconf", "access"
  119. ]:
  120. if hasattr(_os,name): locals()[name] = _mk_single_path_wrapper(name)
  121. def _mk_single_with_return(name):
  122. original = getattr(_os,name)
  123. def wrap(self,path,*args,**kw):
  124. if self._active:
  125. path = self._remap_input(name,path,*args,**kw)
  126. return self._remap_output(name, original(path,*args,**kw))
  127. return original(path,*args,**kw)
  128. return wrap
  129. for name in ['readlink', 'tempnam']:
  130. if hasattr(_os,name): locals()[name] = _mk_single_with_return(name)
  131. def _mk_query(name):
  132. original = getattr(_os,name)
  133. def wrap(self,*args,**kw):
  134. retval = original(*args,**kw)
  135. if self._active:
  136. return self._remap_output(name, retval)
  137. return retval
  138. return wrap
  139. for name in ['getcwd', 'tmpnam']:
  140. if hasattr(_os,name): locals()[name] = _mk_query(name)
  141. def _validate_path(self,path):
  142. """Called to remap or validate any path, whether input or output"""
  143. return path
  144. def _remap_input(self,operation,path,*args,**kw):
  145. """Called for path inputs"""
  146. return self._validate_path(path)
  147. def _remap_output(self,operation,path):
  148. """Called for path outputs"""
  149. return self._validate_path(path)
  150. def _remap_pair(self,operation,src,dst,*args,**kw):
  151. """Called for path pairs like rename, link, and symlink operations"""
  152. return (
  153. self._remap_input(operation+'-from',src,*args,**kw),
  154. self._remap_input(operation+'-to',dst,*args,**kw)
  155. )
  156. if hasattr(os, 'devnull'):
  157. _EXCEPTIONS = [os.devnull,]
  158. else:
  159. _EXCEPTIONS = []
  160. try:
  161. from win32com.client.gencache import GetGeneratePath
  162. _EXCEPTIONS.append(GetGeneratePath())
  163. del GetGeneratePath
  164. except ImportError:
  165. # it appears pywin32 is not installed, so no need to exclude.
  166. pass
  167. class DirectorySandbox(AbstractSandbox):
  168. """Restrict operations to a single subdirectory - pseudo-chroot"""
  169. write_ops = dict.fromkeys([
  170. "open", "chmod", "chown", "mkdir", "remove", "unlink", "rmdir",
  171. "utime", "lchown", "chroot", "mkfifo", "mknod", "tempnam",
  172. ])
  173. _exception_patterns = [
  174. # Allow lib2to3 to attempt to save a pickled grammar object (#121)
  175. '.*lib2to3.*\.pickle$',
  176. ]
  177. "exempt writing to paths that match the pattern"
  178. def __init__(self, sandbox, exceptions=_EXCEPTIONS):
  179. self._sandbox = os.path.normcase(os.path.realpath(sandbox))
  180. self._prefix = os.path.join(self._sandbox,'')
  181. self._exceptions = [
  182. os.path.normcase(os.path.realpath(path))
  183. for path in exceptions
  184. ]
  185. AbstractSandbox.__init__(self)
  186. def _violation(self, operation, *args, **kw):
  187. raise SandboxViolation(operation, args, kw)
  188. if _file:
  189. def _file(self, path, mode='r', *args, **kw):
  190. if mode not in ('r', 'rt', 'rb', 'rU', 'U') and not self._ok(path):
  191. self._violation("file", path, mode, *args, **kw)
  192. return _file(path,mode,*args,**kw)
  193. def _open(self, path, mode='r', *args, **kw):
  194. if mode not in ('r', 'rt', 'rb', 'rU', 'U') and not self._ok(path):
  195. self._violation("open", path, mode, *args, **kw)
  196. return _open(path,mode,*args,**kw)
  197. def tmpnam(self):
  198. self._violation("tmpnam")
  199. def _ok(self, path):
  200. active = self._active
  201. try:
  202. self._active = False
  203. realpath = os.path.normcase(os.path.realpath(path))
  204. return (
  205. self._exempted(realpath)
  206. or realpath == self._sandbox
  207. or realpath.startswith(self._prefix)
  208. )
  209. finally:
  210. self._active = active
  211. def _exempted(self, filepath):
  212. start_matches = (
  213. filepath.startswith(exception)
  214. for exception in self._exceptions
  215. )
  216. pattern_matches = (
  217. re.match(pattern, filepath)
  218. for pattern in self._exception_patterns
  219. )
  220. candidates = itertools.chain(start_matches, pattern_matches)
  221. return any(candidates)
  222. def _remap_input(self, operation, path, *args, **kw):
  223. """Called for path inputs"""
  224. if operation in self.write_ops and not self._ok(path):
  225. self._violation(operation, os.path.realpath(path), *args, **kw)
  226. return path
  227. def _remap_pair(self, operation, src, dst, *args, **kw):
  228. """Called for path pairs like rename, link, and symlink operations"""
  229. if not self._ok(src) or not self._ok(dst):
  230. self._violation(operation, src, dst, *args, **kw)
  231. return (src,dst)
  232. def open(self, file, flags, mode=0o777, *args, **kw):
  233. """Called for low-level os.open()"""
  234. if flags & WRITE_FLAGS and not self._ok(file):
  235. self._violation("os.open", file, flags, mode, *args, **kw)
  236. return _os.open(file,flags,mode, *args, **kw)
  237. WRITE_FLAGS = functools.reduce(
  238. operator.or_, [getattr(_os, a, 0) for a in
  239. "O_WRONLY O_RDWR O_APPEND O_CREAT O_TRUNC O_TEMPORARY".split()]
  240. )
  241. class SandboxViolation(DistutilsError):
  242. """A setup script attempted to modify the filesystem outside the sandbox"""
  243. def __str__(self):
  244. return """SandboxViolation: %s%r %s
  245. The package setup script has attempted to modify files on your system
  246. that are not within the EasyInstall build area, and has been aborted.
  247. This package cannot be safely installed by EasyInstall, and may not
  248. support alternate installation locations even if you run its setup
  249. script by hand. Please inform the package's author and the EasyInstall
  250. maintainers to find out if a fix or workaround is available.""" % self.args
  251. #