Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initialize/Finalize Python multiple time and import _ctypes each time lead to a memory corruption #116467

Closed
vstinner opened this issue Mar 7, 2024 · 10 comments
Labels
3.12 bugs and security fixes 3.13 bugs and security fixes topic-ctypes type-crash A hard crash of the interpreter, possibly with a core dump

Comments

@vstinner
Copy link
Member

vstinner commented Mar 7, 2024

Example on Windows:

import subprocess
program = r"PCbuild\amd64\_testembed_d.exe"
cmd = [program, "test_repeated_init_exec", "import _ctypes"]

for i in range(1, 11):
    print(f"   == Process #{i} ===")
    proc = subprocess.run(cmd)
    exitcode = proc.returncode
    print(f"=> exitcode {exitcode}")
    if exitcode:
        break
    print()

Output:

vstinner@WIN C:\victor\python\main>python bug.py   
Running Debug|x64 interpreter...
   == Process #1 ===
--- Loop #1 ---
--- Loop #2 ---
--- Loop #3 ---
--- Loop #4 ---
=> exitcode 0

   == Process #2 ===
--- Loop #1 ---
--- Loop #2 ---
Assertion failed: PyUnicode_CheckExact(ep->me_key), file C:\victor\python\main\Objects\dictobject.c, line 922
=> exitcode 3
@serhiy-storchaka serhiy-storchaka added topic-ctypes type-crash A hard crash of the interpreter, possibly with a core dump 3.13 bugs and security fixes labels Mar 7, 2024
@vstinner
Copy link
Member Author

vstinner commented Mar 7, 2024

The regression was introduced by commit ea2c001:

commit ea2c0016502472aa8baa3149050ada776d17a009
Author: Eddie Elizondo <eduardo.elizondorueda@gmail.com>
Date:   Sat Apr 22 15:39:37 2023 -0400

    gh-84436: Implement Immortal Objects (gh-19474)

    This is the implementation of PEP683

    Motivation:

    The PR introduces the ability to immortalize instances in CPython which bypasses reference counting. Tagging objects as immortal allows up to skip certain operations when we know that the object will be around for the entire execution of the runtime.

    Note that this by itself will bring a performance regression to the runtime due to the extra reference count checks. However, this brings the ability of having truly immutable objects that are useful in other contexts such as immutable data sharing between sub-interpreters. 

@vstinner
Copy link
Member Author

vstinner commented Mar 7, 2024

_ctypes code can be simplified to:

static int
_ctypes_mod_exec(PyObject *mod)
{
    ctypes_state *st = GLOBAL_STATE();
    if (PyType_Ready(st->PyCData_Type) < 0) {
        return -1;
    }

    st->PyCStgDict_Type->tp_base = &PyDict_Type;
    if (PyType_Ready(st->PyCStgDict_Type) < 0) {
        return -1;
    }
    return 0;
}


PyMODINIT_FUNC
PyInit__ctypes(void)
{
    PyObject *mod = PyModule_Create(&_ctypesmodule);
    if (!mod) {
        return NULL;
    }

    if (_ctypes_mod_exec(mod) < 0) {
        Py_DECREF(mod);
        return NULL;
    }
    return mod;
}

I can still reproduce the bug with this code. Maybe the problem comes from the PyCStgDict_Type type which inherits from PyDict_Type (dict type).

@vstinner
Copy link
Member Author

vstinner commented Mar 7, 2024

Python 3.12 and main branches are affected (Python 3.11 and older are not affected). I can reproduce the issue on Linux with the script:

import subprocess
program = r"Programs/_testembed"
cmd = [program, "test_repeated_init_exec", "import _ctypes"]

for i in range(1, 11):
    print(f"   == Process #{i} ===")
    proc = subprocess.run(cmd)
    exitcode = proc.returncode
    print(f"=> exitcode {exitcode}")
    if exitcode:
        break
    print()

@serhiy-storchaka serhiy-storchaka added the 3.12 bugs and security fixes label Mar 7, 2024
@vstinner
Copy link
Member Author

vstinner commented Mar 7, 2024

The error occurs when the _ctypes extension is imported the second time, so when PyCData_Type type is initialized for the second time by PyType_Ready():

(...)
#4  0x00007ffff7d1bc57 in __assert_fail () from /lib64/libc.so.6
#5  0x0000000000545fb5 in compare_unicode_unicode (mp=0x0, dk=0x7fffea4cc7c0, ep0=0x7fffea4cc7f0, ix=5, 
    key=0xa6a050 <_PyRuntime+47952>, hash=-4323121235327844407) at Objects/dictobject.c:1101
#6  0x0000000000545c97 in do_lookup (mp=0x0, dk=0x7fffea4cc7c0, key=0xa6a050 <_PyRuntime+47952>, hash=-4323121235327844407, 
    check_lookup=0x545f29 <compare_unicode_unicode>) at Objects/dictobject.c:1027
#7  0x0000000000546032 in unicodekeys_lookup_unicode (dk=0x7fffea4cc7c0, key=0xa6a050 <_PyRuntime+47952>, 
    hash=-4323121235327844407) at Objects/dictobject.c:1112
