【问题标题】:Who should call PyErr_Fetch?谁应该调用 PyErr_Fetch?
【发布时间】:2018-09-23 17:07:28
【问题描述】:

如果设置了错误指示符,Python 的 C API 中的许多函数使用起来不安全。特别是,PyFloat_AsDouble 和类似的函数是不明确,因为它们没有为指示错误保留返回值:如果它们成功(但恰好返回用于错误的值),调用的客户端如果错误指示器已经设置,PyErr_Occurred 会认为他们失败了。 (请注意,PyIter_Next 或多或少保证会发生这种情况。)更一般地说,任何可能失败的函数都会覆盖错误指示符,如果发生这种情况,这可能是可取的,也可能是不可取的。

不幸的是,使用错误指示符集调用此类函数的可能性并非完全不可能:对错误的常见反应是Py_DECREF局部变量,并且(除非所有对象的类型可能是(间接)由它释放的已知)可以执行任意代码。 (这是一个很好的例子,说明清理代码可能会失败。)解释器在此类析构函数中捕获异常引发,但它不能防止异常泄漏进入他们

任一端,我们可以使用PyErr_FetchPyErr_Restore 来防止这些问题。调用一个模棱两可的函数,它们可以可靠地确定它是否成功;放在Py_DECREF 周围,它们首先防止在执行任何易受攻击的代码期间设置错误指示器。 (它们甚至可以用于可能会失败的直接调用的清理代码,以便允许选择传播哪个异常。在这种情况下,将它放在哪里是毫无疑问的:清理代码无论如何都不能在多个异常之间进行选择.)

任何一种放置选择都会显着增加代码复杂性和执行时间:有很多对模棱两可的函数的调用,并且在错误处理路径上有很多Py_DECREFs。虽然防御性编程的原则建议在两个地方都使用它,但更好的代码将来自(仔细编程)通用约定(以涵盖正在执行的任意代码)。

C 本身有这样一个约定:errno 必须由任意代码的调用者保存,即使(如 Python 析构函数中的抑制异常)该代码不应将 errno 设置为任何内容。主要原因是它可以被许多成功的库调用重置(但永远不会为0)(让它们在内部处理错误),进一步缩小在errno保持时可以安全执行的操作集一些重要的价值。 (这也可以防止PyErr_Occurred 报告预先存在的错误时出现的问题:C 程序员必须在调用不明确的函数之前将errno 设置为 0。)另一个原因是“调用一些任意代码而不报告错误”不是在大多数 C 程序中都是常见的操作,因此为它增加其他代码的负担是没有意义的。

是否有这样的约定(即使 CPython 本身存在不遵循它的错误代码)?如果做不到这一点,是否有技术原因来指导选择一个建立?或者这可能是一个基于“任意”解读的工程问题:CPython 是否应该在处理析构函数异常时保存和恢复错误指示符本身?

【问题讨论】:

    标签: python error-handling cpython python-internals


    【解决方案1】:

    如果您的清理工作只是一堆 Py_DECREF,则不需要调用 PyErr_FetchPy_DECREF 旨在安全地调用异常集。如果Py_DECREF 中的代码需要对异常集执行一些不安全的操作,它将负责保存和恢复异常状态。 (如果您的清理工作不仅仅涉及Py_DECREF,您可能需要自己处理。)

    例如tp_finalize,最有可能调用任意Python代码的对象销毁步骤之一is explicitly responsible for saving and restoring an active exception

    tp_finalize 不应该改变当前的异常状态; 因此,编写非平凡终结器的推荐方法是:

    static void
    local_finalize(PyObject *self)
    {
        PyObject *error_type, *error_value, *error_traceback;
    
        /* Save the current exception, if any. */
        PyErr_Fetch(&error_type, &error_value, &error_traceback);
    
        /* ... */
    
        /* Restore the saved exception. */
        PyErr_Restore(error_type, error_value, error_traceback);
    }
    

    对于用Python编写的__del__方法,可以在slot_tp_finalize看到相关处理:

    /* Save the current exception, if any. */
    PyErr_Fetch(&error_type, &error_value, &error_traceback);
    
    /* Execute __del__ method, if any. */
    del = lookup_maybe_method(self, &PyId___del__, &unbound);
    if (del != NULL) {
        res = call_unbound_noarg(unbound, del, self);
        if (res == NULL)
            PyErr_WriteUnraisable(del);
        else
            Py_DECREF(res);
        Py_DECREF(del);
    }
    
    /* Restore the saved exception. */
    PyErr_Restore(error_type, error_value, error_traceback);
    

    弱引用系统还 takes responsibility 用于在调用弱引用回调之前保存异常状态:

    if (*list != NULL) {
        PyWeakReference *current = *list;
        Py_ssize_t count = _PyWeakref_GetWeakrefCount(current);
        PyObject *err_type, *err_value, *err_tb;
    
        PyErr_Fetch(&err_type, &err_value, &err_tb);
        if (count == 1) {
            PyObject *callback = current->wr_callback;
    
            current->wr_callback = NULL;
            clear_weakref(current);
            if (callback != NULL) {
                if (((PyObject *)current)->ob_refcnt > 0)
                    handle_callback(current, callback);
                Py_DECREF(callback);
            }
        }
        else {
            ...
    

    所以在设置了异常时调用Py_DECREF 是很可怕的,你考虑它是件好事,但只要对象销毁代码行为正常,应该没问题。


    那么,如果您需要做更多的清理工作而不仅仅是清除您的参考文献呢?在这种情况下,如果您的清理对异常集不安全,您可能应该在完成后调用PyErr_FetchPyErr_Restore 异常状态。如果在清理时引发另一个异常,您可以链接它(C 级别的awkward but possible),或者使用PyErr_WriteUnraisable 向stderr 转储一个简短的警告,然后通过PyErr_Clear-ing 来抑制新异常或者通过PyErr_Restore-ing 原始异常状态。

    【讨论】:

    • 嗯——我知道我已经看到通过 Py_DECREF 设置了一个异常,但那是 3 年前的事了,显然是某些 C 类型的错误(而不是 __del__ 或 @987654346 @)。如果我知道在这种情况下的一般谨慎程度,我就会知道在有机会时识别出哪种 C 类型行为不端!
    • 您的回答暗示但并没有完全说明任何处理错误其他的例程都有责任保存/隐藏Py_DECREFPyErr_WriteUnraisable如有必要,并恢复(或链接)异常。您可以添加它以确保完整性吗?
    • @DavisHerring:答案已扩展。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-12-05
    • 2017-03-25
    • 1970-01-01
    • 1970-01-01
    • 2019-08-25
    • 2011-07-06
    相关资源
    最近更新 更多