Bug 1260066 - Don't allow to use sandbox primitives from anywhere but global scope and templates. r=nalexander
authorMike Hommey <mh+mozilla@glandium.org>
Mon, 28 Mar 2016 07:29:08 +0900
changeset 290996 63338edce3ba60f6668973b60bf832560e78d7c4
parent 290995 7c1b33d35a5b863984f29f6e91963110e38e2cdc
child 290997 735da799e3bbb98c087339f21599571c48ce484f
push id19656
push usergwagner@mozilla.com
push dateMon, 04 Apr 2016 13:43:23 +0000
treeherderb2g-inbound@e99061fde28a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersnalexander
bugs1260066, 1257823
milestone48.0a1
Bug 1260066 - Don't allow to use sandbox primitives from anywhere but global scope and templates. r=nalexander The initial goal of templates was to provide a way to write shorter constructs for some generic tasks during configure. But the limitations of the sandbox and the properties of templates made them used for more general functions. Consequently, this led to templates having to be available from anywhere, which, in turn, led to difficult to introspect constructs. With bug 1257823, we've made almost everything use set_config and similar functions from the global scope, but we don't enforce that those primitives are only used at the global scope. This change does that: it enforces that primitives are only used at the global scope. Or in templates. Now, since templates were used for other purposes than generic uses of other primitives, we now allow non-template functions to be declared. Those can be used everywhere, but don't have access to the sandbox primitives.
build/moz.configure/init.configure
build/moz.configure/old.configure
build/moz.configure/util.configure
python/mozbuild/mozbuild/configure/__init__.py
python/mozbuild/mozbuild/test/configure/data/included.configure
--- a/build/moz.configure/init.configure
+++ b/build/moz.configure/init.configure
@@ -350,17 +350,16 @@ def shell(mozillabuild):
 # Host and target systems
 # ==============================================================
 option('--host', nargs=1, help='Define the system type performing the build')
 
 option('--target', nargs=1,
        help='Define the system type where the resulting executables will be '
             'used')
 
-@template
 def split_triplet(triplet):
     # The standard triplet is defined as
     #   CPU_TYPE-MANUFACTURER-OPERATING_SYSTEM
     # There is also a quartet form:
     #   CPU_TYPE-MANUFACTURER-KERNEL-OPERATING_SYSTEM
     # But we can consider the "KERNEL-OPERATING_SYSTEM" as one.
     cpu, manufacturer, os = triplet.split('-', 2)
 
@@ -436,17 +435,16 @@ def split_triplet(triplet):
         cpu=canonical_cpu,
         kernel=canonical_kernel,
         os=canonical_os,
         raw_cpu=cpu,
         raw_os=os,
     )
 
 
-@template
 @imports('subprocess')
 def config_sub(shell, triplet):
     config_sub = os.path.join(os.path.dirname(__file__), '..',
                               'autoconf', 'config.sub')
     return subprocess.check_output([shell, config_sub, triplet]).strip()
 
 
 @depends('--host', shell)
--- a/build/moz.configure/old.configure
+++ b/build/moz.configure/old.configure
@@ -1,15 +1,14 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
-@template
 @imports('codecs')
 @imports('sys')
 def encoded_open(path, mode):
     encoding = 'mbcs' if sys.platform == 'win32' else 'utf-8'
     return codecs.open(path, mode, encoding)
 
 
 option(env='AUTOCONF', nargs=1, help='Path to autoconf 2.13')