#8  0x000000000054628e in _Py_dict_lookup (mp=0x7fffea4d4830, key=0xa6a050 <_PyRuntime+47952>, hash=-4323121235327844407, 
    value_addr=0x7fffffff63c8) at Objects/dictobject.c:1201
#9  0x000000000054d354 in _PyDict_Contains_KnownHash (op=0x7fffea4d4830, key=0xa6a050 <_PyRuntime+47952>, 
    hash=-4323121235327844407) at Objects/dictobject.c:4589
#10 0x000000000054d2bf in PyDict_Contains (op=0x7fffea4d4830, key=0xa6a050 <_PyRuntime+47952>) at Objects/dictobject.c:4563
#11 0x000000000059a8cf in _PyType_CheckConsistency (type=0x7ffff7c7e3a0 <PyCData_Type>) at Objects/typeobject.c:597
#12 0x00000000005aa80f in PyType_Ready (type=0x7ffff7c7e3a0 <PyCData_Type>) at Objects/typeobject.c:7844
#13 0x00007ffff7c685f7 in _ctypes_add_types (mod=0x7fffea306930) at ./Modules/_ctypes/_ctypes.c:5582
#14 0x00007ffff7c68ec3 in _ctypes_mod_exec (mod=0x7fffea306930) at ./Modules/_ctypes/_ctypes.c:5709
#15 0x00007ffff7c68f26 in PyInit__ctypes () at ./Modules/_ctypes/_ctypes.c:5728
#16 0x00000000006e93cb in _PyImport_LoadDynamicModuleWithSpec (spec=0x7fffea318380, fp=0x0) at ./Python/importdl.c:170
(...)

(gdb) frame 12
#12 0x00000000005aa80f in PyType_Ready (type=0x7ffff7c7e3a0 <PyCData_Type>) at Objects/typeobject.c:7844
7844	        return 0;
(gdb) p type->tp_name
$6 = 0x7ffff7c75172 "_ctypes._CData"

At the first PyType_Ready(st->PyCData_Type), PyCData_Type.tp_dict is set to a new dictionary. Dictionary once _ctypes is created:

{
'__hash__': <slot wrapper '__hash__' of '_ctypes._CData' objects>,
'__buffer__': <slot wrapper '__buffer__' of '_ctypes._CData' objects>,
'__ctypes_from_outparam__': <method '__ctypes_from_outparam__' of '_ctypes._CData' objects>,
'__reduce__': <method '__reduce__' of '_ctypes._CData' objects>,
'__setstate__': <method '__setstate__' of '_ctypes._CData' objects>,
'_b_base_': <member '_b_base_' of '_ctypes._CData' objects>,
'_b_needsfree_': <member '_b_needsfree_' of '_ctypes._CData' objects>,
'_objects': <member '_objects' of '_ctypes._CData' objects>,
'__doc__': 'XXX to be provided',
}

@ericsnowcurrently
Copy link
Member

CC @eduardo-elizondo

@encukou
Copy link
Member

encukou commented Mar 8, 2024

I propose removing StgDict, with this big patch: #116458.
I haven't tested this with that patch yet, but even if it doesn't help, anyone working in this area should be aware of it :)
It definitely won't help 3.12 of course. Then again, maybe this is too obscure for a fix in 3.12.

@erlend-aasland
Copy link
Contributor

erlend-aasland commented Mar 8, 2024

I propose removing StgDict, with this big patch: #116458. I haven't tested this with that patch yet, but even if it doesn't help, anyone working in this area should be aware of it :) It definitely won't help 3.12 of course. Then again,

Using @vstinner's repro from #116467 (comment), I can confirm that #116458 resolves the issue 🚀 🎉

IOW, let's get on with reviewing that big patch 😄

@neonene
Copy link
Contributor

neonene commented Mar 12, 2024

I haven't tested this with that patch yet, but even if it doesn't help,

#116458 does resolve use-after-free of almost all ctypes-related interned strings.

Also, please consider making the following string pointers NULL on each init phase, until the module is fully isolated.

##-----------------------
## cached - initialized once
## manually cached PyUnicodeOjbect
Modules/_ctypes/callproc.c _ctypes_get_errobj error_object_name -
Modules/_ctypes/_ctypes.c CreateSwappedType suffix -

@erlend-aasland
Copy link
Contributor

erlend-aasland commented Mar 20, 2024

@encukou landed #116458, which also fixes this issue IIUC. Let's close it as resolved. Well, it is fixed in 3.13, but not in 3.12. Correct me if I'm wrong.

@vstinner
Copy link
Member Author

@encukou landed #116458, which also fixes this issue IIUC. Let's close it as resolved. Well, it is fixed in 3.13, but not in 3.12. Correct me if I'm wrong.

IMO it's ok to not fix the issue in Python 3.12. Apparently, I was the only one who noticed, so it's not a big deal. I close the issue. Thanks for the fix @encukou!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
3.12 bugs and security fixes 3.13 bugs and security fixes topic-ctypes type-crash A hard crash of the interpreter, possibly with a core dump
Projects
None yet
Development

No branches or pull requests

6 participants