Skip to content

Fix proxy attribute deletion handling - #351

Closed
dynapx wants to merge 6 commits into
GrahamDumpleton:developfrom
dynapx:fix-proxy-attribute-delete
Closed

dynapx wants to merge 6 commits into
GrahamDumpleton:developfrom
dynapx:fix-proxy-attribute-delete

Conversation

@dynapx

@dynapx dynapx commented Sep 8, 2026

Copy link
Copy Markdown

Summary

When deleting the doc or module attribute from an ObjectProxy, CPython passes NULL as the new value to the C-level setter. This is the standard CPython convention for attribute deletion.

The proxy correctly forwards the deletion to the wrapped object, but then attempted to pass the same NULL pointer to PyDict_SetItemString(). This API requires a valid PyObject pointer, so deleting either attribute could cause the Python interpreter to crash.

Problem

The affected code paths are WraptObjectProxy_set_doc and WraptObjectProxy_set_module.

For normal assignment, the setter receives a valid PyObject pointer and stores the value in the proxy's internal dictionary. For deletion, however, value is NULL. The old implementation still called PyDict_SetItemString() with this NULL value.

This behavior could be triggered by normal Python code such as deleting proxy.doc or proxy.module. It was therefore necessary to handle deletion separately from assignment.

Changes

When value is NULL, both setters now remove the corresponding cached dictionary entry using PyDict_DelItemString().

The implementation also treats an already missing cache entry as a successful deletion, while preserving and returning any unexpected dictionary error.

When value is non-NULL, the existing assignment behavior remains unchanged and the value is stored using PyDict_SetItemString().

The following functions were updated:

WraptObjectProxy_set_doc

WraptObjectProxy_set_module

@dynapx

dynapx commented Sep 9, 2026

Copy link
Copy Markdown
Author

I pushed an additional commit, b04070a, addressing another NULL-safety issue found by static analysis.

Both WraptFunctionWrapperBase_call and WraptBoundFunctionWrapper_call checked:

self->enabled != Py_None

If self->enabled was NULL during an exceptional or cleared object state, this condition could still be true and NULL could be passed to PyCallable_Check() or PyObject_Not().

The checks now guard against NULL explicitly:

self->enabled && self->enabled != Py_None

The existing enabled-related tests pass locally.

pull Bot pushed a commit to sysfce2/python-wrapt that referenced this pull request Sep 20, 2026
…object.

Deleting __module__ or __doc__ on a proxy crashed the interpreter with the
C extension, as the attribute setter was invoked with a NULL value for the
deletion and passed that on to PyDict_SetItemString() when updating the
copy held in the proxy's own dict. The pure Python implementation raised
AttributeError instead, as the properties for these attributes had no
deleter, so the deletion was never forwarded to the wrapped object.

Both implementations now forward the deletion so the outcome matches
deleting the attribute on the wrapped object directly. The C extension
then refreshes its cached copy from the wrapped object, leaving the proxy
in the same state as one newly created over it.

The crash and its cause were identified by Ding Qiuran in GrahamDumpleton#351, which
also proposed a fix. This supersedes that pull request with a wider set
of tests, and separates out the unrelated change it had also picked up.
pull Bot pushed a commit to annihilatorrrr/wrapt that referenced this pull request Sep 20, 2026
Calling a FunctionWrapper, BoundFunctionWrapper or
PartialCallableObjectProxy for which __init__() was never called, but
which had __wrapped__ assigned directly, crashed the interpreter with the
C extension. Accessing such a FunctionWrapper as a descriptor did not
crash, but returned a wrapper built from the unset fields which crashed
when called. The check for an uninitialized wrapper only considers
__wrapped__, so it passed and the additional fields were then used while
still NULL. This arises where a derived class overrides __init__() and
sets __wrapped__ itself without calling __init__() of the base class, or
an instance is created using __new__() alone.

The fields are now checked once acquired and an AttributeError naming the
missing _self_ attribute is raised, as the pure Python implementation
already does. The pure Python BoundFunctionWrapper instead failed with a
RecursionError, as its __getattr__() looked up _self_parent on itself, and
now raises AttributeError too.

A crash on calling an uninitialized FunctionWrapper was first noted by
Ding Qiuran in GrahamDumpleton#351, which guarded one of the fields involved. This
addresses all of the fields and the other affected code paths.
@GrahamDumpleton

Copy link
Copy Markdown
Owner

Thank you for this, and for the care taken in tracking both problems down.
Both were real, long-standing crashes in the C extension, reachable from
ordinary Python code, and neither had been reported before.

I'm closing this PR as superseded rather than merging it, because the
fixes have landed by way of two separate PRs:

The reasons for doing it that way rather than merging this directly:

It covered two unrelated issues. The enabled guard added in the
later commit is a different problem, in different code, from the attribute
deletion described in the title and summary. Keeping them separate makes
each change easier to review, and keeps the history and changelog clear
about what was fixed and why.

The second issue turned out to be wider than the guard covered.
Looking into it further, enabled is only one of several fields acquired
together and then used unchecked, so with the guard in place the crash
became a SystemError from the next field along, rather than being
resolved. The same problem also affected PartialCallableObjectProxy, and
accessing an uninitialized FunctionWrapper as a descriptor, neither of
which this PR touched. The pure Python BoundFunctionWrapper had a
related fault of its own, failing with RecursionError. #354 addresses
all of those together.

A different approach was taken to the cached attribute. For the
deletion fix, #353 refreshes the copy of __module__ or __doc__ held in
the proxy's own dictionary from the wrapped object, rather than removing
the entry, so that the proxy ends up in the same state as one newly
created over the wrapped object. The pure Python side adds deleters to the
existing properties rather than special casing the names in
__delattr__(). The tests were also broadened, and changed so as not to
modify the target object shared with the other tests in that file.

Both PRs credit you for identifying the problems, in the commit messages
and the PR descriptions. The fixes will be in the 2.4.2 release.

Thanks again for the contribution.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants