Skip to content

Commit d3d4cf9

Browse files
authored
gh-140739: Fix crashes from corrupted remote memory (#143190)
1 parent de22e71 commit d3d4cf9

File tree

7 files changed

+150
-34
lines changed

7 files changed

+150
-34
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix several crashes due to reading invalid memory in the new Tachyon
2+
sampling profiler. Patch by Pablo Galindo.

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: 13 additions & 4 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,14 +820,15 @@ 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)
831+
uintptr_t task_addr = next_node
823832
- (uintptr_t)unwinder->async_debug_offsets.asyncio_task_object.task_node;
824833

825834
if (process_single_task_node(unwinder, task_addr, NULL, result) < 0) {
@@ -830,7 +839,7 @@ append_awaited_by_for_thread(
830839
// Read next node
831840
if (_Py_RemoteDebug_PagedReadRemoteMemory(
832841
&unwinder->handle,
833-
(uintptr_t)GET_MEMBER(uintptr_t, task_node, unwinder->debug_offsets.llist_node.next),
842+
next_node,
834843
sizeof(task_node),
835844
task_node) < 0) {
836845
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)