gh-157227: _remote_debugging: Validate lists and tuples in write_sample - #157228
gh-157227: _remote_debugging: Validate lists and tuples in write_sample#157228maurycy wants to merge 2 commits into
_remote_debugging: Validate lists and tuples in write_sample#157228Conversation
| } \ | ||
| } while (0) | ||
|
|
||
| #define CHECK_TUPLE_ITEMS(obj, n) do { \ |
There was a problem hiding this comment.
Truth be told, we reinvent similar check over and over:
cpython/Objects/memoryobject.c
Lines 217 to 222 in 9a75080
Lines 26 to 33 in 9a75080
Sometimes without a macro:
cpython/Modules/_io/stringio.c
Lines 922 to 930 in 9a75080
Lines 450 to 465 in 9a75080
| Py_ssize_t num_interpreters = PyList_GET_SIZE(stack_frames); | ||
| for (Py_ssize_t i = 0; i < num_interpreters; i++) { | ||
| PyObject *interp_info = PyList_GET_ITEM(stack_frames, i); | ||
| CHECK_TUPLE_ITEMS(interp_info, 2); |
There was a problem hiding this comment.
We validate as we go, so a bad thread in the middle leaves the earlier threads already written and total_samples bumped even though write_sample raised. Should we walk and validate the whole structure first so this is all-or-nothing? Otherwise the caller has no way to recover from the TypeError.
|
|
||
| PyObject *interp_id_obj = PyStructSequence_GET_ITEM(interp_info, 0); | ||
| PyObject *threads = PyStructSequence_GET_ITEM(interp_info, 1); | ||
| CHECK_LIST(threads); |
There was a problem hiding this comment.
This narrows the hole but does not close it. PyLong_AsLong(status_obj) goes through _PyNumber_Index, so an __index__ that mutates the same list re-enters us while we still hold a cached size and a borrowed thread_info:
threads = []
class Evil:
def __index__(self):
del threads[:]
return 0
threads.extend([(1, Evil(), []), (2, 0, [])])
_remote_debugging.BinaryWriter("/tmp/o.bin", 1000, 0, compression=0).write_sample([(0, threads)], 2000)This still segfaults on PyList_GET_ITEM(threads, 1) because del threads[:] frees ob_item. writer_intern_string has the same problem via PyObject_Hash. If we want to claim write_sample is safe against arbitrary input we need strong references to the containers (or to re-read the sizes), not only a type check up front. Happy to take this as a follow-up, but then let's not close the issue with this PR.
| int32_t end_column = LOCATION_NOT_AVAILABLE; | ||
|
|
||
| if (location != Py_None) { | ||
| CHECK_TUPLE_ITEMS(location, 4); |
There was a problem hiding this comment.
normalize_location() in Lib/profiling/sampling/collector.py accepts an int location (a bare lineno) and binary_collector.py hands stack_frames straight to write_sample, so this now raises where the other collectors work. Do we want a PyLong_Check(location) case here too, or is that branch in normalize_location() dead and we should drop it instead?
See #157227 for more details
Basically,
BinaryWriter.write_sample()only checks whetherstack_framesis a list, and doesPyList_GET_ITEM()/PyStructSequence_GET_ITEM()all the way. The PR adds a check for these containers.Now it's graceful:
instead of
segmentation fault._remote_debugging:BinaryWriter.write_sample()trivial segfault #157227