Skip to content

Fix a memory leak when creating Python3 modules. #2019

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

Merged
merged 1 commit into from
Dec 11, 2019
Merged

Conversation

T045T
Copy link
Contributor

@T045T T045T commented Dec 9, 2019

No description provided.

@T045T T045T force-pushed the master branch 2 times, most recently from bf30d61 to 0f5a7bc Compare December 9, 2019 18:23
@T045T
Copy link
Contributor Author

T045T commented Dec 9, 2019

Is there an easy way to repro the Python3.5 Travis failure? I'd like to run that with a debugger attached.

@bstaletic
Copy link
Collaborator

So, unrelated to this PR, I've been playing with valgrind and pybind this morning. I can confirm that this solves the 104 byte leak.

As for the travis/python3.5 environment, take a look at

https://github.com/pybind/pybind11/blob/master/.travis.yml#L70-L76

@wjakob
Copy link
Member

wjakob commented Dec 11, 2019

Hi @T045T, @bstaletic,

would you mind explaining in a bit more detail what the original problem is?

Best,
Wenzel

@bstaletic
Copy link
Collaborator

@wjakob

I'm not sure how this PR fixes the leak, but I can provide valgrind output to confirm that it does.

Running PYTHONMALLOC=malloc valgrind python -c 'import ycm_core' where ycm_core is an extension module defined here, results in the following leak:

==14848== 104 bytes in 1 blocks are definitely lost in loss record 1,846 of 2,687
==14848==    at 0x4838DEF: operator new(unsigned long) (vg_replace_malloc.c:344)
==14848==    by 0x5A387C1: PyInit_ycm_core (in /home/bstaletic/work/ycmd/ycm_core.so)
==14848==    by 0x4C4FB89: _PyImport_LoadDynamicModuleWithSpec (in /usr/lib/libpython3.8.so.1.0)
==14848==    by 0x4C4FD69: ??? (in /usr/lib/libpython3.8.so.1.0)
==14848==    by 0x4B5E8C6: ??? (in /usr/lib/libpython3.8.so.1.0)
==14848==    by 0x4B6DE90: PyVectorcall_Call (in /usr/lib/libpython3.8.so.1.0)
==14848==    by 0x4BC2ADF: _PyEval_EvalFrameDefault (in /usr/lib/libpython3.8.so.1.0)
==14848==    by 0x4B8D039: _PyEval_EvalCodeWithName (in /usr/lib/libpython3.8.so.1.0)
==14848==    by 0x4B8DDEE: _PyFunction_Vectorcall (in /usr/lib/libpython3.8.so.1.0)
==14848==    by 0x4BC197C: _PyEval_EvalFrameDefault (in /usr/lib/libpython3.8.so.1.0)
==14848==    by 0x4B8DD5A: _PyFunction_Vectorcall (in /usr/lib/libpython3.8.so.1.0)
==14848==    by 0x4BBD88E: _PyEval_EvalFrameDefault (in /usr/lib/libpython3.8.so.1.0)

So pybind is indeed leaking 104 bytes:

==14543==    definitely lost: 104 bytes in 1 blocks
==14543==    indirectly lost: 0 bytes in 0 blocks
==14543==      possibly lost: 260,043 bytes in 1,413 blocks
==14543==    still reachable: 566,855 bytes in 4,552 blocks

The "possibly lost" and "still reachable" are from CPython itself, but the "definitely lost" is pybind's fault. Again, I don't know how this pull request works, but the above valgrind command runs clean with this PR.

@T045T
Copy link
Contributor Author

T045T commented Dec 11, 2019

So, PyModule_Create() really expects to be called with a pointer to a static PyModuleDef. That is, I don't think it takes ownership. Creating the PyModuleDef with new and not freeing it anywhere was what caused the memory leak.

The original code then called Py_INCREF(), which works together with Py_DECREF() to implement reference counting.
When the reference count reaches zero, the pointer is freed.
I figured it would be safest to use this mechanism to eventually deallocate the pointer, because I was not 100% certain that things elsewhere don't call Py_INCREF() or Py_DECREF() on it.

It seemed like nothing called Py_DECREF() on our module definition, however. The memory analysis tools kind of proved that.
That's why I added the m_free callback. That one is called when the module that PyModule_Create(), well, creates, is freed again. The argument is supposed to be a pointer to the module.

So this is how I called Py_DECREF() - now why did I change new to PyMem_New()? Because Py_DECREF() uses a special deallocation function that uses free, not delete, I needed the pointer to be created via malloc, which is exactly what PyMem_New() does.

So there is a risk that using Py_INCREF() and Py_DECREF() here is needless cargo culting, and I could have just used new and delete instead, but I'm not confident that that would not break.

@T045T
Copy link
Contributor Author

T045T commented Dec 11, 2019