@@ -438,24 +437,24 @@ def old_configure(prepare_configure, ext
     # debugging.
     os.remove('config.data')
     return raw_config
 
 
 # set_config is only available in the global namespace, not directly in
 # @depends functions, but we do need to enumerate the result of
 # old_configure, so we cheat.
-@template
+@imports('__sandbox__')
 def set_old_configure_config(name, value):
-    set_config(name, value)
+    __sandbox__.set_config_impl(name, value)
 
 # Same as set_old_configure_config, but for set_define.
-@template
+@imports('__sandbox__')
 def set_old_configure_define(name, value):
-    set_define(name, value)
+    __sandbox__.set_define_impl(name, value)
 
 
 @depends(old_configure)
 @imports('types')
 def post_old_configure(raw_config):
     for k, v in raw_config['substs']:
         set_old_configure_config(
             k[1:-1], v[1:-1] if isinstance(v, types.StringTypes) else v)
--- a/build/moz.configure/util.configure
+++ b/build/moz.configure/util.configure
@@ -1,70 +1,63 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
-@template
 @imports('sys')
 def die(*args):
     'Print an error and terminate configure.'
     log.error(*args)
     sys.exit(1)
 
 
-@template
 @imports(_from='mozbuild.configure', _import='ConfigureError')
 def configure_error(message):
     '''Raise a programming error and terminate configure.
     Primarily for use in moz.configure templates to sanity check
     their inputs from moz.configure usage.'''
     raise ConfigureError(message)
 
 
-@template
 @imports('os')
 def is_absolute_or_relative(path):
     if os.altsep and os.altsep in path:
         return True
     return os.sep in path
 
 
-@template
 @imports(_import='mozpack.path', _as='mozpath')
 def normsep(path):
     return mozpath.normsep(path)
 
 
-@template
 # This unlocks the sandbox. Do not copy blindly.
 @imports(_import='__builtin__', _as='__builtins__')
 def find_program(file):
     if is_absolute_or_relative(file):
         return os.path.abspath(file) if os.path.isfile(file) else None
     # We can't use @imports here because it imports at declaration time,
     # and the declaration of find_program happens before we ensure the
     # which module is available in sys.path somehow.
     from which import which, WhichError
     try:
         return normsep(which(file))
     except WhichError:
         return None
 
 
-@template
 def unique_list(l):
     result = []
     for i in l:
         if l not in result:
             result.append(i)
     return result
 
-@template
 @imports(_from='mozbuild.configure.util', _import='Version', _as='_Version')
 def Version(v):
     'A version number that can be compared usefully.'
     return _Version(v)
 
 # Denotes a deprecated option. Combines option() and @depends:
 # @deprecated_option('--option')
 # def option(value):
@@ -85,17 +78,16 @@ def deprecated_option(*args, **kwargs):
             if value.origin != 'default':
                 return func(value)
         return deprecated
 
     return decorator
 
 
 # from mozbuild.util import ReadOnlyNamespace as namespace
-@template
 @imports(_from='mozbuild.util', _import='ReadOnlyNamespace')
 def namespace(**kwargs):
     return ReadOnlyNamespace(**kwargs)
 
 
 # Some @depends function return namespaces, and one could want to use one
 # specific attribute from such a namespace as a "value" given to functions
 # such as `set_config`. But those functions do not take immediate values.
@@ -105,23 +97,20 @@ def namespace(**kwargs):
 #   def option(value)
 #       return namespace(foo=value)
 #   set_config('FOO', delayed_getattr(option, 'foo')
 @template
 def delayed_getattr(func, key):
     @depends(func)
     @imports(_from='__builtin__', _import='getattr')
     def result(value):
-        try:
-            return getattr(value, key)
-        except AttributeError:
-            # The @depends function we're being passed may have returned
-            # None, or an object that simply doesn't have the wanted key.
-            # In that case, just return None.
-            return None
+        # The @depends function we're being passed may have returned
+        # None, or an object that simply doesn't have the wanted key.
+        # In that case, just return None.
+        return getattr(value, key, None)
     return result
 
 
 # Like @depends, but the decorated function is only called if one of the
 # arguments it would be called with has a positive value (bool(value) is True)
 @template
 def depends_if(*args):
     def decorator(func):
--- a/python/mozbuild/mozbuild/configure/__init__.py
+++ b/python/mozbuild/mozbuild/configure/__init__.py
@@ -233,17 +233,20 @@ class ConfigureSandbox(dict):
 
         return super(ConfigureSandbox, self).__getitem__(key)
 
     def __setitem__(self, key, value):
         if (key in self.BUILTINS or key == '__builtins__' or
                 hasattr(self, '%s_impl' % key)):
             raise KeyError('Cannot reassign builtins')
 
-        if (not isinstance(value, DependsFunction) and
+        if inspect.isfunction(value) and value not in self._templates:
+            value, _ = self._prepare_function(value)
+
+        elif (not isinstance(value, DependsFunction) and
                 value not in self._templates and
                 not (inspect.isclass(value) and issubclass(value, Exception))):
             raise KeyError('Cannot assign `%s` because it is neither a '
                            '@depends nor a @template' % key)
 
         return super(ConfigureSandbox, self).__setitem__(key, value)
 
     def _resolve(self, arg, need_help_dependency=True):
@@ -394,35 +397,43 @@ class ConfigureSandbox(dict):
         Templates allow to simplify repetitive constructs, or to implement
         helper decorators and somesuch.
         '''
         template, glob = self._prepare_function(func)
         glob.update(
             (k[:-len('_impl')], getattr(self, k))
             for k in dir(self) if k.endswith('_impl') and k != 'template_impl'
         )
+        glob.update((k, v) for k, v in self.iteritems() if k not in glob)
 
         # Any function argument to the template must be prepared to be sandboxed.
         # If the template itself returns a function (in which case, it's very
         # likely a decorator), that function must be prepared to be sandboxed as
         # well.
         def wrap_template(template):
+            isfunction = inspect.isfunction
+
+            def maybe_prepare_function(obj):
+                if isfunction(obj):
+                    func, _ = self._prepare_function(obj)
+                    return func
+                return obj
+
+            # The following function may end up being prepared to be sandboxed,
+            # so it mustn't depend on anything from the global scope in this
+            # file. It can however depend on variables from the closure, thus
+            # maybe_prepare_function and isfunction are declared above to be
+            # available there.
             @wraps(template)
             def wrapper(*args, **kwargs):
-                def maybe_prepare_function(obj):
-                    if inspect.isfunction(obj):
-                        func, _ = self._prepare_function(obj)
-                        return func
-                    return obj
-
                 args = [maybe_prepare_function(arg) for arg in args]
                 kwargs = {k: maybe_prepare_function(v)
                           for k, v in kwargs.iteritems()}
                 ret = template(*args, **kwargs)
-                if inspect.isfunction(ret):
+                if isfunction(ret):
                     return wrap_template(ret)
                 return ret
             return wrapper
 
         wrapper = wrap_template(template)
         self._templates.add(wrapper)
         return wrapper
 
@@ -602,17 +613,21 @@ class ConfigureSandbox(dict):
         '''Alter the given function global namespace with the common ground
         for @depends, and @template.
         '''
         if not inspect.isfunction(func):
             raise TypeError("Unexpected type: '%s'" % type(func))
         if func in self._prepared_functions:
             return func, func.func_globals
 
-        glob = SandboxedGlobal(func.func_globals)
+        glob = SandboxedGlobal(
+            (k, v) for k, v in func.func_globals.iteritems()
+            if (inspect.isfunction(v) and v not in self._templates) or (
+                inspect.isclass(v) and issubclass(v, Exception))
+        )
         glob.update(
             __builtins__=self.BUILTINS,
             __file__=self._paths[-1] if self._paths else '',
             os=self.OS,
             log=self.log_impl,
         )
         self._apply_imports(func, glob)
         func = wraps(func)(types.FunctionType(
--- a/python/mozbuild/mozbuild/test/configure/data/included.configure
+++ b/python/mozbuild/mozbuild/test/configure/data/included.configure
@@ -12,22 +12,20 @@ def check_compiler_flag(flag):
         if value:
             return [flag]
     set_config('CFLAGS', check)
     return check
 
 
 check_compiler_flag('-Werror=foobar')
 
-# A template that doesn't return functions can be used in @depends functions.
-@template
+# Normal functions can be used in @depends functions.
 def fortytwo():
     return 42
 
-@template
 def twentyone():
     yield 21
 
 @depends(is_gcc)
 def check(value):
     if value:
         return fortytwo()
 
@@ -36,18 +34,17 @@ set_config('TEMPLATE_VALUE', check)
 @depends(is_gcc)
 def check(value):
     if value:
         for val in twentyone():
             return val
 
 set_config('TEMPLATE_VALUE_2', check)
 
-# Templates can use @imports too to import modules.
-@template
+# Normal functions can use @imports too to import modules.
 @imports('sys')
 def platform():
     return sys.platform
 
 option('--enable-imports-in-template', help='Imports in template')
 @depends('--enable-imports-in-template')
 def check(value):
     if value: