Revert "[ADD] tools: patch C types, e.g. str.format"
This reverts commit 66832f0ba0.
This commit is contained in:
@@ -3,8 +3,6 @@
|
||||
|
||||
import ast
|
||||
|
||||
from markupsafe import Markup
|
||||
|
||||
from odoo import Command
|
||||
from odoo.tests.common import TransactionCase, BaseCase
|
||||
from odoo.tools import mute_logger
|
||||
@@ -62,68 +60,6 @@ class TestSafeEval(BaseCase):
|
||||
with self.assertRaises(NameError):
|
||||
safe_eval("self.__name__", {'self': self}, mode="exec")
|
||||
|
||||
def test_06_safe_eval_format(self):
|
||||
# string.format
|
||||
self.assertEqual(safe_eval("'__{0}__'.format('Foo')"), '__Foo__')
|
||||
self.assertEqual(safe_eval("'{0.__self__}'.format(abs)"), '{0.__self__}')
|
||||
self.assertEqual(safe_eval("'{0.f_globals}'.format(abs)"), '{0.f_globals}')
|
||||
|
||||
# string.format_map
|
||||
self.assertEqual(safe_eval("'__{foo}__'.format_map({'foo': 'Foo'})"), '__Foo__')
|
||||
self.assertEqual(safe_eval("'{foo.__self__}'.format_map({'foo': abs})"), '{foo.__self__}')
|
||||
self.assertEqual(safe_eval("'{foo.f_globals}'.format_map({'foo': abs})"), '{foo.f_globals}')
|
||||
|
||||
# Evaluation context for Markup asserts
|
||||
c = {"Markup": Markup}
|
||||
|
||||
# Markup.format
|
||||
self.assertEqual(safe_eval("Markup('__{0}__').format('Foo')", c), Markup('__Foo__'))
|
||||
with self.assertRaisesRegex(ValueError, 'Access to forbidden name'):
|
||||
safe_eval("Markup('{0.__self__}').format(abs)", c)
|
||||
with self.assertRaisesRegex(ValueError, 'Access to forbidden name'):
|
||||
safe_eval("Markup('{0.f_globals}').format(abs)", c)
|
||||
|
||||
# Markup.format_map
|
||||
self.assertEqual(safe_eval("Markup('__{foo}__').format_map({'foo': 'Foo'})", c), Markup('__Foo__'))
|
||||
self.assertEqual(safe_eval("Markup('{foo.__self__}').format_map({'foo': abs})", c), Markup('{foo.__self__}'))
|
||||
self.assertEqual(safe_eval("Markup('{foo.f_globals}').format_map({'foo': abs})", c), Markup('{foo.f_globals}'))
|
||||
|
||||
def test_07_safe_eval_attribute_error_obj(self):
|
||||
locals_dict = {}
|
||||
try:
|
||||
safe_eval("""
|
||||
try:
|
||||
dict.foo
|
||||
except Exception as e:
|
||||
action = {'args': e.args, 'obj': e.obj, 'name': e.name}
|
||||
""", locals_dict=locals_dict, mode="exec", nocopy=True)
|
||||
except ValueError as e:
|
||||
# AttributeError.name, AttributeError.obj added in Python 3.10
|
||||
# https://github.com/python/cpython/commit/37494b441aced0362d7edd2956ab3ea7801e60c8
|
||||
self.assertIn("'AttributeError' object has no attribute 'obj'", e.args[0])
|
||||
else:
|
||||
exception = locals_dict.get('action')
|
||||
self.assertEqual(exception['args'], ("type object 'dict' has no attribute 'foo'",))
|
||||
self.assertIsNone(exception['name'])
|
||||
self.assertIsNone(exception['obj'])
|
||||
|
||||
attribute_error = None
|
||||
try:
|
||||
raise AttributeError('Foo', name='Bar', obj=[])
|
||||
except TypeError as e:
|
||||
# AttributeError does not take keyword arguments before Python 3.10
|
||||
# https://github.com/python/cpython/commit/37494b441aced0362d7edd2956ab3ea7801e60c8
|
||||
# Error can be either, according to the Python version:
|
||||
# - AttributeError does not take keyword arguments
|
||||
# - AttributeError() takes no keyword arguments
|
||||
self.assertIn("keyword arguments", e.args[0])
|
||||
except AttributeError as e:
|
||||
attribute_error = e
|
||||
if attribute_error:
|
||||
self.assertEqual(attribute_error.args, ('Foo',))
|
||||
self.assertEqual(attribute_error.name, 'Bar')
|
||||
self.assertIsNone(attribute_error.obj)
|
||||
|
||||
|
||||
class TestParentStore(TransactionCase):
|
||||
""" Verify that parent_store computation is done right """
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import ast
|
||||
import ctypes
|
||||
import os
|
||||
import logging
|
||||
import string
|
||||
from shutil import copyfileobj
|
||||
from types import CodeType
|
||||
|
||||
@@ -14,8 +12,6 @@ from werkzeug.wrappers import Request, Response
|
||||
|
||||
from .json import scriptsafe
|
||||
|
||||
from odoo.tools.safe_eval import _UNSAFE_ATTRIBUTES
|
||||
|
||||
try:
|
||||
from xlrd import xlsx
|
||||
except ImportError:
|
||||
@@ -69,140 +65,3 @@ def literal_eval(expr):
|
||||
return orig_literal_eval(expr)
|
||||
|
||||
ast.literal_eval = literal_eval
|
||||
|
||||
|
||||
def _is_safe_expr(expr):
|
||||
return '__' not in expr and not any(att_name in expr for att_name in _UNSAFE_ATTRIBUTES)
|
||||
|
||||
|
||||
origin_formatter_get_field = string.Formatter.get_field
|
||||
|
||||
|
||||
def get_field(self, field_name, args, kwargs):
|
||||
# Monkey-patch `get_field` to raise in case of access to a forbidden name
|
||||
# Ref: https://github.com/python/cpython/blob/812245ecce2d8344c3748228047bab456816180a/Lib/string.py#L267
|
||||
if not _is_safe_expr(field_name):
|
||||
raise NameError('Access to forbidden name %r' % (field_name))
|
||||
return origin_formatter_get_field(self, field_name, args, kwargs)
|
||||
|
||||
|
||||
string.Formatter.get_field = get_field
|
||||
|
||||
|
||||
#
|
||||
# Monkey-Patch C types
|
||||
#
|
||||
|
||||
# PyTypeObject is not in Python ABI, so we map it to a custom ctypes Struct
|
||||
class PyTypeObject(ctypes.Structure):
|
||||
# Ref: https://docs.python.org/3/c-api/typeobj.html
|
||||
_fields_ = [
|
||||
# cover PyObject variable header https://docs.python.org/3/c-api/typeobj.html#pyobject-slots
|
||||
# + 15 first PyTypeObject slots: https://docs.python.org/3/c-api/typeobj.html#pytypeobject-slots
|
||||
('_', 18 * ctypes.c_void_p),
|
||||
# https://docs.python.org/3/c-api/typeobj.html#c.PyTypeObject.tp_getattro
|
||||
('tp_getattro', ctypes.CFUNCTYPE(ctypes.py_object, ctypes.py_object, ctypes.py_object)),
|
||||
('_', 14 * ctypes.c_void_p), # cover 14 slots, not needed so far
|
||||
# https://docs.python.org/3/c-api/typeobj.html#c.PyTypeObject.tp_dict
|
||||
('tp_dict', ctypes.py_object),
|
||||
('_', 3 * ctypes.c_void_p), # cover 3 slots, not needed so far
|
||||
# https://docs.python.org/3/c-api/typeobj.html#c.PyTypeObject.tp_init
|
||||
# /!\ last param mapped to c_void_p because it's nullable and PyObject doesn't support it
|
||||
('tp_init', ctypes.CFUNCTYPE(ctypes.c_int, ctypes.py_object, ctypes.py_object, ctypes.c_void_p)),
|
||||
]
|
||||
_slot_mapping = {
|
||||
# python_method: slot_name
|
||||
'__init__': 'tp_init',
|
||||
}
|
||||
|
||||
|
||||
def patch_c_type(cls, attr, value):
|
||||
# obtain the address of the C type - don't assume id(cls) is guaranteed to hold it
|
||||
cls_pt = ctypes.py_object(cls)
|
||||
cls_addr = ctypes.POINTER(ctypes.c_void_p)(cls_pt)[0]
|
||||
# cast into our custom PyTypeObject struct
|
||||
obj = PyTypeObject.from_address(cls_addr)
|
||||
|
||||
# CASE 1. Slot functions: replace slot function pointer
|
||||
if attr.startswith("__") and attr.endswith("__"):
|
||||
slot_func = PyTypeObject._slot_mapping[attr]
|
||||
c_func_type = dict(PyTypeObject._fields_)[slot_func]
|
||||
c_func = c_func_type(value) # C callback pointer for our patch function
|
||||
|
||||
# incref: store a ref in the type's __dict__
|
||||
cls_dict = getattr(obj, "tp_dict")
|
||||
cls_dict.setdefault('__patch_refs__', []).append(c_func)
|
||||
|
||||
# apply patch function
|
||||
setattr(obj, slot_func, c_func)
|
||||
|
||||
# CASE 2. Other methods: replace the method in the __dict__ of the type
|
||||
else:
|
||||
cls_dict = getattr(obj, "tp_dict")
|
||||
origin = cls_dict.get(attr)
|
||||
if origin:
|
||||
value.__name__ = origin.__name__
|
||||
value.__qualname__ = origin.__qualname__
|
||||
|
||||
# apply patch function
|
||||
cls_dict[attr] = value
|
||||
|
||||
# clear internal caches: https://docs.python.org/3/c-api/type.html#c.PyType_Modified
|
||||
ctypes.pythonapi.PyType_Modified(cls_pt)
|
||||
|
||||
|
||||
# Monkey-patch AttributeError to suppress assignation of `.obj`
|
||||
if hasattr(AttributeError, 'obj'):
|
||||
# AttributeError.name, AttributeError.obj added in Python 3.10
|
||||
# https://github.com/python/cpython/commit/37494b441aced0362d7edd2956ab3ea7801e60c8
|
||||
def __init__(self, args, kwargs):
|
||||
if kwargs:
|
||||
# py_object isn't NULLABLE, so we manually handle the NULL case from a void pointer
|
||||
kwargs = ctypes.cast(kwargs, ctypes.py_object).value
|
||||
else:
|
||||
kwargs = {}
|
||||
|
||||
# emulate super __init__, we don't want to call it, it's cheaper
|
||||
# it's here: https://github.com/python/cpython/blob/d0524caed0f3b77f271640460d0dff1a4c784087/Objects/exceptions.c#L1404
|
||||
self.name = kwargs.get('name')
|
||||
|
||||
# assign .obj immediately so set_attribute_error_context() won't do it
|
||||
# cfr https://github.com/python/cpython/commit/3b3be05a164da43f201e35b6dafbc840993a4d18
|
||||
self.obj = None # pretend it was not really assigned (None != NULL pointer)
|
||||
return 0
|
||||
|
||||
patch_c_type(AttributeError, "__init__", __init__)
|
||||
|
||||
|
||||
def _mkpatch_str_format():
|
||||
# Monkey-patch str.format to forbid usage of dunder attributes in replacement fields,
|
||||
# keeping all refs in the closure
|
||||
formatter = string.Formatter() # thread-safe parsing, can be shared
|
||||
origin_format = str.format
|
||||
origin_format_map = str.format_map
|
||||
|
||||
def _safe_format_fields(s):
|
||||
# First quick pass with strstr(), matching both `{0.__foo__}` and `__{0}__`
|
||||
if not _is_safe_expr(s):
|
||||
# Second pass with a real parsing for "format fields" to avoid blocking `__{0}__`. cfr PEP-3101.
|
||||
field_exprs = tuple(field_expr for _lit, field_expr, _fmt, _conv in formatter.parse(s) if field_expr)
|
||||
for expr in field_exprs:
|
||||
if not _is_safe_expr(expr):
|
||||
return False
|
||||
return True
|
||||
|
||||
def format(*args, **kwargs):
|
||||
if not _safe_format_fields(args[0]):
|
||||
return args[0] # pretend we forgot to format
|
||||
return origin_format(*args, **kwargs)
|
||||
|
||||
def format_map(*args, **kwargs):
|
||||
if not _safe_format_fields(args[0]):
|
||||
return args[0] # pretend we forgot to format
|
||||
return origin_format_map(*args, **kwargs)
|
||||
|
||||
patch_c_type(str, "format", format)
|
||||
patch_c_type(str, "format_map", format_map)
|
||||
|
||||
|
||||
_mkpatch_str_format()
|
||||
|
||||
Reference in New Issue
Block a user