During a code robustness audit using a static analysis tool tailored for Python C-API contracts, we identified two edge-case missing exception checks in coverage/ctracer/tracer.c that could lead to interpreter crashes or SystemError under specific conditions.
This PR addresses both issues to ensure graceful exception propagation.
1. Potential Null Pointer Dereference in Tracer_call
Issue:
In Tracer_call, there is a comment noting: /* In Python, the what argument is a string, we need to find an int for the C function. */. While it is true that under normal execution, sys.settrace will only pass standard ASCII event names (like "call", "return"), the tracer object itself is exposed to the Python user space and can be invoked directly (e.g., tracer_instance(frame, "non_ascii_string", arg)).
If a non-ASCII string is passed (or if extreme memory allocation fails), PyUnicode_AsASCIIString(what_str) returns NULL and sets a UnicodeEncodeError. Currently, this NULL return is blindly consumed by PyBytes_AS_STRING() and Py_DECREF(), triggering a deterministic Segmentation Fault.
Why this matters (CI Resilience & Supply Chain):
As coverage.py is a foundational tool in almost every Python CI/CD pipeline, resilience against malformed inputs is critical. In modern complex test suites, a rogue mock, a buggy third-party plugin, or a compromised package in the software supply chain could inadvertently (or maliciously) feed poisoned data to the tracer callbacks. A Segmentation Fault immediately kills the CI worker process without any Python traceback, making debugging a nightmare for developers.
Fix:
By adding a simple NULL check before macro expansion, we ensure that the boundary between C and Python remains robust. If an invalid string is passed, the C-extension now safely steps back (goto done) and propagates the UnicodeEncodeError, allowing the test suite to fail gracefully with a readable traceback.
2. Tri-state Boolean Confusion and Transactional State Tearing in CTracer_start
Issue:
In CTracer_start, the evaluation of trace_arcs uses PyObject_IsTrue() embedded in a C logical && expression.
According to the C-API contract, PyObject_IsTrue() returns -1 on error. However, C's logical && evaluates -1 as true. This causes:
State corruption: tracing_arcs is incorrectly set to 1.
Exception suppression: Returning a non-NULL pointer (PyObject *)self with an active exception forces a generic SystemError.
State tearing: Since PyEval_SetTrace is called before the evaluation, if an exception were correctly propagated, the interpreter would have a tracer set but atomic_store(&self->started, TRUE) would be skipped, leaving the internal state machine corrupted.
Fix:
We hoisted the PyObject_IsTrue() evaluation before any state mutations (i.e., PyEval_SetTrace and atomic_store). If it returns -1, we immediately return NULL to safely propagate the exception while ensuring transactional atomicity of the object's state.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Hi there! 👋
During a code robustness audit using a static analysis tool tailored for Python C-API contracts, we identified two edge-case missing exception checks in coverage/ctracer/tracer.c that could lead to interpreter crashes or SystemError under specific conditions.
This PR addresses both issues to ensure graceful exception propagation.
1. Potential Null Pointer Dereference in Tracer_call
Issue:
In Tracer_call, there is a comment noting: /* In Python, the what argument is a string, we need to find an int for the C function. */. While it is true that under normal execution, sys.settrace will only pass standard ASCII event names (like "call", "return"), the tracer object itself is exposed to the Python user space and can be invoked directly (e.g., tracer_instance(frame, "non_ascii_string", arg)).
If a non-ASCII string is passed (or if extreme memory allocation fails), PyUnicode_AsASCIIString(what_str) returns NULL and sets a UnicodeEncodeError. Currently, this NULL return is blindly consumed by PyBytes_AS_STRING() and Py_DECREF(), triggering a deterministic Segmentation Fault.
Why this matters (CI Resilience & Supply Chain):
As coverage.py is a foundational tool in almost every Python CI/CD pipeline, resilience against malformed inputs is critical. In modern complex test suites, a rogue mock, a buggy third-party plugin, or a compromised package in the software supply chain could inadvertently (or maliciously) feed poisoned data to the tracer callbacks. A Segmentation Fault immediately kills the CI worker process without any Python traceback, making debugging a nightmare for developers.
Fix:
By adding a simple NULL check before macro expansion, we ensure that the boundary between C and Python remains robust. If an invalid string is passed, the C-extension now safely steps back (goto done) and propagates the UnicodeEncodeError, allowing the test suite to fail gracefully with a readable traceback.
2. Tri-state Boolean Confusion and Transactional State Tearing in CTracer_start
Issue:
In CTracer_start, the evaluation of trace_arcs uses PyObject_IsTrue() embedded in a C logical && expression.
According to the C-API contract, PyObject_IsTrue() returns -1 on error. However, C's logical && evaluates -1 as true. This causes:
Fix:
We hoisted the PyObject_IsTrue() evaluation before any state mutations (i.e., PyEval_SetTrace and atomic_store). If it returns -1, we immediately return NULL to safely propagate the exception while ensuring transactional atomicity of the object's state.