]> git.lizzy.rs Git - plan9front.git/blob - sys/lib/python/rexec.py
glenda's default profile: remove /n/other mount, done by /lib/namespace
[plan9front.git] / sys / lib / python / rexec.py
1 """Restricted execution facilities.
2
3 The class RExec exports methods r_exec(), r_eval(), r_execfile(), and
4 r_import(), which correspond roughly to the built-in operations
5 exec, eval(), execfile() and import, but executing the code in an
6 environment that only exposes those built-in operations that are
7 deemed safe.  To this end, a modest collection of 'fake' modules is
8 created which mimics the standard modules by the same names.  It is a
9 policy decision which built-in modules and operations are made
10 available; this module provides a reasonable default, but derived
11 classes can change the policies e.g. by overriding or extending class
12 variables like ok_builtin_modules or methods like make_sys().
13
14 XXX To do:
15 - r_open should allow writing tmp dir
16 - r_exec etc. with explicit globals/locals? (Use rexec("exec ... in ...")?)
17
18 """
19
20
21 import sys
22 import __builtin__
23 import os
24 import ihooks
25 import imp
26
27 __all__ = ["RExec"]
28
29 class FileBase:
30
31     ok_file_methods = ('fileno', 'flush', 'isatty', 'read', 'readline',
32             'readlines', 'seek', 'tell', 'write', 'writelines', 'xreadlines',
33             '__iter__')
34
35
36 class FileWrapper(FileBase):
37
38     # XXX This is just like a Bastion -- should use that!
39
40     def __init__(self, f):
41         for m in self.ok_file_methods:
42             if not hasattr(self, m) and hasattr(f, m):
43                 setattr(self, m, getattr(f, m))
44
45     def close(self):
46         self.flush()
47
48
49 TEMPLATE = """
50 def %s(self, *args):
51         return getattr(self.mod, self.name).%s(*args)
52 """
53
54 class FileDelegate(FileBase):
55
56     def __init__(self, mod, name):
57         self.mod = mod
58         self.name = name
59
60     for m in FileBase.ok_file_methods + ('close',):
61         exec TEMPLATE % (m, m)
62
63
64 class RHooks(ihooks.Hooks):
65
66     def __init__(self, *args):
67         # Hacks to support both old and new interfaces:
68         # old interface was RHooks(rexec[, verbose])
69         # new interface is RHooks([verbose])
70         verbose = 0
71         rexec = None
72         if args and type(args[-1]) == type(0):
73             verbose = args[-1]
74             args = args[:-1]
75         if args and hasattr(args[0], '__class__'):
76             rexec = args[0]
77             args = args[1:]
78         if args:
79             raise TypeError, "too many arguments"
80         ihooks.Hooks.__init__(self, verbose)
81         self.rexec = rexec
82
83     def set_rexec(self, rexec):
84         # Called by RExec instance to complete initialization
85         self.rexec = rexec
86
87     def get_suffixes(self):
88         return self.rexec.get_suffixes()
89
90     def is_builtin(self, name):
91         return self.rexec.is_builtin(name)
92
93     def init_builtin(self, name):
94         m = __import__(name)
95         return self.rexec.copy_except(m, ())
96
97     def init_frozen(self, name): raise SystemError, "don't use this"
98     def load_source(self, *args): raise SystemError, "don't use this"
99     def load_compiled(self, *args): raise SystemError, "don't use this"
100     def load_package(self, *args): raise SystemError, "don't use this"
101
102     def load_dynamic(self, name, filename, file):
103         return self.rexec.load_dynamic(name, filename, file)
104
105     def add_module(self, name):
106         return self.rexec.add_module(name)
107
108     def modules_dict(self):
109         return self.rexec.modules
110
111     def default_path(self):
112         return self.rexec.modules['sys'].path
113
114
115 # XXX Backwards compatibility
116 RModuleLoader = ihooks.FancyModuleLoader
117 RModuleImporter = ihooks.ModuleImporter
118
119
120 class RExec(ihooks._Verbose):
121     """Basic restricted execution framework.
122
123     Code executed in this restricted environment will only have access to
124     modules and functions that are deemed safe; you can subclass RExec to
125     add or remove capabilities as desired.
126
127     The RExec class can prevent code from performing unsafe operations like
128     reading or writing disk files, or using TCP/IP sockets.  However, it does
129     not protect against code using extremely large amounts of memory or
130     processor time.
131
132     """
133
134     ok_path = tuple(sys.path)           # That's a policy decision
135
136     ok_builtin_modules = ('audioop', 'array', 'binascii',
137                           'cmath', 'errno', 'imageop',
138                           'marshal', 'math', 'md5', 'operator',
139                           'parser', 'select',
140                           'sha', '_sre', 'strop', 'struct', 'time',
141                           '_weakref')
142
143     ok_posix_names = ('error', 'fstat', 'listdir', 'lstat', 'readlink',
144                       'stat', 'times', 'uname', 'getpid', 'getppid',
145                       'getcwd', 'getuid', 'getgid', 'geteuid', 'getegid')
146
147     ok_sys_names = ('byteorder', 'copyright', 'exit', 'getdefaultencoding',
148                     'getrefcount', 'hexversion', 'maxint', 'maxunicode',
149                     'platform', 'ps1', 'ps2', 'version', 'version_info')
150
151     nok_builtin_names = ('open', 'file', 'reload', '__import__')
152
153     ok_file_types = (imp.C_EXTENSION, imp.PY_SOURCE)
154
155     def __init__(self, hooks = None, verbose = 0):
156         """Returns an instance of the RExec class.
157
158         The hooks parameter is an instance of the RHooks class or a subclass
159         of it.  If it is omitted or None, the default RHooks class is
160         instantiated.
161
162         Whenever the RExec module searches for a module (even a built-in one)
163         or reads a module's code, it doesn't actually go out to the file
164         system itself.  Rather, it calls methods of an RHooks instance that
165         was passed to or created by its constructor.  (Actually, the RExec
166         object doesn't make these calls --- they are made by a module loader
167         object that's part of the RExec object.  This allows another level of
168         flexibility, which can be useful when changing the mechanics of
169         import within the restricted environment.)
170
171         By providing an alternate RHooks object, we can control the file
172         system accesses made to import a module, without changing the
173         actual algorithm that controls the order in which those accesses are
174         made.  For instance, we could substitute an RHooks object that
175         passes all filesystem requests to a file server elsewhere, via some
176         RPC mechanism such as ILU.  Grail's applet loader uses this to support
177         importing applets from a URL for a directory.
178
179         If the verbose parameter is true, additional debugging output may be
180         sent to standard output.
181
182         """
183
184         raise RuntimeError, "This code is not secure in Python 2.2 and 2.3"
185
186         ihooks._Verbose.__init__(self, verbose)
187         # XXX There's a circular reference here:
188         self.hooks = hooks or RHooks(verbose)
189         self.hooks.set_rexec(self)
190         self.modules = {}
191         self.ok_dynamic_modules = self.ok_builtin_modules
192         list = []
193         for mname in self.ok_builtin_modules:
194             if mname in sys.builtin_module_names:
195                 list.append(mname)
196         self.ok_builtin_modules = tuple(list)
197         self.set_trusted_path()
198         self.make_builtin()
199         self.make_initial_modules()
200         # make_sys must be last because it adds the already created
201         # modules to its builtin_module_names
202         self.make_sys()
203         self.loader = RModuleLoader(self.hooks, verbose)
204         self.importer = RModuleImporter(self.loader, verbose)
205
206     def set_trusted_path(self):
207         # Set the path from which dynamic modules may be loaded.
208         # Those dynamic modules must also occur in ok_builtin_modules
209         self.trusted_path = filter(os.path.isabs, sys.path)
210
211     def load_dynamic(self, name, filename, file):
212         if name not in self.ok_dynamic_modules:
213             raise ImportError, "untrusted dynamic module: %s" % name
214         if name in sys.modules:
215             src = sys.modules[name]
216         else:
217             src = imp.load_dynamic(name, filename, file)
218         dst = self.copy_except(src, [])
219         return dst
220
221     def make_initial_modules(self):
222         self.make_main()
223         self.make_osname()
224
225     # Helpers for RHooks
226
227     def get_suffixes(self):
228         return [item   # (suff, mode, type)
229                 for item in imp.get_suffixes()
230                 if item[2] in self.ok_file_types]
231
232     def is_builtin(self, mname):
233         return mname in self.ok_builtin_modules
234
235     # The make_* methods create specific built-in modules
236
237     def make_builtin(self):
238         m = self.copy_except(__builtin__, self.nok_builtin_names)
239         m.__import__ = self.r_import
240         m.reload = self.r_reload
241         m.open = m.file = self.r_open
242
243     def make_main(self):
244         m = self.add_module('__main__')
245
246     def make_osname(self):
247         osname = os.name
248         src = __import__(osname)
249         dst = self.copy_only(src, self.ok_posix_names)
250         dst.environ = e = {}
251         for key, value in os.environ.items():
252             e[key] = value
253
254     def make_sys(self):
255         m = self.copy_only(sys, self.ok_sys_names)
256         m.modules = self.modules
257         m.argv = ['RESTRICTED']
258         m.path = map(None, self.ok_path)
259         m.exc_info = self.r_exc_info
260         m = self.modules['sys']
261         l = self.modules.keys() + list(self.ok_builtin_modules)
262         l.sort()
263         m.builtin_module_names = tuple(l)
264
265     # The copy_* methods copy existing modules with some changes
266
267     def copy_except(self, src, exceptions):
268         dst = self.copy_none(src)
269         for name in dir(src):
270             setattr(dst, name, getattr(src, name))
271         for name in exceptions:
272             try:
273                 delattr(dst, name)
274             except AttributeError:
275                 pass
276         return dst
277
278     def copy_only(self, src, names):
279         dst = self.copy_none(src)
280         for name in names:
281             try:
282                 value = getattr(src, name)
283             except AttributeError:
284                 continue
285             setattr(dst, name, value)
286         return dst
287
288     def copy_none(self, src):
289         m = self.add_module(src.__name__)
290         m.__doc__ = src.__doc__
291         return m
292
293     # Add a module -- return an existing module or create one
294
295     def add_module(self, mname):
296         m = self.modules.get(mname)
297         if m is None:
298             self.modules[mname] = m = self.hooks.new_module(mname)
299         m.__builtins__ = self.modules['__builtin__']
300         return m
301
302     # The r* methods are public interfaces
303
304     def r_exec(self, code):
305         """Execute code within a restricted environment.
306
307         The code parameter must either be a string containing one or more
308         lines of Python code, or a compiled code object, which will be
309         executed in the restricted environment's __main__ module.
310
311         """
312         m = self.add_module('__main__')
313         exec code in m.__dict__
314
315     def r_eval(self, code):
316         """Evaluate code within a restricted environment.
317
318         The code parameter must either be a string containing a Python
319         expression, or a compiled code object, which will be evaluated in
320         the restricted environment's __main__ module.  The value of the
321         expression or code object will be returned.
322
323         """
324         m = self.add_module('__main__')
325         return eval(code, m.__dict__)
326
327     def r_execfile(self, file):
328         """Execute the Python code in the file in the restricted
329         environment's __main__ module.
330
331         """
332         m = self.add_module('__main__')
333         execfile(file, m.__dict__)
334
335     def r_import(self, mname, globals={}, locals={}, fromlist=[]):
336         """Import a module, raising an ImportError exception if the module
337         is considered unsafe.
338
339         This method is implicitly called by code executing in the
340         restricted environment.  Overriding this method in a subclass is
341         used to change the policies enforced by a restricted environment.
342
343         """
344         return self.importer.import_module(mname, globals, locals, fromlist)
345
346     def r_reload(self, m):
347         """Reload the module object, re-parsing and re-initializing it.
348
349         This method is implicitly called by code executing in the
350         restricted environment.  Overriding this method in a subclass is
351         used to change the policies enforced by a restricted environment.
352
353         """
354         return self.importer.reload(m)
355
356     def r_unload(self, m):
357         """Unload the module.
358
359         Removes it from the restricted environment's sys.modules dictionary.
360
361         This method is implicitly called by code executing in the
362         restricted environment.  Overriding this method in a subclass is
363         used to change the policies enforced by a restricted environment.
364
365         """
366         return self.importer.unload(m)
367
368     # The s_* methods are similar but also swap std{in,out,err}
369
370     def make_delegate_files(self):
371         s = self.modules['sys']
372         self.delegate_stdin = FileDelegate(s, 'stdin')
373         self.delegate_stdout = FileDelegate(s, 'stdout')
374         self.delegate_stderr = FileDelegate(s, 'stderr')
375         self.restricted_stdin = FileWrapper(sys.stdin)
376         self.restricted_stdout = FileWrapper(sys.stdout)
377         self.restricted_stderr = FileWrapper(sys.stderr)
378
379     def set_files(self):
380         if not hasattr(self, 'save_stdin'):
381             self.save_files()
382         if not hasattr(self, 'delegate_stdin'):
383             self.make_delegate_files()
384         s = self.modules['sys']
385         s.stdin = self.restricted_stdin
386         s.stdout = self.restricted_stdout
387         s.stderr = self.restricted_stderr
388         sys.stdin = self.delegate_stdin
389         sys.stdout = self.delegate_stdout
390         sys.stderr = self.delegate_stderr
391
392     def reset_files(self):
393         self.restore_files()
394         s = self.modules['sys']
395         self.restricted_stdin = s.stdin
396         self.restricted_stdout = s.stdout
397         self.restricted_stderr = s.stderr
398
399
400     def save_files(self):
401         self.save_stdin = sys.stdin
402         self.save_stdout = sys.stdout
403         self.save_stderr = sys.stderr
404
405     def restore_files(self):
406         sys.stdin = self.save_stdin
407         sys.stdout = self.save_stdout
408         sys.stderr = self.save_stderr
409
410     def s_apply(self, func, args=(), kw={}):
411         self.save_files()
412         try:
413             self.set_files()
414             r = func(*args, **kw)
415         finally:
416             self.restore_files()
417         return r
418
419     def s_exec(self, *args):
420         """Execute code within a restricted environment.
421
422         Similar to the r_exec() method, but the code will be granted access
423         to restricted versions of the standard I/O streams sys.stdin,
424         sys.stderr, and sys.stdout.
425
426         The code parameter must either be a string containing one or more
427         lines of Python code, or a compiled code object, which will be
428         executed in the restricted environment's __main__ module.
429
430         """
431         return self.s_apply(self.r_exec, args)
432
433     def s_eval(self, *args):
434         """Evaluate code within a restricted environment.
435
436         Similar to the r_eval() method, but the code will be granted access
437         to restricted versions of the standard I/O streams sys.stdin,
438         sys.stderr, and sys.stdout.
439
440         The code parameter must either be a string containing a Python
441         expression, or a compiled code object, which will be evaluated in
442         the restricted environment's __main__ module.  The value of the
443         expression or code object will be returned.
444
445         """
446         return self.s_apply(self.r_eval, args)
447
448     def s_execfile(self, *args):
449         """Execute the Python code in the file in the restricted
450         environment's __main__ module.
451
452         Similar to the r_execfile() method, but the code will be granted
453         access to restricted versions of the standard I/O streams sys.stdin,
454         sys.stderr, and sys.stdout.
455
456         """
457         return self.s_apply(self.r_execfile, args)
458
459     def s_import(self, *args):
460         """Import a module, raising an ImportError exception if the module
461         is considered unsafe.
462
463         This method is implicitly called by code executing in the
464         restricted environment.  Overriding this method in a subclass is
465         used to change the policies enforced by a restricted environment.
466
467         Similar to the r_import() method, but has access to restricted
468         versions of the standard I/O streams sys.stdin, sys.stderr, and
469         sys.stdout.
470
471         """
472         return self.s_apply(self.r_import, args)
473
474     def s_reload(self, *args):
475         """Reload the module object, re-parsing and re-initializing it.
476
477         This method is implicitly called by code executing in the
478         restricted environment.  Overriding this method in a subclass is
479         used to change the policies enforced by a restricted environment.
480
481         Similar to the r_reload() method, but has access to restricted
482         versions of the standard I/O streams sys.stdin, sys.stderr, and
483         sys.stdout.
484
485         """
486         return self.s_apply(self.r_reload, args)
487
488     def s_unload(self, *args):
489         """Unload the module.
490
491         Removes it from the restricted environment's sys.modules dictionary.
492
493         This method is implicitly called by code executing in the
494         restricted environment.  Overriding this method in a subclass is
495         used to change the policies enforced by a restricted environment.
496
497         Similar to the r_unload() method, but has access to restricted
498         versions of the standard I/O streams sys.stdin, sys.stderr, and
499         sys.stdout.
500
501         """
502         return self.s_apply(self.r_unload, args)
503
504     # Restricted open(...)
505
506     def r_open(self, file, mode='r', buf=-1):
507         """Method called when open() is called in the restricted environment.
508
509         The arguments are identical to those of the open() function, and a
510         file object (or a class instance compatible with file objects)
511         should be returned.  RExec's default behaviour is allow opening
512         any file for reading, but forbidding any attempt to write a file.
513
514         This method is implicitly called by code executing in the
515         restricted environment.  Overriding this method in a subclass is
516         used to change the policies enforced by a restricted environment.
517
518         """
519         mode = str(mode)
520         if mode not in ('r', 'rb'):
521             raise IOError, "can't open files for writing in restricted mode"
522         return open(file, mode, buf)
523
524     # Restricted version of sys.exc_info()
525
526     def r_exc_info(self):
527         ty, va, tr = sys.exc_info()
528         tr = None
529         return ty, va, tr
530
531
532 def test():
533     import getopt, traceback
534     opts, args = getopt.getopt(sys.argv[1:], 'vt:')
535     verbose = 0
536     trusted = []
537     for o, a in opts:
538         if o == '-v':
539             verbose = verbose+1
540         if o == '-t':
541             trusted.append(a)
542     r = RExec(verbose=verbose)
543     if trusted:
544         r.ok_builtin_modules = r.ok_builtin_modules + tuple(trusted)
545     if args:
546         r.modules['sys'].argv = args
547         r.modules['sys'].path.insert(0, os.path.dirname(args[0]))
548     else:
549         r.modules['sys'].path.insert(0, "")
550     fp = sys.stdin
551     if args and args[0] != '-':
552         try:
553             fp = open(args[0])
554         except IOError, msg:
555             print "%s: can't open file %r" % (sys.argv[0], args[0])
556             return 1
557     if fp.isatty():
558         try:
559             import readline
560         except ImportError:
561             pass
562         import code
563         class RestrictedConsole(code.InteractiveConsole):
564             def runcode(self, co):
565                 self.locals['__builtins__'] = r.modules['__builtin__']
566                 r.s_apply(code.InteractiveConsole.runcode, (self, co))
567         try:
568             RestrictedConsole(r.modules['__main__'].__dict__).interact()
569         except SystemExit, n:
570             return n
571     else:
572         text = fp.read()
573         fp.close()
574         c = compile(text, fp.name, 'exec')
575         try:
576             r.s_exec(c)
577         except SystemExit, n:
578             return n
579         except:
580             traceback.print_exc()
581             return 1
582
583
584 if __name__ == '__main__':
585     sys.exit(test())