Skip to content

Commit 8dc7a48

Browse files
committed
gh-140739: Fix crashes from corrupted remote memory
The remote debugging module reads memory from another Python process which can be modified or freed at any time due to race conditions. When garbage data is read, various code paths could cause SIGSEGV crashes in the profiler process itself rather than gracefully rejecting the sample. Add bounds checking and validation for data read from remote memory: linetable parsing now checks buffer bounds, PyLong reading validates digit count, stack chunk sizes are bounded, set iteration limits table size, task pointer arithmetic checks for underflow, TLBC index is validated against array bounds, and thread list iteration detects cycles. All cases now reject the sample with an exception instead of crashing or looping forever.
1 parent cf6758f commit 8dc7a48

File tree

6 files changed

+156
-36
lines changed

6 files changed

+156
-36
lines changed

Modules/_remote_debugging/_remote_debugging.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,11 @@ typedef enum _WIN32_THREADSTATE {
140140
#define SIZEOF_GC_RUNTIME_STATE sizeof(struct _gc_runtime_state)
141141
#define SIZEOF_INTERPRETER_STATE sizeof(PyInterpreterState)
142142

143+
/* Maximum sizes for validation to prevent buffer overflows from corrupted data */
144+
#define MAX_STACK_CHUNK_SIZE (16 * 1024 * 1024) /* 16 MB max for stack chunks */
145+
#define MAX_LONG_DIGITS 64 /* Allows values up to ~2^1920 */
146+
#define MAX_SET_TABLE_SIZE (1 << 20) /* 1 million entries max for set iteration */
147+
143148
#ifndef MAX
144149
#define MAX(a, b) ((a) > (b) ? (a) : (b))
145150
#endif
@@ -451,6 +456,7 @@ extern PyObject *make_frame_info(
451456
extern bool parse_linetable(
452457
const uintptr_t addrq,
453458
const char* linetable,
459+
Py_ssize_t linetable_size,
454460
int firstlineno,
455461
LocationInfo* info
456462
);

Modules/_remote_debugging/asyncio.c

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,17 @@ iterate_set_entries(
112112
}
113113

114114
Py_ssize_t num_els = GET_MEMBER(Py_ssize_t, set_object, unwinder->debug_offsets.set_object.used);
115-
Py_ssize_t set_len = GET_MEMBER(Py_ssize_t, set_object, unwinder->debug_offsets.set_object.mask) + 1;
115+
Py_ssize_t mask = GET_MEMBER(Py_ssize_t, set_object, unwinder->debug_offsets.set_object.mask);
116116
uintptr_t table_ptr = GET_MEMBER(uintptr_t, set_object, unwinder->debug_offsets.set_object.table);
117117

118+
// Validate mask and num_els to prevent huge loop iterations from garbage data
119+
if (mask < 0 || mask >= MAX_SET_TABLE_SIZE || num_els < 0 || num_els > mask + 1) {
120+
set_exception_cause(unwinder, PyExc_RuntimeError,
121+
"Invalid set object (corrupted remote memory)");
122+
return -1;
123+
}
124+
Py_ssize_t set_len = mask + 1;
125+
118126
Py_ssize_t i = 0;
119127
Py_ssize_t els = 0;
120128
while (i < set_len && els < num_els) {
@@ -812,25 +820,32 @@ append_awaited_by_for_thread(
812820
return -1;
813821
}
814822

815-
if (GET_MEMBER(uintptr_t, task_node, unwinder->debug_offsets.llist_node.next) == 0) {
823+
uintptr_t next_node = GET_MEMBER(uintptr_t, task_node, unwinder->debug_offsets.llist_node.next);
824+
if (next_node == 0) {
816825
PyErr_SetString(PyExc_RuntimeError,
817826
"Invalid linked list structure reading remote memory");
818827
set_exception_cause(unwinder, PyExc_RuntimeError, "NULL pointer in task linked list");
819828
return -1;
820829
}
821830

822-
uintptr_t task_addr = (uintptr_t)GET_MEMBER(uintptr_t, task_node, unwinder->debug_offsets.llist_node.next)
823-
- (uintptr_t)unwinder->async_debug_offsets.asyncio_task_object.task_node;
831+
// Validate next_node to prevent underflow when computing task_addr
832+
uintptr_t task_node_offset = (uintptr_t)unwinder->async_debug_offsets.asyncio_task_object.task_node;
833+
if (next_node < task_node_offset) {
834+
set_exception_cause(unwinder, PyExc_RuntimeError,
835+
"Invalid task node pointer (corrupted remote memory)");
836+
return -1;
837+
}
838+
uintptr_t task_addr = next_node - task_node_offset;
824839

825840
if (process_single_task_node(unwinder, task_addr, NULL, result) < 0) {
826841
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to process task node in awaited_by");
827842
return -1;
828843
}
829844

830-
// Read next node
845+
// Read next node (use already-validated next_node)
831846
if (_Py_RemoteDebug_PagedReadRemoteMemory(
832847
&unwinder->handle,
833-
(uintptr_t)GET_MEMBER(uintptr_t, task_node, unwinder->debug_offsets.llist_node.next),
848+
next_node,
834849
sizeof(task_node),
835850
task_node) < 0) {
836851
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read next task node in awaited_by");

Modules/_remote_debugging/code_objects.c

Lines changed: 91 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -123,44 +123,74 @@ cache_tlbc_array(RemoteUnwinderObject *unwinder, uintptr_t code_addr, uintptr_t
123123
* LINE TABLE PARSING FUNCTIONS
124124
* ============================================================================ */
125125

126+
// Inline helper for bounds-checked byte reading (no function call overhead)
127+
static inline int
128+
read_byte(const uint8_t **ptr, const uint8_t *end, uint8_t *out)
129+
{
130+
if (*ptr >= end) {
131+
return -1;
132+
}
133+
*out = *(*ptr)++;
134+
return 0;
135+
}
136+
126137
static int
127-
scan_varint(const uint8_t **ptr)
138+
scan_varint(const uint8_t **ptr, const uint8_t *end)
128139
{
129-
unsigned int read = **ptr;
130-
*ptr = *ptr + 1;
140+
uint8_t read;
141+
if (read_byte(ptr, end, &read) < 0) {
142+
return -1;
143+
}
131144
unsigned int val = read & 63;
132145
unsigned int shift = 0;
133146
while (read & 64) {
134-
read = **ptr;
135-
*ptr = *ptr + 1;
147+
if (read_byte(ptr, end, &read) < 0) {
148+
return -1;
149+
}
136150
shift += 6;
151+
// Prevent infinite loop on malformed data (shift overflow)
152+
if (shift > 28) {
153+
return -1;
154+
}
137155
val |= (read & 63) << shift;
138156
}
139-
return val;
157+
return (int)val;
140158
}
141159

142160
static int
143-
scan_signed_varint(const uint8_t **ptr)
161+
scan_signed_varint(const uint8_t **ptr, const uint8_t *end)
144162
{
145-
unsigned int uval = scan_varint(ptr);
163+
int uval = scan_varint(ptr, end);
164+
if (uval < 0) {
165+
return INT_MIN; // Error sentinel (valid signed varints won't be INT_MIN)
166+
}
146167
if (uval & 1) {
147-
return -(int)(uval >> 1);
168+
return -(int)((unsigned int)uval >> 1);
148169
}
149170
else {
150-
return uval >> 1;
171+
return (int)((unsigned int)uval >> 1);
151172
}
152173
}
153174

154175
bool
155-
parse_linetable(const uintptr_t addrq, const char* linetable, int firstlineno, LocationInfo* info)
176+
parse_linetable(const uintptr_t addrq, const char* linetable, Py_ssize_t linetable_size,
177+
int firstlineno, LocationInfo* info)
156178
{
179+
// Reject garbage: zero or negative size
180+
if (linetable_size <= 0) {
181+
return false;
182+
}
183+
157184
const uint8_t* ptr = (const uint8_t*)(linetable);
185+
const uint8_t* end = ptr + linetable_size;
158186
uintptr_t addr = 0;
159187
int computed_line = firstlineno; // Running accumulator, separate from output
188+
int val; // Temporary for varint results
189+
uint8_t byte; // Temporary for byte reads
160190
const size_t MAX_LINETABLE_ENTRIES = 65536;
161191
size_t entry_count = 0;
162192

163-
while (*ptr != '\0' && entry_count < MAX_LINETABLE_ENTRIES) {
193+
while (ptr < end && *ptr != '\0' && entry_count < MAX_LINETABLE_ENTRIES) {
164194
entry_count++;
165195
uint8_t first_byte = *(ptr++);
166196
uint8_t code = (first_byte >> 3) & 15;
@@ -173,14 +203,34 @@ parse_linetable(const uintptr_t addrq, const char* linetable, int firstlineno, L
173203
info->column = info->end_column = -1;
174204
break;
175205
case PY_CODE_LOCATION_INFO_LONG:
176-
computed_line += scan_signed_varint(&ptr);
206+
val = scan_signed_varint(&ptr, end);
207+
if (val == INT_MIN) {
208+
return false;
209+
}
210+
computed_line += val;
177211
info->lineno = computed_line;
178-
info->end_lineno = computed_line + scan_varint(&ptr);
179-
info->column = scan_varint(&ptr) - 1;
180-
info->end_column = scan_varint(&ptr) - 1;
212+
val = scan_varint(&ptr, end);
213+
if (val < 0) {
214+
return false;
215+
}
216+
info->end_lineno = computed_line + val;
217+
val = scan_varint(&ptr, end);
218+
if (val < 0) {
219+
return false;
220+
}
221+
info->column = val - 1;
222+
val = scan_varint(&ptr, end);
223+
if (val < 0) {
224+
return false;
225+
}
226+
info->end_column = val - 1;
181227
break;
182228
case PY_CODE_LOCATION_INFO_NO_COLUMNS:
183-
computed_line += scan_signed_varint(&ptr);
229+
val = scan_signed_varint(&ptr, end);
230+
if (val == INT_MIN) {
231+
return false;
232+
}
233+
computed_line += val;
184234
info->lineno = info->end_lineno = computed_line;
185235
info->column = info->end_column = -1;
186236
break;
@@ -189,17 +239,25 @@ parse_linetable(const uintptr_t addrq, const char* linetable, int firstlineno, L
189239
case PY_CODE_LOCATION_INFO_ONE_LINE2:
190240
computed_line += code - 10;
191241
info->lineno = info->end_lineno = computed_line;
192-
info->column = *(ptr++);
193-
info->end_column = *(ptr++);
242+
if (read_byte(&ptr, end, &byte) < 0) {
243+
return false;
244+
}
245+
info->column = byte;
246+
if (read_byte(&ptr, end, &byte) < 0) {
247+
return false;
248+
}
249+
info->end_column = byte;
194250
break;
195251
default: {
196-
uint8_t second_byte = *(ptr++);
197-
if ((second_byte & 128) != 0) {
252+
if (read_byte(&ptr, end, &byte) < 0) {
253+
return false;
254+
}
255+
if ((byte & 128) != 0) {
198256
return false;
199257
}
200258
info->lineno = info->end_lineno = computed_line;
201-
info->column = code << 3 | (second_byte >> 4);
202-
info->end_column = info->column + (second_byte & 15);
259+
info->column = code << 3 | (byte >> 4);
260+
info->end_column = info->column + (byte & 15);
203261
break;
204262
}
205263
}
@@ -384,8 +442,14 @@ parse_code_object(RemoteUnwinderObject *unwinder,
384442
tlbc_entry = get_tlbc_cache_entry(unwinder, real_address, unwinder->tlbc_generation);
385443
}
386444

387-
if (tlbc_entry && ctx->tlbc_index < tlbc_entry->tlbc_array_size) {
388-
assert(ctx->tlbc_index >= 0);
445+
// Validate tlbc_index and check TLBC cache
446+
if (tlbc_entry) {
447+
// Validate index bounds (also catches negative values since tlbc_index is signed)
448+
if (ctx->tlbc_index < 0 || ctx->tlbc_index >= tlbc_entry->tlbc_array_size) {
449+
set_exception_cause(unwinder, PyExc_RuntimeError,
450+
"Invalid tlbc_index (corrupted remote memory)");
451+
goto error;
452+
}
389453
assert(tlbc_entry->tlbc_array_size > 0);
390454
// Use cached TLBC data
391455
uintptr_t *entries = (uintptr_t *)((char *)tlbc_entry->tlbc_array + sizeof(Py_ssize_t));
@@ -398,7 +462,7 @@ parse_code_object(RemoteUnwinderObject *unwinder,
398462
}
399463
}
400464

401-
// Fall back to main bytecode
465+
// Fall back to main bytecode (no tlbc_entry or tlbc_bytecode_addr was 0)
402466
addrq = (uint16_t *)ip - (uint16_t *)meta->addr_code_adaptive;
403467

404468
done_tlbc:
@@ -409,6 +473,7 @@ parse_code_object(RemoteUnwinderObject *unwinder,
409473
; // Empty statement to avoid C23 extension warning
410474
LocationInfo info = {0};
411475
bool ok = parse_linetable(addrq, PyBytes_AS_STRING(meta->linetable),
476+
PyBytes_GET_SIZE(meta->linetable),
412477
meta->first_lineno, &info);
413478
if (!ok) {
414479
info.lineno = -1;

Modules/_remote_debugging/frames.c

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,15 @@ process_single_stack_chunk(
4545
// Check actual size and reread if necessary
4646
size_t actual_size = GET_MEMBER(size_t, this_chunk, offsetof(_PyStackChunk, size));
4747
if (actual_size != current_size) {
48+
// Validate size: reject garbage (too small or unreasonably large)
49+
// Size must be at least enough for the header and reasonably bounded
50+
if (actual_size <= offsetof(_PyStackChunk, data) || actual_size > MAX_STACK_CHUNK_SIZE) {
51+
PyMem_RawFree(this_chunk);
52+
set_exception_cause(unwinder, PyExc_RuntimeError,
53+
"Invalid stack chunk size (corrupted remote memory)");
54+
return -1;
55+
}
56+
4857
this_chunk = PyMem_RawRealloc(this_chunk, actual_size);
4958
if (!this_chunk) {
5059
PyErr_NoMemory();
@@ -129,7 +138,11 @@ void *
129138
find_frame_in_chunks(StackChunkList *chunks, uintptr_t remote_ptr)
130139
{
131140
for (size_t i = 0; i < chunks->count; ++i) {
132-
assert(chunks->chunks[i].size > offsetof(_PyStackChunk, data));
141+
// Validate size: reject garbage that would cause underflow
142+
if (chunks->chunks[i].size <= offsetof(_PyStackChunk, data)) {
143+
// Skip this chunk - corrupted size from remote memory
144+
continue;
145+
}
133146
uintptr_t base = chunks->chunks[i].remote_addr + offsetof(_PyStackChunk, data);
134147
size_t payload = chunks->chunks[i].size - offsetof(_PyStackChunk, data);
135148

Modules/_remote_debugging/module.c

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -584,13 +584,22 @@ _remote_debugging_RemoteUnwinder_get_stack_trace_impl(RemoteUnwinderObject *self
584584
}
585585

586586
while (current_tstate != 0) {
587+
uintptr_t prev_tstate = current_tstate;
587588
PyObject* frame_info = unwind_stack_for_thread(self, &current_tstate,
588589
gil_holder_tstate,
589590
gc_frame);
590591
if (!frame_info) {
591592
// Check if this was an intentional skip due to mode-based filtering
592593
if ((self->mode == PROFILING_MODE_CPU || self->mode == PROFILING_MODE_GIL ||
593594
self->mode == PROFILING_MODE_EXCEPTION) && !PyErr_Occurred()) {
595+
// Detect cycle: if current_tstate didn't advance, we have corrupted data
596+
if (current_tstate == prev_tstate) {
597+
Py_DECREF(interpreter_threads);
598+
set_exception_cause(self, PyExc_RuntimeError,
599+
"Thread list cycle detected (corrupted remote memory)");
600+
Py_CLEAR(result);
601+
goto exit;
602+
}
594603
// Thread was skipped due to mode filtering, continue to next thread
595604
continue;
596605
}

Modules/_remote_debugging/object_reading.c

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -194,17 +194,29 @@ read_py_long(
194194
return 0;
195195
}
196196

197-
// If the long object has inline digits, use them directly
197+
// Validate size: reject garbage (negative or unreasonably large)
198+
if (size < 0 || size > MAX_LONG_DIGITS) {
199+
set_exception_cause(unwinder, PyExc_RuntimeError,
200+
"Invalid PyLong size (corrupted remote memory)");
201+
return -1;
202+
}
203+
204+
// Calculate how many digits fit inline in our local buffer
205+
Py_ssize_t ob_digit_offset = unwinder->debug_offsets.long_object.ob_digit;
206+
Py_ssize_t inline_digits_space = SIZEOF_LONG_OBJ - ob_digit_offset;
207+
Py_ssize_t max_inline_digits = inline_digits_space / (Py_ssize_t)sizeof(digit);
208+
209+
// If the long object has inline digits that fit in our buffer, use them directly
198210
digit *digits;
199-
if (size <= _PY_NSMALLNEGINTS + _PY_NSMALLPOSINTS) {
211+
if (size <= max_inline_digits && size <= _PY_NSMALLNEGINTS + _PY_NSMALLPOSINTS) {
200212
// For small integers, digits are inline in the long_value.ob_digit array
201213
digits = (digit *)PyMem_RawMalloc(size * sizeof(digit));
202214
if (!digits) {
203215
PyErr_NoMemory();
204216
set_exception_cause(unwinder, PyExc_MemoryError, "Failed to allocate digits for small PyLong");
205217
return -1;
206218
}
207-
memcpy(digits, long_obj + unwinder->debug_offsets.long_object.ob_digit, size * sizeof(digit));
219+
memcpy(digits, long_obj + ob_digit_offset, size * sizeof(digit));
208220
} else {
209221
// For larger integers, we need to read the digits separately
210222
digits = (digit *)PyMem_RawMalloc(size * sizeof(digit));

0 commit comments

Comments
 (0)