Skip to content

Conversation

@hyongtao-code
Copy link
Contributor

@hyongtao-code hyongtao-code commented Dec 24, 2025

In _ctypes._build_result(), a result tuple is allocated when numretvals > 1. If v becomes NULL during result construction, the function returns early without DECREF'ing the tuple, leaking a reference.
Add Py_XDECREF(tup) before returning when numretvals > 1 and v == NULL.

In _ctypes._build_result(), a result tuple is allocated when numretvals > 1.
If v becomes NULL during result construction, the function returns early
without DECREF'ing the tuple, leaking a reference.
Add Py_XDECREF(tup) before returning when numretvals > 1 and v == NULL.

Signed-off-by: Yongtao Huang <yongtaoh2022@gmail.com>
Copy link
Member

@StanFromIreland StanFromIreland left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, but this should get an issue and blurb.

@hyongtao-code hyongtao-code changed the title Fix reference leak in ctypes _build_result() gh-143145: Fix reference leak in ctypes _build_result() Dec 24, 2025
@hyongtao-code
Copy link
Contributor Author

Thanks for the review. More testing details are provided below:

Reproducer

import ctypes
import sys
import gc

class BadInt(ctypes.c_int):
    def __ctypes_from_outparam__(self):
        raise RuntimeError("boom from __ctypes_from_outparam__")

msvcrt = ctypes.CDLL("msvcrt.dll")
PROTO = ctypes.CFUNCTYPE(ctypes.c_void_p, ctypes.POINTER(BadInt), ctypes.POINTER(BadInt), ctypes.c_size_t)

PARAMFLAG_FIN = 1
PARAMFLAG_FOUT = 2
paramflags = (
    (PARAMFLAG_FOUT, "a", BadInt()),
    (PARAMFLAG_FOUT, "b", BadInt()),
    (PARAMFLAG_FIN,  "n"),
)
f = PROTO(("memmove", msvcrt), paramflags)

def totalrefcount():
    return getattr(sys, "gettotalrefcount")()

base = totalrefcount()
print("base totalrefcount:", base)

N = 20000
step = 2000

for i in range(1, N + 1):
    try:
        f(4)
    except RuntimeError:
        pass
    if i % step == 0:
        gc.collect()
        cur = totalrefcount()
        print(f"{i} totalrefcount: {cur}  delta: {cur - base}")

Result without pr

When running the reproducer on a debug build python_d.exe, sys.gettotalrefcount() increases linearly with the number of calls.

d:\MyCode\cpython\PCbuild\amd64>python_d.exe py_leak_outparam_tuple.py
base totalrefcount: 26086
2000 totalrefcount: 27922  delta: 1836
4000 totalrefcount: 29923  delta: 3837
6000 totalrefcount: 31923  delta: 5837
8000 totalrefcount: 33923  delta: 7837
10000 totalrefcount: 35923  delta: 9837
12000 totalrefcount: 37923  delta: 11837
14000 totalrefcount: 39923  delta: 13837
16000 totalrefcount: 41923  delta: 15837
18000 totalrefcount: 43923  delta: 17837
20000 totalrefcount: 45923  delta: 19837

Result with pr

After applying the fix, the total reference count remains stable. No linear growth is observed across repeated calls, confirming that the reference leak is fixed.

d:\MyCode\cpython\PCbuild\amd64>python_d.exe py_leak_outparam_tuple.py
base totalrefcount: 26078
2000 totalrefcount: 25914  delta: -164
4000 totalrefcount: 25915  delta: -163
6000 totalrefcount: 25915  delta: -163
8000 totalrefcount: 25915  delta: -163
10000 totalrefcount: 25915  delta: -163
12000 totalrefcount: 25915  delta: -163
14000 totalrefcount: 25915  delta: -163
16000 totalrefcount: 25915  delta: -163
18000 totalrefcount: 25915  delta: -163
20000 totalrefcount: 25915  delta: -163

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants