[prev in list] [next in list] [prev in thread] [next in thread]
List: gcc-python-plugin-commits
Subject: [gcc-python-plugin] cpychecker: revamp of the documentation
From: dmalcolm () fedoraproject ! org (dmalcolm)
Date: 2011-10-26 20:59:44
Message-ID: 20111026205944.57E6A120387 () lists ! fedorahosted ! org
[Download RAW message or body]
commit 3eaf0eb6a7ed791c2e7c478aba6263d94f0848bf
Author: David Malcolm <dmalcolm at redhat.com>
Date: Wed Oct 26 16:59:16 2011 -0400
cpychecker: revamp of the documentation
docs/cpychecker.rst | 320 +++++++++++++++++++++++++++++++++----
docs/sample-html-error-report.png | Bin 0 -> 72902 bytes
2 files changed, 292 insertions(+), 28 deletions(-)
---
diff --git a/docs/cpychecker.rst b/docs/cpychecker.rst
index 2503c66..9c4a4de 100644
--- a/docs/cpychecker.rst
+++ b/docs/cpychecker.rst
@@ -18,59 +18,315 @@
Usage example: a static analysis tool for CPython extension code
================================================================
+.. note:: This code is under heavy development, and still contains bugs. It
+ is not unusual to see Python tracebacks when running the checker. You
+ should verify what the checker reports before acting on it: it could be
+ wrong.
+
An example of using the plugin is a static analysis tool I'm working on which
checks the C source of CPython extension modules for common coding errors.
-This code is under heavy development, and is not yet ready to be used.
-
This was one of my main motivations for writing the GCC plugin, and I often
need to extend the plugin to support this use case.
For this reason, the checker is embedded within the gcc-python source tree
itself for now:
- * `cpychecker.py` is a short top-level script, which will eventually be
- invoked like this::
-
- gcc -fplugin=python.so -fplugin-arg-python-script=cpychecker.py OTHER_ARGS
+ * `gcc-with-cpychecker` is a harness script, which invokes GCC, adding
+ the arguments necessary to use the Python plugin, using the
+ `libcpychecker` Python code
* the `libcpychecker` subdirectory contains the code that does the actual
work
- * various test cases
+ * various test cases (in the source tree, below `tests/cpychecker`)
-Here's a list of C coding bugs I intend for the tool to detect (each one
-followed by its current status):
+So it should be possible to use the checker on arbitrary CPython extension
+code by replacing "gcc" with "gcc-with-cpychecker" in your build with
+something like::
- * lack of error handling (i.e. assuming that calls to the Python API
- succeed) [AT EARLY, BUGGY, STAGE]
+ make CC=/path/to/built/plugin/gcc-with-cpychecker
- * reference-counting errors: [AT EARLY, BUGGY, STAGE]
+You may need to supply an absolute path, especially if the "make" recursively
+invokes "make" within subdirectories (thus having a different working
+directory).
- * missing Py_INCREF/Py_DECREF
+(TODO: some way of integrating the checker with `distutils
+<http://docs.python.org/library/distutils.html>`_)
- * leaking references to objects, not having enough references to an object
+Reference-count checking
+------------------------
+The checker attempts to analyze all possible paths through each function,
+tracking the various PyObject* objects encountered.
- * using an object after a point where it might have been deleted
+For each path through the function and PyObject*, it determines what the
+reference count ought to be at the end of the function, issuing an error report
+for any that are incorrect.
- * tp_traverse errors (which can mess up the garbage collector); missing it
- altogether, or omitting fields [NOT IMPLEMENTED YET]
+The error report is in two forms: the classic textual output to GCC's standard
+error stream.
+
+For example, given:
+
+.. code-block:: c
+
+ PyObject *
+ test(PyObject *self, PyObject *args)
+ {
+ PyObject *list;
+ PyObject *item;
+ list = PyList_New(1);
+ if (!list)
+ return NULL;
+ item = PyLong_FromLong(42);
+ /* This error handling is incorrect: it's missing an
+ invocation of Py_DECREF(list): */
+ if (!item)
+ return NULL;
+ /* This steals a reference to item; item is not leaked when we get here: */
+ PyList_SetItem(list, 0, item);
+ return list;
+ }
+
+the checker emits these messages to stderr::
+
+ input.c: In function 'test':
+ input.c:38:1: error: ob_refcnt of '*list' is 1 too high
+ input.c:38:1: note: was expecting final ob_refcnt to be N + 0 (for some unknown \
N) + input.c:38:1: note: but final ob_refcnt is N + 1
+ input.c:27:10: note: PyListObject allocated at: list = PyList_New(1);
+ input.c:27:10: note: when PyList_New() succeeds at: list = PyList_New(1);
+ input.c:27:10: note: ob_refcnt is now refs: 1 + N where N >= 0
+ input.c:28:8: note: taking False path at: if (!list)
+ input.c:30:10: note: reaching: item = PyLong_FromLong(42);
+ input.c:30:10: note: when PyLong_FromLong() fails at: item = \
PyLong_FromLong(42); + input.c:33:8: note: taking True path at: if (!item)
+ input.c:34:9: note: reaching: return NULL;
+ input.c:38:1: note: returning
+ input.c:24:1: note: graphical error report for function 'test' written out to \
'input.c.test-refcount-errors.html' +
+and also an HTML report indicating the flow through the function, in graphical
+form:
+
+ .. figure:: sample-html-error-report.png
+ :alt: screenshot of the HTML report
+
+The HTML report is intended to be relatively self-contained, and thus easy to
+attached to bug tracking systems: it embeds its own CSS inline, and references
+the JavaScript it uses via URLs to the web.
+
+.. note:: The arrow graphics in the HTML form of the report are added by using
+ the JSPlumb JavaScript library to generate HTML 5 <canvas> elements. You
+ may need a relatively modern browser to see them.
+
+.. note:: The checker tracks reference counts in an abstract way, in two parts:
+ a part of the reference count that it knows about within the context of the
+ function, along with a second part: all of the other references held by the
+ rest of the program.
+
+ For example, in a call to PyInt_FromLong(0), it is assumed that if the call
+ succeeds, the object has a reference count of 1 + N, where N is some unknown
+ amount of other references held by the rest of the program. The checker
+ knows that N >= 0.
+
+ If the object is then stored in an opaque container which is known to
+ increment the reference count, the checker can say that the reference count
+ is then 1 + (N+1).
+
+ If the function then decrements the reference count (to finish transferring
+ the reference to the opaque container), the checker now treats the object as
+ having a reference count of 0 + (N+1): it no longer owns any references on
+ the object, but the reference count is actually unchanged relative to the
+ original 1 + N amount. It also knows, given that N >= 0 that the actual
+ reference count is >= 1, and thus the object won't (yet) be deallocated.
+
+Assumptions and configuration
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+For any function returning a PyObject*, it assumes that the PyObject* should be
+either a new reference to an object, or NULL (with an exception set) - the
+function's caller should "own" a reference to that object. For all
+other PyObject*, it assumes that there should be no references owned by the
+function when the function terminates.
+
+It will assume this behavior for any function (or call through a function
+pointer) that returns a PyObject*.
+
+It is possible to override this behavior using custom compiler attributes:
+
+.. code-block:: c
+
+ /* The checker automatically defines this preprocessor name when creating
+ the custom attribute: */
+ #if defined(WITH_CPYCHECKER_RETURNS_BORROWED_REF_ATTRIBUTE)
+ #define CPYCHECKER_RETURNS_BORROWED_REF \
+ __attribute__((cpychecker_returns_borrowed_ref))
+ #else
+ #define CPYCHECKER_RETURNS_BORROWED_REF
+ #endif
+
+ PyObject *foo(void)
+ CPYCHECKER_RETURNS_BORROWED_REF;
+
+Given the above, the checker will assume that invocations of ``foo()`` are
+returning a borrowed reference (or NULL), rather than a new reference, and
+will apply the same policy when verifying the implementation of ``foo()``
+itself.
+
+Error-handling checking
+-----------------------
+The checker has knowledge of much of the CPython C API, and will generate
+a trace tree containing many of the possible error paths. It will issue
+error reports for code that appears to not gracefully handle an error.
+
+(TODO: show example)
+
+As noted above, it assumes that any function that returns a PyObject* can
+return can either NULL (setting an exception), or a new reference. It knows
+about much of the other parts of the CPython C API, including many other
+functions that can fail.
+
+The checker will emit errors for various events:
+
+ * if it detects a dereferencing of a ``NULL`` value
+
+ * if a ``NULL`` value is erroneously passed to various CPython API
+ entrypoints which implicitly deference those arguments (which would lead
+ to a segmentation fault if that code path were executed).
+
+ * if it detects that an uninitialized local variable has been used
+
+ * if it detects access to an object that has been deallocated, or such an
+ object being returned::
+
+ input.c: In function 'test':
+ input.c:43:1: error: returning pointer to deallocated memory
+ input.c:29:15: note: when PyLong_FromLong() succeeds at: PyObject *tmp = \
PyLong_FromLong(0x1000); + input.c:31:8: note: taking False path at: if \
(!tmp) { + input.c:39:5: note: reaching: Py_DECREF(tmp);
+ input.c:39:5: note: when taking False path at: Py_DECREF(tmp);
+ input.c:39:5: note: reaching: Py_DECREF(tmp);
+ input.c:39:5: note: calling tp_dealloc on PyLongObject allocated at \
input.c:29 at: Py_DECREF(tmp); + input.c:42:5: note: reaching: return \
tmp; + input.c:43:1: note: returning
+ input.c:39:5: note: memory deallocated here
+ input.c:27:1: note: graphical error report for function \
'returning_dead_object' written out to 'input.c.test.html'
- * errors in PyArg_ParseTuple and friends:
+Errors in exception-handling
+----------------------------
+The checker keeps track of the per-thread exception state. It will issue a
+warning about any paths through functions returning a PyObject* that return
+NULL for which the per-thread exception state has not been set::
- * type mismatches e.g. `int` vs `long` (often leads to flaws on big-endian
- 64-bit architectures, where sizeof(int) != sizeof(long))
- [MOSTLY IMPLEMENTED FOR PyArg_Parse*, NOT IMPLEMENTED FOR Py_BuildValue]
+ input.c: In function 'test':
+ input.c:32:5: error: returning (PyObject*)NULL without setting an exception
- * reference-counting errors, for the cases where objects are returned with
- either new or borrowed references (depending on the format codes).
- [NOT IMPLEMENTED YET]
+(TODO: provide a way to mark a function as setting this state)
- * errors in exception handling, so that we can issue errors about e.g. not
- returning NULL if an exception is set, or returning NULL if an exception is
- not set [NOT IMPLEMENTED YET]
- * errors in GIL-handling [NOT IMPLEMENTED YET]
+Format string checking
+----------------------
+
+The checker will analyze some `Python APIs that take format strings
+<http://docs.python.org/c-api/arg.html>`_ and detect mismatches between the
+number and types of arguments that are passed in, as compared with those
+described by the format string.
+
+It currently verifies the arguments to the following API entrypoints:
+
+ * `PyArg_ParseTuple
+ <http://docs.python.org/c-api/arg.html#PyArg_ParseTuple>`_
+
+ * `PyArg_ParseTupleAndKeywords
+ <http://docs.python.org/c-api/arg.html#PyArg_ParseTupleAndKeywords>`_
+
+ * `Py_BuildValue
+ <http://docs.python.org/c-api/arg.html#Py_BuildValue>`_
+
+along with the variants that occur if you define `PY_SSIZE_T_CLEAN` before
+`#include <Python.h>`.
+
+For example, type mismatches between ``int`` vs ``long`` can lead to flaws
+when the code is compiled on big-endian 64-bit architectures, where
+``sizeof(int) != sizeof(long)`` and the in-memory layout of those types differs
+from what you might expect.
+
+The checker will also issue a warning if the list of keyword arguments in a
+call to PyArg_ParseTupleAndKeywords is not NULL-terminated.
+
+.. note:: All of the various "#" codes in these format strings are affected by
+ the presence of the macro `PY_SSIZE_T_CLEAN`. If the macro was defined
+ before including Python.h, the various lengths for these format codes are of
+ C type `Py_ssize_t` rather than `int`.
+
+ This behavior was clarified in the Python 3 version of the C API
+ documentation, though the Python 2 version of the API docs leave the matter
+ of which codes are affected somewhat ambiguous.
+
+ Nevertheless, the API *does* work this way in Python 2: all format codes
+ with a "#" do work this way.
+
+ Internally, the C preprocessor converts such function calls into invocations
+ of:
+
+ * `_PyArg_ParseTuple_SizeT`
+ * `_PyArg_ParseTupleAndKeywords_SizeT`
+
+ The checker handles this behavior correctly, by checking "#" codes in the
+ regular functions against `int` and those in the modified functions against
+ `Py_ssize_t`.
+
+
+Limitations and caveats
+-----------------------
+
+Compiling with the checker is significantly slower than with "vanilla" gcc.
+I have been focussing on correctness and features, rather than optimization.
+I hope that it will be possible to greatly speed up the checker via
+ahead-of-time compilation of the Python code (e.g. using Cython).
+
+The checker does not yet fully implement all of C: expect to see Python
+tracebacks when it encounters less common parts of the language. (We'll fix
+those bugs as we come to them)
+
+The checker has a rather simplistic way of tracking the flow through a
+function: it builds a tree of all possible traces of execution through a
+function. This brings with it some shortcomings:
+
+ * In order to guarantee that the analysis terminates, the checker will only
+ track the first time through any loop, and stop analysing that trace for
+ subsequent iterations. This appears to be good enough for detecting many
+ kinds of reference leaks, especially in simple wrapper code, but is clearly
+ suboptimal.
+
+ * In order to avoid combinatorial explosion, the checker will stop analyzing
+ a function once the trace tree gets sufficiently large. When it reaches
+ this cutoff, a warning is issued::
+
+ input.c: In function 'add_module_objects':
+ input.c:31:1: note: this function is too complicated for the reference-count \
checker to analyze +
+ * The checker doesn't yet match up similar traces, and so a single bug that
+ affects multiple traces in the trace tree can lead to duplicate error
+ reports.
+
+Only a subset of the CPython API has been modelled so far.
+
+
+Ideas for future tests
+----------------------
+
+Here's a list of some other C coding bugs I intend for the tool to detect:
+
+ * type-checking for PyMethodDef tables: the callbacks are typically cast
+ to PyCFunction, but the exact type needs to correspond to the flags
+ given (e.g. ``(METH_VARARGS | METH_KEYWORDS)`` implies a different function
+ signature to the default, and the C compiler has currently no way of
+ verifying that these agree with each other).
+
+ * tp_traverse errors (which can mess up the garbage collector); missing it
+ altogether, or omitting fields
+
+ * errors in GIL-handling
* lock/release mismatches
@@ -82,3 +338,11 @@ Ideas for other tests are most welcome (patches even more so!)
We will probably need various fallbacks and suppression modes for turning off
individual tests (perhaps pragmas, perhaps compile-line flags, etc)
+
+Reusing this code for other projects
+------------------------------------
+It may be possible to reuse the analysis engine from cpychecker for other
+kinds of analysis - hopefully the python-specific parts are relatively
+self-contained. Email the `gcc-python-plugin's mailing list
+<https://fedorahosted.org/mailman/listinfo/gcc-python-plugin/>`_ if you're
+interested in adding verifiers for other kinds of code.
diff --git a/docs/sample-html-error-report.png b/docs/sample-html-error-report.png
new file mode 100644
index 0000000..add7291
Binary files /dev/null and b/docs/sample-html-error-report.png differ
[prev in list] [next in list] [prev in thread] [next in thread]
Configure |
About |
News |
Add a list |
Sponsored by KoreLogic