记一次 pickle 对象引发的 stack overflow 异常
问题
工程中有一个类是代理类,大概实现如下:
class Wrapper(object):
def __init__(self, obj):
self._obj = obj
def __getattr__(self, item):
return getattr(self._obj, item)
多数情况下这块代码运行正常,但是在我们通过 celery 调用时,如果返回该对象构造的实例,会引发一个 RecursionError: maximum recusion depth exceeded while calling a Python object
异常,从而导致整个代码异常退出。
分析
通过 DEBUG 发现,unpickle Wrapper
实例时,首先会查看该实例是否有定义 __setattr__
方法:
// Modules/_pickle.c
static int
load_build(UnpicklerObject *self)
{
PyObject *state, *inst, *slotstate;
PyObject *setstate;
int status = 0;
_Py_IDENTIFIER(__setstate__);
inst = self->stack->data[Py_SIZE(self->stack) - 1];
// 查看实例是否有定义 __setstate__ 方法
if (_PyObject_LookupAttrId(inst, &PyId___setstate__, &setstate) < 0) {
Py_DECREF(state);
return -1;
}
if (setstate != NULL) {
PyObject *result;
/* The explicit __setstate__ is responsible for everything. */
result = _Pickle_FastCall(setstate, state);
Py_DECREF(setstate);
if (result == NULL)
return -1;
Py_DECREF(result);
return 0;
}
官方文档描述为:
object.__setstate__(state)
当解封时,如果类定义了
__setstate__()
,就会在已解封状态下调用它。此时不要求实例的 state 对象必须是 dict。没有定义此方法的话,先前封存的 state 对象必须是 dict,且该 dict 内容会在解封时赋给新实例的 dict。备注: 如果
__getstate__()
返回 False,那么在解封时就不会调用__setstate__()
方法。
此时相当于调用了 getattr(wrapper, '__setstate__')
,显然没有,但是解释器发现实例定义了 __getattr__
方法,于是转而调用了 __getattr__
方法,此时走到了这一行:
def __getattr__(self, item):
# self 没有 _obj 属性
return getattr(self._obj, item)
但此时反序列化尚未完成,实例的 __dict__
里面还没有 _obj
这个属性,于是转而又去调用实例的 __getattr__
方法,导致一直循环调用自己,引发了 RecursionError
。
修复
修复问题的方式有很多种,比如定义 __setstate__
方法等,这里我使用的方式为:
class Wrapper(object):
def __init__(self, obj):
self._obj = obj
def __getattr__(self, item):
# 通过调用 object.__getattribute__ 避免引发循环调用
o = object.__getattribute__(self, '_obj')
return getattr(o, item)
对于 object.__getattribute__
,官方文档如下:
object.__getattribute__(self, name)¶
此方法会无条件地被调用以实现对类实例属性的访问。如果类还定义了
__getattr__()
,则后者不会被调用,除非__getattribute__()
显式地调用它或是引发了AttributeError
。此方法应当返回(找到的)属性值或是引发一个AttributeError
异常。为了避免此方法中的无限递归,其实现应该总是调用具有相同名称的基类方法来访问它所需要的任何属性,例如object.__getattribute__(self, name)
。备注:此方法在作为通过特定语法或内置函数隐式地调用的结果的情况下查找特殊方法时仍可能会被跳过。参见 特殊方法查找。
引发一个 审计事件
object.__getattr__
,附带参数obj
,name
。