Bug report
Bug description:
methodcaller_clear in Modules/_operator.c is declared as returning void, but it is
installed in the Py_tp_clear slot, which requires inquiry
(typedef int (*inquiry)(PyObject *);, Include/object.h:285).
/* Modules/_operator.c:1743 */
static void
methodcaller_clear(PyObject *op)
{
methodcallerobject *mc = methodcallerobject_CAST(op);
Py_CLEAR(mc->name);
Py_CLEAR(mc->args);
Py_CLEAR(mc->kwds);
Py_CLEAR(mc->vectorcall_args);
Py_CLEAR(mc->vectorcall_kwnames);
}
/* Modules/_operator.c:1928 */
static PyType_Slot methodcaller_type_slots[] = {
...
{Py_tp_clear, methodcaller_clear},
...
};
The mismatch is not diagnosed at compile time because PyType_Slot.pfunc is void *
(Include/object.h:327), so the function pointer converts implicitly and is later cast
back to inquiry.
UBSan with -fsanitize=function reports it whenever a methodcaller instance is cleared
by the cyclic GC:
Python/gc_free_threading.c:1761:24: runtime error: call to function methodcaller_clear through pointer to incorrect function type 'int (*)(struct _object *)'
Modules/_operator.c:1745: note: methodcaller_clear defined here
The trace above is from a free-threaded build. The defect is in the slot registration, not in
either GC, so it is present in both builds and both reach the bad call the same way:
- default build:
Python/gc.c:1105
- free-threaded build:
Python/gc_free_threading.c:1761
Impact
Benign on the mainstream targets I checked. Both GC implementations discard the result
((void) clear(op); at Python/gc.c:1105 and Python/gc_free_threading.c:1761), and
operator.methodcaller does not set Py_TPFLAGS_BASETYPE, so the return baseclear(self);
path in subtype_clear (Objects/typeobject.c:2751) cannot reach it.
It is still an indirect call through an incompatible function type, i.e. undefined behaviour,
and it matters for:
- builds using Clang CFI (
-fsanitize=cfi-icall), where an indirect-call type mismatch aborts;
- WebAssembly targets, where indirect call signatures are checked by the runtime;
- UBSan builds in general, where it is persistent noise that hides other findings.
methodcaller_traverse immediately below (Modules/_operator.c:1764) uses the correct
static int form, so this looks like an oversight from the slot-signature cleanup.
I grepped every tp_clear slot registration under Modules/, Objects/ and Python/:
methodcaller_clear is the only one with a void return type.
Reproducer
The wrong slot signature is present in every build; a sanitizer build is only what makes it
visible. Configured with:
CC=clang CXX=clang++ LDFLAGS='-fuse-ld=lld' ./configure --with-address-sanitizer --with-undefined-behavior-sanitizer --with-pydebug --disable-gil
--with-undefined-behavior-sanitizer adds -fsanitize=undefined (configure.ac:3528), and
Clang includes function in that group, which is the check that fires here.
Then:
./python -c "import gc, operator
class C: pass
c = C()
c.m = operator.methodcaller('foo', c)
del c
gc.collect()"
The methodcaller keeps a reference to c through its args tuple, so the cycle
c -> m -> args -> c reaches tp_clear and the diagnostic fires.
Suggested fix
-static void
+static int
methodcaller_clear(PyObject *op)
{
methodcallerobject *mc = methodcallerobject_CAST(op);
Py_CLEAR(mc->name);
Py_CLEAR(mc->args);
Py_CLEAR(mc->kwds);
Py_CLEAR(mc->vectorcall_args);
Py_CLEAR(mc->vectorcall_kwnames);
+ return 0;
}
methodcaller_dealloc (Modules/_operator.c:1759) already calls it as
(void)methodcaller_clear(op); and needs no change.
Environment
- CPython
main, commit b93576a2b66
- Linux x86-64
- Clang 22.1.8, lld
- Free-threaded debug build with ASan and UBSan:
CC=clang CXX=clang++ LDFLAGS='-fuse-ld=lld' ./configure --with-address-sanitizer --with-undefined-behavior-sanitizer --with-pydebug --disable-gil
CPython versions tested on:
3.16, CPython main branch, 3.15
Operating systems tested on:
Linux
Linked PRs
Bug report
Bug description:
methodcaller_clearinModules/_operator.cis declared as returningvoid, but it isinstalled in the
Py_tp_clearslot, which requiresinquiry(
typedef int (*inquiry)(PyObject *);,Include/object.h:285).The mismatch is not diagnosed at compile time because
PyType_Slot.pfuncisvoid *(
Include/object.h:327), so the function pointer converts implicitly and is later castback to
inquiry.UBSan with
-fsanitize=functionreports it whenever amethodcallerinstance is clearedby the cyclic GC:
The trace above is from a free-threaded build. The defect is in the slot registration, not in
either GC, so it is present in both builds and both reach the bad call the same way:
Python/gc.c:1105Python/gc_free_threading.c:1761Impact
Benign on the mainstream targets I checked. Both GC implementations discard the result
(
(void) clear(op);atPython/gc.c:1105andPython/gc_free_threading.c:1761), andoperator.methodcallerdoes not setPy_TPFLAGS_BASETYPE, so thereturn baseclear(self);path in
subtype_clear(Objects/typeobject.c:2751) cannot reach it.It is still an indirect call through an incompatible function type, i.e. undefined behaviour,
and it matters for:
-fsanitize=cfi-icall), where an indirect-call type mismatch aborts;methodcaller_traverseimmediately below (Modules/_operator.c:1764) uses the correctstatic intform, so this looks like an oversight from the slot-signature cleanup.I grepped every
tp_clearslot registration underModules/,Objects/andPython/:methodcaller_clearis the only one with avoidreturn type.Reproducer
The wrong slot signature is present in every build; a sanitizer build is only what makes it
visible. Configured with:
--with-undefined-behavior-sanitizeradds-fsanitize=undefined(configure.ac:3528), andClang includes
functionin that group, which is the check that fires here.Then:
The
methodcallerkeeps a reference tocthrough itsargstuple, so the cyclec -> m -> args -> creachestp_clearand the diagnostic fires.Suggested fix
methodcaller_dealloc(Modules/_operator.c:1759) already calls it as(void)methodcaller_clear(op);and needs no change.Environment
main, commitb93576a2b66CPython versions tested on:
3.16, CPython main branch, 3.15
Operating systems tested on:
Linux
Linked PRs