odoo_17.0.1/odoo/tools/_monkeypatches.py

209 lines
7.7 KiB
Python
Raw Permalink Normal View History

import ast
import ctypes
import os
import logging
import string
from shutil import copyfileobj
from types import CodeType
_logger = logging.getLogger(__name__)
from werkzeug.datastructures import FileStorage
from werkzeug.routing import Rule
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:
pass
else:
from lxml import etree
# xlrd.xlsx supports defusedxml, defusedxml's etree interface is broken
# (missing ElementTree and thus ElementTree.iter) which causes a fallback to
# Element.getiterator(), triggering a warning before 3.9 and an error from 3.9.
#
# We have defusedxml installed because zeep has a hard dep on defused and
# doesn't want to drop it (mvantellingen/python-zeep#1014).
#
# Ignore the check and set the relevant flags directly using lxml as we have a
# hard dependency on it.
xlsx.ET = etree
xlsx.ET_has_iterparse = True
xlsx.Element_has_iter = True
FileStorage.save = lambda self, dst, buffer_size=1<<20: copyfileobj(self.stream, dst, buffer_size)
Request.json_module = Response.json_module = scriptsafe
get_func_code = getattr(Rule, '_get_func_code', None)
if get_func_code:
@staticmethod
def _get_func_code(code, name):
assert isinstance(code, CodeType)
return get_func_code(code, name)
Rule._get_func_code = _get_func_code
orig_literal_eval = ast.literal_eval
def literal_eval(expr):
# limit the size of the expression to avoid segmentation faults
# the default limit is set to 100KiB
# can be overridden by setting the ODOO_LIMIT_LITEVAL_BUFFER buffer_size_environment variable
buffer_size = 102400
buffer_size_env = os.getenv("ODOO_LIMIT_LITEVAL_BUFFER")
if buffer_size_env:
if buffer_size_env.isdigit():
buffer_size = int(buffer_size_env)
else:
_logger.error("ODOO_LIMIT_LITEVAL_BUFFER has to be an integer, defaulting to 100KiB")
if isinstance(expr, str) and len(expr) > buffer_size:
raise ValueError("expression can't exceed buffer limit")
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()