Quantcast
Channel: Planet Python
Viewing all articles
Browse latest Browse all 22462

Python Sweetness: Debugging C extension memory safety with valgrind on CPython

$
0
0

Here is a quick summary of the steps required to prepare a build of Python 2.7 for use with valgrind memcheck tool.

Valgrind is an indispensable tool for discovering memory safety issues, and for quickly tracking down the source of many kinds of memory corruption, especially in C code the user may not be familiar with. If your Python program is crashing with a segmentation fault, executing it using an instrumented build running under valgrind can very often pinpoint the exact cause of the fault, if the fault is due to a memory safety violation.

I needed this setup as today I decided to finalize an old incomplete patch to py-lmdb, to allow passing fixed-size integer keys and values to the database, useful for reducing storage and performance overhead. Today valgrind is useful since with the patch enabled, LMDB appears to disable some of its sanity checks around the size of key/value pointers passed to it, and so in order to maintain memory safety in the binding, I’d like to discover any places where I’ve forgotten to add a corresponding size check.

Grab dependencies

Grab a copy of the Python source tree, and check out the 2.7 branch:

$ sudo apt-get install build-essential valgrind
$ pip install Mercurial
$ cd /home/dmw/src
$ hg clone https://hg.python.org/cpython/
$ cd cpython
$ hg checkout 2.7

Build Python

Build a copy of Python with pymalloc disabled. This is required since when pymalloc is enabled, Python asks for large chunks of memory at a time from the C library, and so valgrind is unable to discover allocation boundaries using its magical instrumented malloc and free implementations that it injects into the target process.

Additionally build with Py_DEBUG to cause reference counts to be more thoroughly checked. Do not --enable-shared, as this produces a Python binary that will link to your system-installed libpython.so unless you remember to set LD_LIBRARY_PATH every time you run the debug build.

There is also a --with-valgrind option that prepares a build where pymalloc is disabled at runtime when running under valgrind, but means the Python binary will change behaviour depending on whether it’s running directly, under valgrind, or under gdb, a fabulous source of seeming heisenbugs when the user (me) is half asleep, so I prefer to avoid it.

Finally, set --prefix to a subdirectory so that when we come to install pip and setuptools, these tools are not installed into /usr/local or any other global filesystem path.

$ ./configure --without-pymalloc --with-pydebug --prefix=`pwd`/build
$ make -j 16

Test The Build

We do not need to install the built tree in order to make use of it, simply invoke ./python to run it in-place:

