[ADD] tools: patch C types, e.g. str.format
Monkey-patching C types is not straightforward. It relies on changing the attributes or methods in memory at the right address with the exact right size. This requires the greatest caution. A simple mistake can mess up the memory used by the Python interpreter, and for instance lead to `SegmentationFault` exceptions or unforeseen behaviors. However, being able to patch C type is a very powerful tool. With great power comes great responsibility. `patch_c_type` is implemented with the greateast caution. The Python C-API documentation has been thoroughly followed and understood. In addition, this patch has been battle tested in real conditions. In the end, this allows to patch unwanted behaviors from types implemented in C. Co-authored-by: Denis Ledoux <dle@odoo.com> Co-authored-by: Christophe Simonis <chs@odoo.com> Co-authored-by: Mathieu Walravens <wama@odoo.com>
This commit is contained in:
co-authored by
Denis Ledoux
Christophe Simonis
Mathieu Walravens
parent
515ea2afb5
commit
66832f0ba0
@@ -1,6 +1,8 @@
|
||||
import ast
|
||||
import ctypes
|
||||
import os
|
||||
import logging
|
||||
import string
|
||||
from shutil import copyfileobj
|
||||
from types import CodeType
|
||||
|
||||
@@ -12,6 +14,8 @@ 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:
|
||||
@@ -65,3 +69,140 @@ 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