Bug 1259351 - Properly sandbox functions that are decorated with templates. r=nalexander
authorMike Hommey <mh+mozilla@glandium.org>
Sun, 27 Mar 2016 10:50:27 +0900
changeset 290988 178b2c7228b6527ce26c306c07dde4ed538215b6
parent 290987 352568360a612b1cca16f2083a981276d3c81502
child 290989 f01a178847233cf15e9cddaaa97a5db6fc371cc0
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
bugs1259351
milestone48.0a1
Bug 1259351 - Properly sandbox functions that are decorated with templates. r=nalexander
python/mozbuild/mozbuild/configure/__init__.py
python/mozbuild/mozbuild/test/configure/data/decorators.configure
python/mozbuild/mozbuild/test/configure/test_configure.py
--- a/python/mozbuild/mozbuild/configure/__init__.py
+++ b/python/mozbuild/mozbuild/configure/__init__.py
@@ -229,17 +229,17 @@ class ConfigureSandbox(dict):
 
     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
                 value not in self._templates and
-                not issubclass(value, Exception)):
+                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):
         if isinstance(arg, DependsFunction):
             assert arg in self._depends
@@ -388,18 +388,42 @@ 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'
         )
-        self._templates.add(template)
-        return template
+
+        # 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):
+            @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):
+                    return wrap_template(ret)
+                return ret
+            return wrapper
+
+        wrapper = wrap_template(template)
+        self._templates.add(wrapper)
+        return wrapper
 
     def advanced_impl(self, func):
         '''Implementation of @advanced.
         This function gives the decorated function access to the complete set
         of builtins, allowing the import keyword as an expected side effect.
         '''
         func, glob = self._prepare_function(func)
         glob.update(__builtins__=__builtins__)
new file mode 100644
--- /dev/null
+++ b/python/mozbuild/mozbuild/test/configure/data/decorators.configure
@@ -0,0 +1,44 @@
+# -*- 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
+def simple_decorator(func):
+    return func
+
+@template
+def wrapper_decorator(func):
+    def wrapper(*args, **kwargs):
+        return func(*args, **kwargs)
+    return wrapper
+
+@template
+def function_decorator(*args, **kwargs):
+    # We could return wrapper_decorator from above here, but then we wouldn't
+    # know if this works as expected because wrapper_decorator itself was
+    # modified or because the right thing happened here.
+    def wrapper_decorator(func):
+        def wrapper(*args, **kwargs):
+            return func(*args, **kwargs)
+        return wrapper
+    return wrapper_decorator
+
+@depends('--help')
+@simple_decorator
+def foo(help):
+    global FOO
+    FOO = 1
+
+@depends('--help')
+@wrapper_decorator
+def bar(help):
+    global BAR
+    BAR = 1
+
+@depends('--help')
+@function_decorator('a', 'b', 'c')
+def qux(help):
+    global QUX
+    QUX = 1
--- a/python/mozbuild/mozbuild/test/configure/test_configure.py
+++ b/python/mozbuild/mozbuild/test/configure/test_configure.py
@@ -254,16 +254,27 @@ class TestConfigure(unittest.TestCase):
         self.assertIn('TEMPLATE_VALUE_2', config)
         self.assertEquals(config['TEMPLATE_VALUE_2'], 21)
 
     def test_template_advanced(self):
         config = self.get_config(['--enable-advanced-template'])
         self.assertIn('PLATFORM', config)
         self.assertEquals(config['PLATFORM'], sys.platform)
 
+    def test_decorators(self):
+        config = {}
+        out = StringIO()
+        sandbox = ConfigureSandbox(config, {}, [], out, out)
+
+        sandbox.exec_file(mozpath.join(test_data_path, 'decorators.configure'))
+
+        self.assertNotIn('FOO', sandbox)
+        self.assertNotIn('BAR', sandbox)
+        self.assertNotIn('QUX', sandbox)
+
     def test_set_config(self):
         def get_config(*args):
             return self.get_config(*args, configure='set_config.configure')
 
         config, out = self.get_result(['--help'],
                                       configure='set_config.configure')
         self.assertEquals(config, {})