$ ./python
Python 2.7.12+ (2.7:11a9bca71528, Oct 17 2016, 16:21:52)
[GCC 4.9.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>>
[59613 refs]
[27063 refs]

The presence of [59613 refs] indicates the binary you are running was built with --with-pydebug.

Try It Under Valgrind

Now let’s try running the binary by itself under valgrind, to ensure our configuration is sane, and that valgrind is not reporting errors because we screwed up some step.

$ time valgrind ./python -S -c 'print 123'
==12914== Memcheck, a memory error detector
==12914== Copyright (C) 2002-2013, and GNU GPL'd, by Julian Seward et al.
==12914== Using Valgrind-3.10.0 and LibVEX; rerun with -h for copyright info
==12914== Command: ./python -S -c print\ 123
==12914==
123
[10871 refs]
==12914==
==12914== HEAP SUMMARY:
==12914==     in use at exit: 470,972 bytes in 2,963 blocks
==12914==   total heap usage: 9,362 allocs, 6,399 frees, 1,626,675 bytes allocated
==12914==
==12914== LEAK SUMMARY:
==12914==    definitely lost: 0 bytes in 0 blocks
==12914==    indirectly lost: 0 bytes in 0 blocks
==12914==      possibly lost: 77,979 bytes in 310 blocks
==12914==    still reachable: 392,993 bytes in 2,653 blocks
==12914==         suppressed: 0 bytes in 0 blocks
==12914== Rerun with --leak-check=full to see details of leaked memory
==12914==
==12914== For counts of detected and suppressed errors, rerun with: -v
==12914== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

real    0m2.201s
user    0m2.168s
sys     0m0.032s

We see already valgrind is reporting memory has been leaked. This is normal, as there are a variety of small structures allocated by CPython at startup that it never frees, usually because doing so would be too difficult.

You may also notice this Python build is much slower! Not only is valgrind slowing performance quite a lot, but the absense of pymalloc vastly hurts performance of the built interpreter. Why is this? Well at least, pymalloc knows more about the allocation styles that Python needs in comparison to the C library allocator, and so can be specialized for it, but perhaps more importantly, unlike the C allocator, pymalloc knows it will always run under the Global Interpreter Lock, so it does not require any additional mutexes for its data structures.

Running with --leak-check=full should indicate most of these “leaks” have PyType_Ready somewhere in the call stack, indicating they are heap allocations to support the static Py_Type structures that implement a variety of built-in functionality (such as the str object).

Install pip and setuptools

$ ./python -m ensurepip

We can now install any Python packages we need to support our test environment. If your application is complex, it might be desirable to also ./python -m pip install virtualenv along with make install to get a working virtualenv. I do not need it now so we will skip this step.

Build your application with the debug Python build

Since we built a completely new Python interpreter with debugging enabled, its header definitions will no longer match the headers that were in use by any C extensions you may have previously built. This is important, since the critical macros Py_INCREF and Py_DECREF change their implementation significantly in debug mode. For that reason, you must rebuild any third party modules your application is using with the new Python build.

In my case, I am not debugging an application, just an extension module, so I will only rebuild it, and add a little symlink so I do not need to install it to --prefix every time I rebuild it.

$ cd /home/dmw/src/py-lmdb
$ git clean -dfx
$ /home/dmw/src/cpython/python setup.py build
running build
running build_py
...
[91959 refs]

$ cd lmdb
$ ln -s ../build/lib.linux-x86_64-2.7-pydebug/lmdb/cpython.so

Trigger behaviour leading to the scenario you wish to debug

Self-explanatory. In my case, running py-lmdb’s tests is sufficient to reach the code paths I suspect might be damaged, so I’ll install py.test and run the tests under vaglrind.

To demonstrate what a valgrind error looks like, I’ve injected some obviously buggy code into cpython.c:

    {
        printf("hello\n");
        char *x = malloc(3);
        setenv("X", x, 0);
    }

This allocates some uninitialized memory, then attempts to set the X environment variable to the content of that memory. It is a bug since the memory could contain any random junk, and accessing uninitialized memory in C is “undefined behaviour”, i.e. the specification allows for your program to crash, your computer to catch fire, or the moon to come crashing into the earth.

Now let’s see how the error looks under valgrind:

$ /home/dmw/src/cpython/python  -m pip install pytest
$ PYTHONPATH=. valgrind /home/dmw/src/cpython/python -m pytest tests/txn_test.py
==29422== Memcheck, a memory error detector
==29422== Copyright (C) 2002-2013, and GNU GPL'd, by Julian Seward et al.
==29422== Using Valgrind-3.10.0 and LibVEX; rerun with -h for copyright info
==29422== Command: /home/dmw/src/cpython/python -m pytest tests/txn_test.py
==29422==
=============================================================================== test session starts ================================================================================
platform linux2 -- Python 2.7.12+, pytest-3.0.3, py-1.4.31, pluggy-0.4.0
rootdir: /home/dmw/src/py-lmdb, inifile:
collected 46 items

tests/txn_test.py ==29422== Conditional jump or move depends on uninitialised value(s)
==29422==    at 0x4C30759: setenv (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==29422==    by 0xD1A152F: trans_put (cpython.c:3271)
==29422==    by 0x562798: PyCFunction_Call (methodobject.c:85)
==29422==    by 0x4D6457: call_function (ceval.c:4350)
==29422==    by 0x4D1281: PyEval_EvalFrameEx (ceval.c:2987)
==29422==    by 0x4D3C57: PyEval_EvalCodeEx (ceval.c:3582)
==29422==    by 0x4D6A31: fast_function (ceval.c:4445)
==29422==    by 0x4D6632: call_function (ceval.c:4370)
==29422==    by 0x4D1281: PyEval_EvalFrameEx (ceval.c:2987)
==29422==    by 0x4D3C57: PyEval_EvalCodeEx (ceval.c:3582)
==29422==    by 0x55FE44: function_call (funcobject.c:523)
==29422==    by 0x420F28: PyObject_Call (abstract.c:2547)
==29422==

From the stack trace, we can see cpython.c:3271 is mentioned, corresponding to the source line where our erroneous setenv() call appears. It is very obvious from this output that uninitialized data was accessed, and who accessed it.

valgrind –db-attach

There is one final crucial flag to know about for valgrind, and that is --db-attach. This causes valgrind to immediately halt the Python interpreter when an error is detected, and drop you into a gdb debugger shell, where you can inspect the program using a regular debugger with the exact state of memory preserved at the point where the error occurred.

Don’t forget to apt-get install gdb before trying it out.

Happy debugging!

Valgrind catches a huge variety of memory violations, this simple example only demonstrates one. A full tutorial on these errors would be hard to write, as I doubt I’ve even seen them all. As always, better understanding comes through practice, so next you catch a Python segmentation fault, perhaps try this route to see if you can diagnose or fix it for yourself.


Viewing all articles
Browse latest Browse all 22462

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>