I also just pushed an updated version that might resolve the Segfault in the Travis builds – in case either of the pointers is NULL (Py_XDECREF() checks for NULL).

@bstaletic
Copy link
Collaborator

@T045T In case you haven't, rebase onto latest master, since some travis fixes have been merged.

Copy link
Member

@wjakob wjakob left a comment

Choose a reason for hiding this comment

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

Just some super-minor code style comments.

std::memset(def, 0, sizeof(PyModuleDef));
def->m_name = name;
def->m_doc = doc;
def->m_size = -1;
def->m_free = [](void* module) {
if (module != nullptr) {
Py_XDECREF( PyModule_GetDef((PyObject*) module));
Copy link
Member

Choose a reason for hiding this comment

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

extra space after '('

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed. Note that neither of these were reported by tools/check-style.sh

@wjakob
Copy link
Member

wjakob commented Dec 11, 2019

Merged, thanks!

@wjakob wjakob merged commit 819802d into pybind:master Dec 11, 2019
@wjakob
Copy link
Member

wjakob commented Dec 11, 2019

Hmm, I realize now that Python 3.5 segfaults likely due to this PR. I'll revert the commit for now until this issue is resolved.

wjakob added a commit that referenced this pull request Dec 11, 2019
@T045T
Copy link
Contributor Author

T045T commented Dec 11, 2019 via email

@bstaletic
Copy link
Collaborator

@T045T I was able to repro the segfault with either gcc or clang. The only requirement was a debug version of python.

Cmake configuration output:

bstaletic@Gallifrey build  (git)-[tstate-leak]-% cmake -DCMAKE_BUILD_TYPE=Debug -DPYTHON_EXECUTABLE=/usr/bin/python3.5dm -DPYBIND11_PYTHON_VERSION=3.5 -DPYBIND11_CPP_STANDARD=-std=c++14 -DPYBIND11_WERROR=ON -DDOWNLOAD_CATCH=ON ..
-- The CXX compiler identification is Clang 9.0.0
-- Check for working CXX compiler: /usr/sbin/clang++
-- Check for working CXX compiler: /usr/sbin/clang++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Found PythonInterp: /usr/bin/python3.5dm (found suitable version "3.5.9", minimum required is "3.5")
-- Found PythonLibs: /usr/lib/libpython3.5dm.so
-- Building tests WITHOUT Eigen
-- Found Boost: /usr/lib64/cmake/Boost-1.71.0/BoostConfig.cmake (found suitable version "1.71.0", minimum required is "1.56")
-- Performing Test HAS_FLTO_THIN
-- Performing Test HAS_FLTO_THIN - Success
-- LTO enabled
-- Downloading catch v1.9.3...
-- Building interpreter tests using Catch v1.9.3
-- Looking for C++ include pthread.h
-- Looking for C++ include pthread.h - found
-- Performing Test CMAKE_HAVE_LIBC_PTHREAD
-- Performing Test CMAKE_HAVE_LIBC_PTHREAD - Failed
-- Looking for pthread_create in pthreads
-- Looking for pthread_create in pthreads - not found
-- Looking for pthread_create in pthread
-- Looking for pthread_create in pthread - found
-- Found Threads: TRUE
-- pybind11 v2.4.dev4
-- Configuring done
-- Generating done
-- Build files have been written to: /home/bstaletic/work/pybind11/build

After running make pytest and failing, I could do the following:

  • cd tests
  • python3.5dm -m pytest test_eval.py
bstaletic@Gallifrey tests  (git)-[tstate-leak]-% python3.5dm -m pytest test_eval.py
===================================================================== test session starts =====================================================================
platform linux -- Python 3.5.9, pytest-5.3.1, py-1.8.0, pluggy-0.13.1
rootdir: /home/bstaletic/work/pybind11/tests, inifile: pytest.ini
collected 1 item

test_eval.py .                                                                                                                                          [100%]

====================================================================== 1 passed in 0.01s ======================================================================
Fatal Python error: Segmentation fault

Current thread 0x00007f0792657740 (most recent call first):
zsh: segmentation fault  python3.5dm -m pytest test_eval.py

Running the same in gdb:

bstaletic@Gallifrey tests  (git)-[tstate-leak]-% gdb python3.5dm --quiet
Reading symbols from python3.5dm...
(No debugging symbols found in python3.5dm)
(gdb) run -m pytest test_eval.py
Starting program: /usr/bin/python3.5dm -m pytest test_eval.py
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/libthread_db.so.1".
[Detaching after fork from child process 9962]
/usr/lib/../share/gcc-9.2.0/python/libstdcxx/v6/xmethods.py:731: SyntaxWarning: list indices must be integers or slices, not str; perhaps you missed a comma?
  refcounts = ['_M_refcount']['_M_pi']
===================================================================== test session starts =====================================================================
platform linux -- Python 3.5.9, pytest-5.3.1, py-1.8.0, pluggy-0.13.1
rootdir: /home/bstaletic/work/pybind11/tests, inifile: pytest.ini
collected 1 item

test_eval.py .                                                                                                                                          [100%]

====================================================================== 1 passed in 0.01s ======================================================================

Program received signal SIGSEGV, Segmentation fault.
0x00007ffff7cecb24 in _Py_ForgetReference (op=op@entry=0x55555595f530) at Objects/object.c:1766
1766        if (op == &refchain ||
(gdb) bt
#0  0x00007ffff7cecb24 in _Py_ForgetReference (op=op@entry=0x55555595f530) at Objects/object.c:1766
#1  0x00007ffff7cec367 in _Py_Dealloc (op=0x55555595f530) at Objects/object.c:1794
#2  0x00007ffff7cd32e6 in free_keys_object (keys=0x555555ad9b40) at Objects/dictobject.c:351
#3  0x00007ffff7cd409b in dict_dealloc (mp=0x7ffff757f598) at Objects/dictobject.c:1654
#4  0x00007ffff7dc9e8e in _PyImport_Fini () at Python/import.c:301
#5  0x00007ffff7ddaaed in Py_Finalize () at Python/pylifecycle.c:605
#6  Py_Finalize () at Python/pylifecycle.c:519
#7  0x00007ffff7ddbb49 in Py_Exit (sts=sts@entry=0) at Python/pylifecycle.c:1474
#8  0x00007ffff7ddf8e7 in handle_system_exit () at Python/pythonrun.c:617
#9  0x00007ffff7ddfded in handle_system_exit () at Python/pythonrun.c:685
#10 PyErr_PrintEx (set_sys_last_vars=set_sys_last_vars@entry=1) at Python/pythonrun.c:627
#11 0x00007ffff7de015b in PyErr_Print () at Python/pythonrun.c:523
#12 0x00007ffff7dfe9de in RunModule (modname=<optimized out>, set_argv0=<optimized out>) at Modules/main.c:210
#13 0x00007ffff7dff8a0 in RunMainFromImporter (sys_path0=<optimized out>) at Modules/main.c:735
#14 Py_Main (argc=4, argv=<optimized out>) at Modules/main.c:749
#15 0x0000555555555194 in main ()
(gdb)

@bstaletic
Copy link
Collaborator

Here's a minimal repro:

  • Prerequisites:
    • Python 3.5 compiled like this
    • foo.cpp that contains the following code:
#include <pybind11/pybind11.h>
PYBIND11_MODULE(foo, m){}
  • Steps to repro:

    • c++ -shared foo.cpp -o foo.so -fPIC `python3.5dm-config --cflags --ldflags` -isystem pybind/include
      
    • python -c 'import foo'
      
  • Expected output: empty

  • Actual output: Segmentation fault

  • Backtrace: same as above

@T045T
Copy link
Contributor Author

T045T commented Dec 12, 2019

I cannot repro this, either via CMake or your minimal repro case.

These are the steps I used to set up the dev environment:

# install prerequisites
sudo apt install -y make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev libncursesw5-dev xz-utils tk-dev libffi-dev liblzma-dev python-openssl git valgrind

# set up pyenv
git clone https://github.com/pyenv/pyenv.git ~/.pyenv
echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bashrc
echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bashrc
echo -e 'if command -v pyenv 1>/dev/null 2>&1; then\n  eval "$(pyenv init -)"\nfi' >> ~/.bashrc
source ~/.bashrc

# install Python 3.5.9 with the same build settings as the arch config
PYTHON_CONFIGURE_OPTS="--enable-shared --with-threads --with-computed-gotos --enable-ipv6 --with-valgrind --with-system-expat --with-dbmliborder=gdbm:ndbm --with-system-ffi  OPT=\"-fPIC\"" pyenv install 3.5.9

# Tell pyenv to use 3.5.9 in the pybind11 folder (not strictly necessary, except for the python call at the very end).
cd $HOME/pybind11
pyenv local 3.5.9

# build pybind
mkdir build
cd build
cmake -DCMAKE_BUILD_TYPE=Debug -DPYTHON_EXECUTABLE=$(pyenv root)/versions/3.5.9/bin/python3 -DPYBIND11_PYTHON_VERSION=3.5 -DPYBIND11_CPP_STANDARD=-std=c++14 -DPYBIND11_WERROR=ON -DDOWNLOAD_CATCH=ON ..

# run test
make pytest -j4

# Alternatively, manually compile repro example
cd $HOME/pybind11
echo -e "#include <pybind11/pybind11.h>\nPYBIND11_MODULE(foo, m){}" > foo.cpp
c++ -shared foo.cpp -o foo.so -fPIC `$(pyenv root)/versions/3.5.9/bin/python3.5-config --cflags --ldflags` -isystem include

python -c 'import foo'

@bstaletic
Copy link
Collaborator

That's strange... I don't see anything different compared to what I have and I can consistently repro.

I've actually started with a "debian:stretch" docker container, since that's what Travis is actually running. The following is what travis is executing for the problematic build:

export CXX=g++-6 CC=gcc-6
docker pull debian:stretch
containerid=$(docker run --detach --tty \
  --volume="$PWD":/pybind11 --workdir=/pybind11 \
  --env="CC=$CC" --env="CXX=$CXX" \
  debian:stretch)
SCRIPT_RUN_PREFIX="docker exec --tty $containerid"
$SCRIPT_RUN_PREFIX sh -c 'for s in 0 15; do sleep $s; apt-get update && apt-get -qy dist-upgrade && break; done'
cmake --version
$SCRIPT_RUN_PREFIX sh -c "for s in 0 15; do sleep \$s; \
 apt-get -qy --no-install-recommends install \
 python3.5-dbg python3-scipy-dbg python3.5-dev python3-pytest python3-scipy \
 libeigen3-dev libboost-dev cmake make g++-6 && break; done"
$SCRIPT_RUN_PREFIX cmake \
  -DCMAKE_BUILD_TYPE=Debug \
  -DPYTHON_EXECUTABLE=/usr/bin/python3.5dm \
  -DPYBIND11_PYTHON_VERSION=3.5 \
  -DPYBIND11_CPP_STANDARD=-std=c++14 \
  -DPYBIND11_WERROR=ON \
  -DDOWNLOAD_CATCH=ON \
  .
$SCRIPT_RUN_PREFIX make pytest -j 2 VERBOSE=1
$SCRIPT_RUN_PREFIX make cpptest -j 2

The cpptest step shouldn't be needed or even reached, since the segfault is in pytest.
Note that I haven't tried running this script all at once, I executed the commands manually.

@bstaletic
Copy link
Collaborator

@T045T Actually, forget that docker. I know what's wrong with your build step...

I had to edit locally the PKGBUILD I linked. Add --with-pydebug to PYTHON_CONFIGURE_OPTS.

@T045T
Copy link
Contributor Author

T045T commented Dec 12, 2019

That did it, thanks! Confirmed that changing to new/delete fixes the segfault, and works on at least 3.5 and 3.7 (I'll let Travis verify that nothing else broke).

rwgk added a commit to rwgk/pybind11 that referenced this pull request Aug 19, 2020
Trying a different approach to pybind#2019.

EXPERIMENTAL, PROOF OF CONCEPT, please do not review.
If this approach works out additional work is needed to avoid code duplication.
rwgk added a commit to rwgk/pybind11 that referenced this pull request Aug 28, 2020
This PR solves the same issue as pybind#2019 (rolled back), but in a way that is
certain to be portable and will work for any leak checker.

The Python 3 documentation suggests `static` allocation for `PyModuleDef`:

  * https://docs.python.org/3/c-api/module.html#initializing-c-modules

  * The module definition struct, which holds all information needed to
    create a module object. There is usually only one statically initialized
    variable of this type for each module.

This PR changes the `PYBIND11_MODULE` macro accordingly: `static PyModuleDef mdef;`

The `pybind11::module::module` code is slightly refactored, with the idea
to make the future removal of Python 2 support straightforward.
rwgk added a commit to rwgk/pybind11 that referenced this pull request Aug 28, 2020
This PR solves the same issue as pybind#2019 (rolled back), but in a way that is
certain to be portable and will work for any leak checker.

The Python 3 documentation suggests `static` allocation for `PyModuleDef`:

  * https://docs.python.org/3/c-api/module.html#initializing-c-modules

  * The module definition struct, which holds all information needed to
    create a module object. There is usually only one statically initialized
    variable of this type for each module.

This PR changes the `PYBIND11_MODULE` macro accordingly: `static PyModuleDef mdef;`

The `pybind11::module::module` code is slightly refactored, with the idea
to make the future removal of Python 2 support straightforward.
rwgk added a commit to rwgk/pybind11 that referenced this pull request Sep 16, 2020
This PR solves the same issue as pybind#2019 (rolled back), but in a way that is
certain to be portable and will work for any leak checker.

The Python 3 documentation suggests `static` allocation for `PyModuleDef`:

  * https://docs.python.org/3/c-api/module.html#initializing-c-modules

  * The module definition struct, which holds all information needed to
    create a module object. There is usually only one statically initialized
    variable of this type for each module.

This PR changes the `PYBIND11_MODULE` macro accordingly: `static PyModuleDef mdef;`

The `pybind11::module::module` code is slightly refactored, with the idea
to make the future removal of Python 2 support straightforward.
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.

3 participants