From 8890707f40f886fac17cbd0a0c99ff3d7ffb73ab Mon Sep 17 00:00:00 2001 From: Pablo Galindo Salgado Date: Mon, 25 May 2026 23:22:46 +0100 Subject: [PATCH] [3.15] gh-149619: Harden _remote_debugging error paths (GH-150349) (cherry picked from commit a5be25d3bdc1b3cbc9638a3249c0e3db5a97ebc6) Co-authored-by: Pablo Galindo Salgado --- .../test_binary_format.py | 41 ++ Modules/_remote_debugging/_remote_debugging.h | 2 +- Modules/_remote_debugging/asyncio.c | 41 +- Modules/_remote_debugging/binary_io_reader.c | 72 +++- Modules/_remote_debugging/binary_io_writer.c | 42 +- Modules/_remote_debugging/code_objects.c | 2 - Modules/_remote_debugging/module.c | 3 + Modules/_remote_debugging/object_reading.c | 58 +-- Modules/_remote_debugging/subprocess.c | 51 ++- Modules/_remote_debugging/threads.c | 91 ++-- Python/remote_debug.h | 390 +++++++++++++----- 11 files changed, 597 insertions(+), 196 deletions(-) diff --git a/Lib/test/test_profiling/test_sampling_profiler/test_binary_format.py b/Lib/test/test_profiling/test_sampling_profiler/test_binary_format.py index 1fbb4e2d6c6fbb4..5efc60a92111754 100644 --- a/Lib/test/test_profiling/test_sampling_profiler/test_binary_format.py +++ b/Lib/test/test_profiling/test_sampling_profiler/test_binary_format.py @@ -975,7 +975,11 @@ def test_writer_total_samples_after_close_returns_zero(self): class TestBinaryFormatValidation(BinaryFormatTestBase): """Tests for malformed binary files.""" + HDR_OFF_SAMPLES = 28 HDR_OFF_THREADS = 32 + HDR_OFF_STR_TABLE = 36 + HDR_OFF_FRAME_TABLE = 44 + FILE_HEADER_PLACEHOLDER_SIZE = 64 def test_replay_rejects_more_threads_than_declared(self): """Replay rejects files with more unique threads than the header declares.""" @@ -1000,6 +1004,43 @@ def test_replay_rejects_more_threads_than_declared(self): "threads than declared in header (declared 1, found at least 2)", ) + def test_replay_rejects_sample_count_mismatch(self): + """Replay rejects files whose decoded samples disagree with the header.""" + samples = [[make_interpreter(0, [ + make_thread(1, [make_frame("sample.py", 10, "sample")]) + ])]] + filename = self.create_binary_file(samples, compression="none") + + with open(filename, "r+b") as raw: + raw.seek(self.HDR_OFF_SAMPLES) + raw.write(struct.pack("=I", 2)) + + with BinaryReader(filename) as reader: + self.assertEqual(reader.get_info()["sample_count"], 2) + with self.assertRaises(ValueError) as cm: + reader.replay_samples(RawCollector()) + self.assertEqual( + str(cm.exception), + "Sample count mismatch: header declares 2 samples " + "but replay decoded 1", + ) + + def test_replay_rejects_trailing_partial_sample_header(self): + """Replay rejects partial sample bytes instead of silently stopping.""" + filename = self.create_binary_file([], compression="none") + sample_data_end = self.FILE_HEADER_PLACEHOLDER_SIZE + 1 + + with open(filename, "r+b") as raw: + raw.seek(self.HDR_OFF_STR_TABLE) + raw.write(struct.pack("=Q", sample_data_end)) + raw.seek(self.HDR_OFF_FRAME_TABLE) + raw.write(struct.pack("=Q", sample_data_end)) + + with BinaryReader(filename) as reader: + with self.assertRaises(ValueError) as cm: + reader.replay_samples(RawCollector()) + self.assertEqual(str(cm.exception), "Truncated sample data: 1 trailing bytes") + class TestBinaryEncodings(BinaryFormatTestBase): """Tests specifically targeting different stack encodings.""" diff --git a/Modules/_remote_debugging/_remote_debugging.h b/Modules/_remote_debugging/_remote_debugging.h index d91ce54a18c813a..635e6e208902af5 100644 --- a/Modules/_remote_debugging/_remote_debugging.h +++ b/Modules/_remote_debugging/_remote_debugging.h @@ -180,7 +180,7 @@ typedef enum _WIN32_THREADSTATE { #define set_exception_cause(unwinder, exc_type, message) \ do { \ assert(PyErr_Occurred() && "function returned -1 without setting exception"); \ - if (unwinder->debug) { \ + if (unwinder->debug && !_Py_RemoteDebug_HasPermissionError()) { \ _set_debug_exception_cause(exc_type, message); \ } \ } while (0) diff --git a/Modules/_remote_debugging/asyncio.c b/Modules/_remote_debugging/asyncio.c index fc7487d4044bfb2..44a9a3cbce0061a 100644 --- a/Modules/_remote_debugging/asyncio.c +++ b/Modules/_remote_debugging/asyncio.c @@ -22,35 +22,38 @@ _Py_RemoteDebug_GetAsyncioDebugAddress(proc_handle_t* handle) address = search_windows_map_for_section(handle, "AsyncioD", L"_asyncio", NULL); if (address == 0) { - // Error out: 'python' substring covers both executable and DLL - PyObject *exc = PyErr_GetRaisedException(); - PyErr_SetString(PyExc_RuntimeError, "Failed to find the AsyncioDebug section in the process."); - _PyErr_ChainExceptions1(exc); + if (!_Py_RemoteDebug_HasPermissionError()) { + PyObject *exc = PyErr_GetRaisedException(); + PyErr_SetString(PyExc_RuntimeError, "Failed to find the AsyncioDebug section in the process."); + _PyErr_ChainExceptions1(exc); + } } #elif defined(__linux__) && HAVE_PROCESS_VM_READV // On Linux, search for asyncio debug in executable or DLL address = search_linux_map_for_section(handle, "AsyncioDebug", "python", NULL); if (address == 0) { - // Error out: 'python' substring covers both executable and DLL - PyObject *exc = PyErr_GetRaisedException(); - PyErr_SetString(PyExc_RuntimeError, "Failed to find the AsyncioDebug section in the process."); - _PyErr_ChainExceptions1(exc); + if (!_Py_RemoteDebug_HasPermissionError()) { + PyObject *exc = PyErr_GetRaisedException(); + PyErr_SetString(PyExc_RuntimeError, "Failed to find the AsyncioDebug section in the process."); + _PyErr_ChainExceptions1(exc); + } } #elif defined(__APPLE__) && TARGET_OS_OSX // On macOS, try libpython first, then fall back to python address = search_map_for_section(handle, "AsyncioDebug", "libpython", NULL); - if (address == 0) { + if (address == 0 && !_Py_RemoteDebug_HasPermissionError()) { PyErr_Clear(); address = search_map_for_section(handle, "AsyncioDebug", "python", NULL); } if (address == 0) { - // Error out: 'python' substring covers both executable and DLL - PyObject *exc = PyErr_GetRaisedException(); - PyErr_SetString(PyExc_RuntimeError, "Failed to find the AsyncioDebug section in the process."); - _PyErr_ChainExceptions1(exc); + if (!_Py_RemoteDebug_HasPermissionError()) { + PyObject *exc = PyErr_GetRaisedException(); + PyErr_SetString(PyExc_RuntimeError, "Failed to find the AsyncioDebug section in the process."); + _PyErr_ChainExceptions1(exc); + } } #else Py_UNREACHABLE(); @@ -96,10 +99,12 @@ ensure_async_debug_offsets(RemoteUnwinderObject *unwinder) return -1; } if (result < 0) { - PyErr_Clear(); - PyErr_SetString(PyExc_RuntimeError, "AsyncioDebug section not available"); - set_exception_cause(unwinder, PyExc_RuntimeError, - "AsyncioDebug section unavailable - asyncio module may not be loaded in target process"); + if (!_Py_RemoteDebug_HasPermissionError()) { + PyErr_Clear(); + PyErr_SetString(PyExc_RuntimeError, "AsyncioDebug section not available"); + set_exception_cause(unwinder, PyExc_RuntimeError, + "AsyncioDebug section unavailable - asyncio module may not be loaded in target process"); + } return -1; } @@ -218,7 +223,7 @@ parse_task_name( if ((GET_MEMBER(unsigned long, type_obj, unwinder->debug_offsets.type_object.tp_flags) & Py_TPFLAGS_LONG_SUBCLASS)) { long res = read_py_long(unwinder, task_name_addr); - if (res == -1) { + if (res == -1 && PyErr_Occurred()) { set_exception_cause(unwinder, PyExc_RuntimeError, "Task name PyLong parsing failed"); return NULL; } diff --git a/Modules/_remote_debugging/binary_io_reader.c b/Modules/_remote_debugging/binary_io_reader.c index 972b197cfbad861..ce1c3d232c94e0f 100644 --- a/Modules/_remote_debugging/binary_io_reader.c +++ b/Modules/_remote_debugging/binary_io_reader.c @@ -380,7 +380,22 @@ binary_reader_open(PyObject *path) Py_fclose(fp); goto error; } + if (st.st_size < 0) { + PyErr_SetString(PyExc_IOError, "Invalid negative file size"); + Py_fclose(fp); + goto error; + } + if ((uintmax_t)st.st_size > SIZE_MAX) { + PyErr_SetString(PyExc_OverflowError, "File is too large to map"); + Py_fclose(fp); + goto error; + } reader->mapped_size = st.st_size; + if (reader->mapped_size == 0) { + PyErr_SetString(PyExc_ValueError, "File too small for header"); + Py_fclose(fp); + goto error; + } /* Map the file into memory. * MAP_POPULATE (Linux-only) pre-faults all pages at mmap time, which: @@ -424,7 +439,10 @@ binary_reader_open(PyObject *path) } #endif - (void)Py_fclose(fp); + if (Py_fclose(fp) != 0) { + PyErr_SetFromErrno(PyExc_IOError); + goto error; + } uint8_t *data = reader->mapped_data; size_t file_size = reader->mapped_size; @@ -444,7 +462,15 @@ binary_reader_open(PyObject *path) PyErr_SetFromErrno(PyExc_IOError); goto error; } + if ((uint64_t)file_size_off > SIZE_MAX) { + PyErr_SetString(PyExc_OverflowError, "File is too large to read"); + goto error; + } reader->file_size = (size_t)file_size_off; + if (reader->file_size == 0) { + PyErr_SetString(PyExc_ValueError, "File too small for header"); + goto error; + } if (FSEEK64(reader->fp, 0, SEEK_SET) != 0) { PyErr_SetFromErrno(PyExc_IOError); goto error; @@ -456,8 +482,18 @@ binary_reader_open(PyObject *path) goto error; } - if (fread(reader->file_data, 1, reader->file_size, reader->fp) != reader->file_size) { - PyErr_SetFromErrno(PyExc_IOError); + size_t nread = fread(reader->file_data, 1, reader->file_size, reader->fp); + if (nread != reader->file_size) { + int err = errno; + if (ferror(reader->fp) && err != 0) { + errno = err; + PyErr_SetFromErrno(PyExc_IOError); + } + else { + PyErr_Format(PyExc_ValueError, + "Unexpected end of file: read %zu of %zu bytes", + nread, reader->file_size); + } goto error; } @@ -944,10 +980,16 @@ invoke_progress_callback(PyObject *callback, Py_ssize_t current, uint32_t total) Py_ssize_t binary_reader_replay(BinaryReader *reader, PyObject *collector, PyObject *progress_callback) { - if (!PyObject_HasAttrString(collector, "collect")) { + PyObject *collect_method; + int has_collect = PyObject_GetOptionalAttrString(collector, "collect", &collect_method); + if (has_collect < 0) { + return -1; + } + if (has_collect == 0) { PyErr_SetString(PyExc_TypeError, "Collector must have a collect() method"); return -1; } + Py_DECREF(collect_method); /* Get module state for struct sequence types */ PyObject *module = PyImport_ImportModule("_remote_debugging"); @@ -973,7 +1015,10 @@ binary_reader_replay(BinaryReader *reader, PyObject *collector, PyObject *progre while (offset < reader->sample_data_size) { /* Read thread_id (8 bytes) + interpreter_id (4 bytes) + encoding byte */ if (reader->sample_data_size - offset < SAMPLE_HEADER_FIXED_SIZE) { - break; /* End of data */ + PyErr_Format(PyExc_ValueError, + "Truncated sample data: %zu trailing bytes", + reader->sample_data_size - offset); + return -1; } /* Use memcpy to avoid strict aliasing violations, then byte-swap if needed */ @@ -1019,6 +1064,11 @@ binary_reader_replay(BinaryReader *reader, PyObject *collector, PyObject *progre count, max_possible_samples); return -1; } + if ((uint64_t)count > (uint64_t)PY_SSIZE_T_MAX - (uint64_t)replayed) { + PyErr_SetString(PyExc_OverflowError, + "Sample count exceeds Py_ssize_t maximum"); + return -1; + } reader->stats.repeat_records++; reader->stats.repeat_samples += count; @@ -1149,6 +1199,11 @@ binary_reader_replay(BinaryReader *reader, PyObject *collector, PyObject *progre return -1; } Py_DECREF(timestamps_list); + if (replayed == PY_SSIZE_T_MAX) { + PyErr_SetString(PyExc_OverflowError, + "Sample count exceeds Py_ssize_t maximum"); + return -1; + } replayed++; reader->stats.total_samples++; break; @@ -1167,6 +1222,13 @@ binary_reader_replay(BinaryReader *reader, PyObject *collector, PyObject *progre } } + if ((uint64_t)replayed != reader->sample_count) { + PyErr_Format(PyExc_ValueError, + "Sample count mismatch: header declares %u samples but replay decoded %zd", + reader->sample_count, replayed); + return -1; + } + /* Final progress callback at 100% */ if (invoke_progress_callback(progress_callback, replayed, reader->sample_count) < 0) { return -1; diff --git a/Modules/_remote_debugging/binary_io_writer.c b/Modules/_remote_debugging/binary_io_writer.c index c31ed7d746466f5..341f9f7dc8ac457 100644 --- a/Modules/_remote_debugging/binary_io_writer.c +++ b/Modules/_remote_debugging/binary_io_writer.c @@ -108,7 +108,15 @@ fwrite_checked_allow_threads(const void *data, size_t size, FILE *fp) written = fwrite(data, 1, size, fp); Py_END_ALLOW_THREADS if (written != size) { - PyErr_SetFromErrno(PyExc_IOError); + int err = errno; + if (ferror(fp) && err != 0) { + errno = err; + PyErr_SetFromErrno(PyExc_IOError); + } + else { + PyErr_Format(PyExc_IOError, + "short write: wrote %zu of %zu bytes", written, size); + } return -1; } return 0; @@ -366,6 +374,11 @@ writer_intern_string(BinaryWriter *writer, PyObject *string, uint32_t *index) return 0; } + if (writer->string_count >= UINT32_MAX) { + PyErr_SetString(PyExc_OverflowError, + "too many strings for binary format"); + return -1; + } if (writer->string_count >= writer->string_capacity) { if (grow_parallel_arrays((void **)&writer->strings, (void **)&writer->string_lengths, @@ -380,6 +393,12 @@ writer_intern_string(BinaryWriter *writer, PyObject *string, uint32_t *index) if (!str_data) { return -1; } + if ((uintmax_t)str_len > UINT32_MAX) { + PyErr_Format(PyExc_OverflowError, + "string length %zd exceeds binary format maximum %u", + str_len, UINT32_MAX); + return -1; + } char *str_copy = PyMem_Malloc(str_len + 1); if (!str_copy) { @@ -422,6 +441,11 @@ writer_intern_frame(BinaryWriter *writer, const FrameEntry *entry, uint32_t *ind return 0; } + if (writer->frame_count >= UINT32_MAX) { + PyErr_SetString(PyExc_OverflowError, + "too many frames for binary format"); + return -1; + } if (GROW_ARRAY(writer->frame_entries, writer->frame_count, writer->frame_capacity, FrameEntry) < 0) { return -1; @@ -466,6 +490,11 @@ writer_get_or_create_thread_entry(BinaryWriter *writer, uint64_t thread_id, } } + if (writer->thread_count >= UINT32_MAX) { + PyErr_SetString(PyExc_OverflowError, + "too many threads for binary format"); + return NULL; + } if (writer->thread_count >= writer->thread_capacity) { ThreadEntry *new_entries = grow_array(writer->thread_entries, &writer->thread_capacity, @@ -600,6 +629,11 @@ flush_pending_rle(BinaryWriter *writer, ThreadEntry *entry) if (!entry->has_pending_rle || entry->pending_rle_count == 0) { return 0; } + if (entry->pending_rle_count > UINT32_MAX - writer->total_samples) { + PyErr_SetString(PyExc_OverflowError, + "too many samples for binary format"); + return -1; + } /* Write RLE record: * [thread_id: 8] [interpreter_id: 4] [STACK_REPEAT: 1] [count: varint] @@ -644,6 +678,12 @@ write_sample_with_encoding(BinaryWriter *writer, ThreadEntry *entry, const uint32_t *frame_indices, size_t stack_depth, size_t shared_count, size_t pop_count, size_t push_count) { + if (writer->total_samples == UINT32_MAX) { + PyErr_SetString(PyExc_OverflowError, + "too many samples for binary format"); + return -1; + } + /* Header: thread_id(8) + interpreter_id(4) + encoding(1) + delta(varint) + status(1) */ uint8_t header_buf[SAMPLE_HEADER_MAX_SIZE]; memcpy(header_buf + SMP_OFF_THREAD_ID, &entry->thread_id, SMP_SIZE_THREAD_ID); diff --git a/Modules/_remote_debugging/code_objects.c b/Modules/_remote_debugging/code_objects.c index 3af58f2b3c379ec..ab889a130ee4e7e 100644 --- a/Modules/_remote_debugging/code_objects.c +++ b/Modules/_remote_debugging/code_objects.c @@ -47,7 +47,6 @@ cache_tlbc_array(RemoteUnwinderObject *unwinder, uintptr_t code_addr, uintptr_t // Read the TLBC array pointer if (read_ptr(unwinder, tlbc_array_addr, &tlbc_array_ptr) != 0) { - PyErr_SetString(PyExc_RuntimeError, "Failed to read TLBC array pointer"); set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read TLBC array pointer"); return 0; // Read error } @@ -61,7 +60,6 @@ cache_tlbc_array(RemoteUnwinderObject *unwinder, uintptr_t code_addr, uintptr_t // Read the TLBC array size Py_ssize_t tlbc_size; if (_Py_RemoteDebug_PagedReadRemoteMemory(&unwinder->handle, tlbc_array_ptr, sizeof(tlbc_size), &tlbc_size) != 0) { - PyErr_SetString(PyExc_RuntimeError, "Failed to read TLBC array size"); set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read TLBC array size"); return 0; // Read error } diff --git a/Modules/_remote_debugging/module.c b/Modules/_remote_debugging/module.c index 3e60a7c2f794adb..984213d18817523 100644 --- a/Modules/_remote_debugging/module.c +++ b/Modules/_remote_debugging/module.c @@ -411,6 +411,9 @@ _remote_debugging_RemoteUnwinder___init___impl(RemoteUnwinderObject *self, return -1; } if (async_debug_result < 0) { + if (_Py_RemoteDebug_HasPermissionError()) { + return -1; + } PyErr_Clear(); memset(&self->async_debug_offsets, 0, sizeof(self->async_debug_offsets)); self->async_debug_offsets_available = 0; diff --git a/Modules/_remote_debugging/object_reading.c b/Modules/_remote_debugging/object_reading.c index b63b103a2617acc..1cea96a2151fcc6 100644 --- a/Modules/_remote_debugging/object_reading.c +++ b/Modules/_remote_debugging/object_reading.c @@ -6,6 +6,7 @@ ******************************************************************************/ #include "_remote_debugging.h" +#include /* ============================================================================ * MEMORY READING FUNCTIONS @@ -264,26 +265,16 @@ read_py_long( Py_ssize_t inline_digits_space = SIZEOF_LONG_OBJ - ob_digit_offset; Py_ssize_t max_inline_digits = inline_digits_space / (Py_ssize_t)sizeof(digit); - // If the long object has inline digits that fit in our buffer, use them directly - digit *digits; + digit *digits = (digit *)PyMem_RawMalloc(size * sizeof(digit)); + if (!digits) { + PyErr_NoMemory(); + set_exception_cause(unwinder, PyExc_MemoryError, "Failed to allocate digits for PyLong"); + return -1; + } + if (size <= max_inline_digits && size <= _PY_NSMALLNEGINTS + _PY_NSMALLPOSINTS) { - // For small integers, digits are inline in the long_value.ob_digit array - digits = (digit *)PyMem_RawMalloc(size * sizeof(digit)); - if (!digits) { - PyErr_NoMemory(); - set_exception_cause(unwinder, PyExc_MemoryError, "Failed to allocate digits for small PyLong"); - return -1; - } memcpy(digits, long_obj + ob_digit_offset, size * sizeof(digit)); } else { - // For larger integers, we need to read the digits separately - digits = (digit *)PyMem_RawMalloc(size * sizeof(digit)); - if (!digits) { - PyErr_NoMemory(); - set_exception_cause(unwinder, PyExc_MemoryError, "Failed to allocate digits for large PyLong"); - return -1; - } - bytes_read = _Py_RemoteDebug_PagedReadRemoteMemory( &unwinder->handle, address + (uintptr_t)unwinder->debug_offsets.long_object.ob_digit, @@ -296,19 +287,34 @@ read_py_long( } } - long long value = 0; + unsigned long limit = negative + ? (unsigned long)LONG_MAX + 1UL + : (unsigned long)LONG_MAX; + unsigned long value = 0; - // In theory this can overflow, but because of llvm/llvm-project#16778 - // we can't use __builtin_mul_overflow because it fails to link with - // __muloti4 on aarch64. In practice this is fine because all we're - // testing here are task numbers that would fit in a single byte. - for (Py_ssize_t i = 0; i < size; ++i) { - long long factor = digits[i] * (1UL << (Py_ssize_t)(shift * i)); - value += factor; + for (Py_ssize_t i = size; i-- > 0;) { + if (digits[i] >= PyLong_BASE) { + PyErr_Format(PyExc_RuntimeError, + "Invalid PyLong digit: %u (base %u)", digits[i], PyLong_BASE); + set_exception_cause(unwinder, PyExc_RuntimeError, + "Invalid PyLong digit (corrupted remote memory)"); + goto error; + } + if (value > ((limit - (unsigned long)digits[i]) >> shift)) { + PyErr_SetString(PyExc_OverflowError, + "Remote PyLong value does not fit in C long"); + set_exception_cause(unwinder, PyExc_OverflowError, + "Remote PyLong value is too large"); + goto error; + } + value = (value << shift) | (unsigned long)digits[i]; } PyMem_RawFree(digits); if (negative) { - value *= -1; + if (value == (unsigned long)LONG_MAX + 1UL) { + return LONG_MIN; + } + return -(long)value; } return (long)value; error: diff --git a/Modules/_remote_debugging/subprocess.c b/Modules/_remote_debugging/subprocess.c index 1b16dd8343f2a5b..cdad75e318be914 100644 --- a/Modules/_remote_debugging/subprocess.c +++ b/Modules/_remote_debugging/subprocess.c @@ -223,8 +223,19 @@ get_child_pids_platform(pid_t target_pid, int recursive, pid_array_t *result) } /* Single pass: collect PIDs and their PPIDs together */ - struct dirent *entry; - while ((entry = readdir(proc_dir)) != NULL) { + for (;;) { + errno = 0; + struct dirent *entry = readdir(proc_dir); + if (entry == NULL) { + if (errno != 0) { + int err = errno; + _set_debug_oserror_from_errno_with_filename(err, "/proc", + "Failed to read process directory '/proc': %s", + strerror(err)); + goto done; + } + break; + } /* Skip non-numeric entries (also skips . and ..) */ if (entry->d_name[0] < '1' || entry->d_name[0] > '9') { continue; @@ -245,7 +256,14 @@ get_child_pids_platform(pid_t target_pid, int recursive, pid_array_t *result) } } - closedir(proc_dir); + if (closedir(proc_dir) != 0) { + int err = errno; + proc_dir = NULL; + _set_debug_oserror_from_errno_with_filename(err, "/proc", + "Failed to close process directory '/proc': %s", + strerror(err)); + goto done; + } proc_dir = NULL; if (find_children_bfs(target_pid, recursive, @@ -358,7 +376,8 @@ get_child_pids_platform(pid_t target_pid, int recursive, pid_array_t *result) snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (snapshot == INVALID_HANDLE_VALUE) { - PyErr_SetFromWindowsErr(0); + DWORD error = GetLastError(); + PyErr_SetFromWindowsErr(error); goto done; } @@ -373,13 +392,23 @@ get_child_pids_platform(pid_t target_pid, int recursive, pid_array_t *result) /* Single pass: collect PIDs and PPIDs together */ PROCESSENTRY32 pe; pe.dwSize = sizeof(PROCESSENTRY32); - if (Process32First(snapshot, &pe)) { - do { - if (pid_array_append(&all_pids, (pid_t)pe.th32ProcessID) < 0 || - pid_array_append(&ppids, (pid_t)pe.th32ParentProcessID) < 0) { - goto done; - } - } while (Process32Next(snapshot, &pe)); + if (!Process32First(snapshot, &pe)) { + DWORD error = GetLastError(); + PyErr_SetFromWindowsErr(error); + goto done; + } + + do { + if (pid_array_append(&all_pids, (pid_t)pe.th32ProcessID) < 0 || + pid_array_append(&ppids, (pid_t)pe.th32ParentProcessID) < 0) { + goto done; + } + } while (Process32Next(snapshot, &pe)); + + DWORD error = GetLastError(); + if (error != ERROR_NO_MORE_FILES) { + PyErr_SetFromWindowsErr(error); + goto done; } CloseHandle(snapshot); diff --git a/Modules/_remote_debugging/threads.c b/Modules/_remote_debugging/threads.c index ae120a26d5f4ece..5176c4cf0671bb1 100644 --- a/Modules/_remote_debugging/threads.c +++ b/Modules/_remote_debugging/threads.c @@ -658,8 +658,7 @@ read_thread_ids(RemoteUnwinderObject *unwinder, _Py_RemoteDebug_ThreadsState *st DIR *dir = opendir(task_path); if (dir == NULL) { - st->tids = NULL; - st->count = 0; + _Py_RemoteDebug_InitThreadsState(unwinder, st); if (errno == ENOENT || errno == ESRCH) { PyErr_Format(PyExc_ProcessLookupError, "Process %d has terminated", unwinder->handle.pid); @@ -671,8 +670,21 @@ read_thread_ids(RemoteUnwinderObject *unwinder, _Py_RemoteDebug_ThreadsState *st st->count = 0; - struct dirent *entry; - while ((entry = readdir(dir)) != NULL) { + for (;;) { + errno = 0; + struct dirent *entry = readdir(dir); + if (entry == NULL) { + if (errno != 0) { + int err = errno; + closedir(dir); + _Py_RemoteDebug_InitThreadsState(unwinder, st); + _set_debug_oserror_from_errno_with_filename(err, task_path, + "Failed to read process task directory '%s': %s", + task_path, strerror(err)); + return -1; + } + break; + } if (entry->d_name[0] < '1' || entry->d_name[0] > '9') { continue; } @@ -686,8 +698,7 @@ read_thread_ids(RemoteUnwinderObject *unwinder, _Py_RemoteDebug_ThreadsState *st pid_t *new_tids = PyMem_RawRealloc(unwinder->thread_tids, new_cap * sizeof(pid_t)); if (new_tids == NULL) { closedir(dir); - st->tids = NULL; - st->count = 0; + _Py_RemoteDebug_InitThreadsState(unwinder, st); PyErr_NoMemory(); return -1; } @@ -697,8 +708,15 @@ read_thread_ids(RemoteUnwinderObject *unwinder, _Py_RemoteDebug_ThreadsState *st unwinder->thread_tids[st->count++] = (pid_t)tid; } + if (closedir(dir) != 0) { + int err = errno; + _Py_RemoteDebug_InitThreadsState(unwinder, st); + _set_debug_oserror_from_errno_with_filename(err, task_path, + "Failed to close process task directory '%s': %s", + task_path, strerror(err)); + return -1; + } st->tids = unwinder->thread_tids; - closedir(dir); return 0; } @@ -711,28 +729,30 @@ detach_threads(_Py_RemoteDebug_ThreadsState *st, size_t up_to) } static int -seize_thread(pid_t tid) +seize_thread(pid_t tid, int *err) { if (ptrace(PTRACE_SEIZE, tid, NULL, 0) == 0) { return 0; } - if (errno == ESRCH) { + *err = errno; + if (*err == ESRCH) { return 1; // Thread gone, skip } - if (errno == EPERM) { + if (*err == EPERM) { // Thread may have exited, be in a special state, or already be traced. // Skip rather than fail - this avoids endless retry loops when // threads transiently become inaccessible. return 1; } - if (errno == EINVAL || errno == EIO) { + if (*err == EINVAL || *err == EIO) { // Fallback for older kernels if (ptrace(PTRACE_ATTACH, tid, NULL, NULL) == 0) { int status; waitpid(tid, &status, __WALL); return 0; } - if (errno == ESRCH || errno == EPERM) { + *err = errno; + if (*err == ESRCH || *err == EPERM) { return 1; // Thread gone or inaccessible } } @@ -746,39 +766,50 @@ _Py_RemoteDebug_StopAllThreads(RemoteUnwinderObject *unwinder, _Py_RemoteDebug_T return -1; } - for (size_t i = 0; i < st->count; i++) { + size_t n_tids = st->count; + size_t seized = 0; + for (size_t i = 0; i < n_tids; i++) { pid_t tid = st->tids[i]; - int ret = seize_thread(tid); + int err = 0; + int ret = seize_thread(tid, &err); if (ret == 1) { continue; // Thread gone, skip } if (ret < 0) { - detach_threads(st, i); - PyErr_Format(PyExc_RuntimeError, "Failed to seize thread %d: %s", tid, strerror(errno)); - st->tids = NULL; - st->count = 0; + detach_threads(st, seized); + _set_debug_oserror_from_errno(err, + "Failed to seize thread %d: %s", tid, strerror(err)); + _Py_RemoteDebug_InitThreadsState(unwinder, st); return -1; } + st->tids[seized++] = tid; - if (ptrace(PTRACE_INTERRUPT, tid, NULL, NULL) == -1 && errno != ESRCH) { - detach_threads(st, i + 1); - PyErr_Format(PyExc_RuntimeError, "Failed to interrupt thread %d: %s", tid, strerror(errno)); - st->tids = NULL; - st->count = 0; - return -1; + if (ptrace(PTRACE_INTERRUPT, tid, NULL, NULL) == -1) { + err = errno; + if (err != ESRCH) { + detach_threads(st, seized); + _set_debug_oserror_from_errno(err, + "Failed to interrupt thread %d: %s", tid, strerror(err)); + _Py_RemoteDebug_InitThreadsState(unwinder, st); + return -1; + } } int status; - if (waitpid(tid, &status, __WALL) == -1 && errno != ECHILD && errno != ESRCH) { - detach_threads(st, i + 1); - PyErr_Format(PyExc_RuntimeError, "waitpid failed for thread %d: %s", tid, strerror(errno)); - st->tids = NULL; - st->count = 0; - return -1; + if (waitpid(tid, &status, __WALL) == -1) { + err = errno; + if (err != ECHILD && err != ESRCH) { + detach_threads(st, seized); + _set_debug_oserror_from_errno(err, + "waitpid failed for thread %d: %s", tid, strerror(err)); + _Py_RemoteDebug_InitThreadsState(unwinder, st); + return -1; + } } } + st->count = seized; return 0; } diff --git a/Python/remote_debug.h b/Python/remote_debug.h index 53bbd571ad3cef4..6fecc23502b46ef 100644 --- a/Python/remote_debug.h +++ b/Python/remote_debug.h @@ -100,9 +100,16 @@ extern "C" { # define HAVE_PROCESS_VM_READV 0 #endif +static inline int +_Py_RemoteDebug_HasPermissionError(void) +{ + return PyErr_Occurred() + && PyErr_ExceptionMatches(PyExc_PermissionError); +} + #define _set_debug_exception_cause(exception, format, ...) \ do { \ - if (!PyErr_ExceptionMatches(PyExc_PermissionError)) { \ + if (!_Py_RemoteDebug_HasPermissionError()) { \ PyThreadState *tstate = _PyThreadState_GET(); \ if (!_PyErr_Occurred(tstate)) { \ _PyErr_Format(tstate, exception, format, ##__VA_ARGS__); \ @@ -112,6 +119,20 @@ extern "C" { } \ } while (0) +#define _set_debug_oserror_from_errno(err, format, ...) \ + do { \ + errno = (err); \ + PyErr_SetFromErrno(PyExc_OSError); \ + _set_debug_exception_cause(PyExc_OSError, format, ##__VA_ARGS__); \ + } while (0) + +#define _set_debug_oserror_from_errno_with_filename(err, filename, format, ...) \ + do { \ + errno = (err); \ + PyErr_SetFromErrnoWithFilename(PyExc_OSError, filename); \ + _set_debug_exception_cause(PyExc_OSError, format, ##__VA_ARGS__); \ + } while (0) + static inline size_t get_page_size(void) { size_t page_size = 0; @@ -170,7 +191,7 @@ _Py_RemoteDebug_ValidatePyRuntimeCookie(proc_handle_t *handle, uintptr_t address } char buf[sizeof(_Py_Debug_Cookie) - 1]; if (_Py_RemoteDebug_ReadRemoteMemory(handle, address, sizeof(buf), buf) != 0) { - if (!PyErr_ExceptionMatches(PyExc_PermissionError)) { + if (!_Py_RemoteDebug_HasPermissionError()) { PyErr_Clear(); } return 0; @@ -207,6 +228,21 @@ static mach_port_t pid_to_task(pid_t pid); // Initialize the process handle UNUSED static int _Py_RemoteDebug_InitProcHandle(proc_handle_t *handle, pid_t pid) { + handle->pid = 0; +#if defined(__APPLE__) && defined(TARGET_OS_OSX) && TARGET_OS_OSX + handle->task = 0; +#elif defined(MS_WINDOWS) + handle->hProcess = NULL; +#elif defined(__linux__) + handle->memfd = -1; +#endif + handle->page_size = get_page_size(); + handle->page_cache_count = 0; + for (int i = 0; i < MAX_PAGES; i++) { + handle->pages[i].data = NULL; + handle->pages[i].valid = 0; + } + handle->pid = pid; #if defined(__APPLE__) && defined(TARGET_OS_OSX) && TARGET_OS_OSX handle->task = pid_to_task(handle->pid); @@ -219,19 +255,12 @@ _Py_RemoteDebug_InitProcHandle(proc_handle_t *handle, pid_t pid) { PROCESS_VM_READ | PROCESS_VM_WRITE | PROCESS_VM_OPERATION | PROCESS_QUERY_INFORMATION | PROCESS_SUSPEND_RESUME, FALSE, pid); if (handle->hProcess == NULL) { - PyErr_SetFromWindowsErr(0); + DWORD error = GetLastError(); + PyErr_SetFromWindowsErr(error); _set_debug_exception_cause(PyExc_RuntimeError, "Failed to initialize Windows process handle"); return -1; } -#elif defined(__linux__) - handle->memfd = -1; #endif - handle->page_size = get_page_size(); - handle->page_cache_count = 0; - for (int i = 0; i < MAX_PAGES; i++) { - handle->pages[i].data = NULL; - handle->pages[i].valid = 0; - } return 0; } @@ -396,17 +425,19 @@ return_section_address_fat( size_t cpu_size = sizeof(cpu), abi64_size = sizeof(is_abi64); if (sysctlbyname("hw.cputype", &cpu, &cpu_size, NULL, 0) != 0) { - PyErr_Format(PyExc_OSError, + int err = errno; + _set_debug_oserror_from_errno(err, "Failed to determine CPU type via sysctlbyname " "for fat binary analysis at 0x%lx: %s", - base, strerror(errno)); + base, strerror(err)); return 0; } if (sysctlbyname("hw.cpu64bit_capable", &is_abi64, &abi64_size, NULL, 0) != 0) { - PyErr_Format(PyExc_OSError, + int err = errno; + _set_debug_oserror_from_errno(err, "Failed to determine CPU ABI capability via sysctlbyname " "for fat binary analysis at 0x%lx: %s", - base, strerror(errno)); + base, strerror(err)); return 0; } @@ -459,26 +490,29 @@ search_section_in_file(const char* secname, char* path, uintptr_t base, mach_vm_ { int fd = open(path, O_RDONLY); if (fd == -1) { - PyErr_Format(PyExc_OSError, + int err = errno; + _set_debug_oserror_from_errno_with_filename(err, path, "Cannot open binary file '%s' for section '%s' search: %s", - path, secname, strerror(errno)); + path, secname, strerror(err)); return 0; } struct stat fs; if (fstat(fd, &fs) == -1) { - PyErr_Format(PyExc_OSError, + int err = errno; + _set_debug_oserror_from_errno_with_filename(err, path, "Cannot get file size for binary '%s' during section '%s' search: %s", - path, secname, strerror(errno)); + path, secname, strerror(err)); close(fd); return 0; } void* map = mmap(0, fs.st_size, PROT_READ, MAP_SHARED, fd, 0); if (map == MAP_FAILED) { - PyErr_Format(PyExc_OSError, + int err = errno; + _set_debug_oserror_from_errno_with_filename(err, path, "Cannot memory map binary file '%s' (size: %lld bytes) for section '%s' search: %s", - path, (long long)fs.st_size, secname, strerror(errno)); + path, (long long)fs.st_size, secname, strerror(err)); close(fd); return 0; } @@ -507,15 +541,21 @@ search_section_in_file(const char* secname, char* path, uintptr_t base, mach_vm_ } if (munmap(map, fs.st_size) != 0) { - PyErr_Format(PyExc_OSError, - "Failed to unmap binary file '%s' (size: %lld bytes): %s", - path, (long long)fs.st_size, strerror(errno)); + if (!PyErr_Occurred()) { + int err = errno; + _set_debug_oserror_from_errno_with_filename(err, path, + "Failed to unmap binary file '%s' (size: %lld bytes): %s", + path, (long long)fs.st_size, strerror(err)); + } result = 0; } if (close(fd) != 0) { - PyErr_Format(PyExc_OSError, - "Failed to close binary file '%s': %s", - path, strerror(errno)); + if (!PyErr_Occurred()) { + int err = errno; + _set_debug_oserror_from_errno_with_filename(err, path, + "Failed to close binary file '%s': %s", + path, strerror(err)); + } result = 0; } return result; @@ -560,14 +600,15 @@ search_map_for_section(proc_handle_t *handle, const char* secname, const char* s char map_filename[MAXPATHLEN + 1]; - while (mach_vm_region( - proc_ref, - &address, - &size, - VM_REGION_BASIC_INFO_64, - (vm_region_info_t)®ion_info, - &count, - &object_name) == KERN_SUCCESS) + kern_return_t kr; + while ((kr = mach_vm_region( + proc_ref, + &address, + &size, + VM_REGION_BASIC_INFO_64, + (vm_region_info_t)®ion_info, + &count, + &object_name)) == KERN_SUCCESS) { if ((region_info.protection & VM_PROT_READ) == 0 @@ -591,18 +632,32 @@ search_map_for_section(proc_handle_t *handle, const char* secname, const char* s } if (strncmp(filename, substr, strlen(substr)) == 0) { + PyErr_Clear(); uintptr_t result = search_section_in_file( secname, map_filename, address, size, proc_ref); - if (result != 0 - && (validator == NULL || validator(handle, result))) - { - return result; + if (result != 0) { + if (validator == NULL || validator(handle, result)) { + return result; + } + if (_Py_RemoteDebug_HasPermissionError()) { + return 0; + } + } + else if (_Py_RemoteDebug_HasPermissionError()) { + return 0; } } address += size; } + if (kr != KERN_INVALID_ADDRESS && !PyErr_Occurred()) { + PyErr_Format(PyExc_RuntimeError, + "mach_vm_region failed while searching PID %d for section '%s' " + "(kern_return_t: %d)", + handle->pid, secname, kr); + } + return 0; } @@ -625,25 +680,29 @@ search_elf_file_for_section( int fd = open(elf_file, O_RDONLY); if (fd < 0) { - PyErr_Format(PyExc_OSError, + int err = errno; + _set_debug_oserror_from_errno_with_filename(err, elf_file, "Cannot open ELF file '%s' for section '%s' search: %s", - elf_file, secname, strerror(errno)); + elf_file, secname, strerror(err)); goto exit; } struct stat file_stats; if (fstat(fd, &file_stats) != 0) { - PyErr_Format(PyExc_OSError, + int err = errno; + _set_debug_oserror_from_errno_with_filename(err, elf_file, "Cannot get file size for ELF file '%s' during section '%s' search: %s", - elf_file, secname, strerror(errno)); + elf_file, secname, strerror(err)); goto exit; } file_memory = mmap(NULL, file_stats.st_size, PROT_READ, MAP_PRIVATE, fd, 0); if (file_memory == MAP_FAILED) { - PyErr_Format(PyExc_OSError, + int err = errno; + _set_debug_oserror_from_errno_with_filename(err, elf_file, "Cannot memory map ELF file '%s' (size: %lld bytes) for section '%s' search: %s", - elf_file, (long long)file_stats.st_size, secname, strerror(errno)); + elf_file, (long long)file_stats.st_size, secname, strerror(err)); + file_memory = NULL; goto exit; } @@ -700,12 +759,23 @@ search_elf_file_for_section( exit: if (file_memory != NULL) { - munmap(file_memory, file_stats.st_size); + if (munmap(file_memory, file_stats.st_size) != 0) { + if (!PyErr_Occurred()) { + int err = errno; + _set_debug_oserror_from_errno_with_filename(err, elf_file, + "Failed to unmap ELF file '%s' (size: %lld bytes): %s", + elf_file, (long long)file_stats.st_size, strerror(err)); + } + result = 0; + } } if (fd >= 0 && close(fd) != 0) { - PyErr_Format(PyExc_OSError, - "Failed to close ELF file '%s': %s", - elf_file, strerror(errno)); + if (!PyErr_Occurred()) { + int err = errno; + _set_debug_oserror_from_errno_with_filename(err, elf_file, + "Failed to close ELF file '%s': %s", + elf_file, strerror(err)); + } result = 0; } return result; @@ -720,9 +790,10 @@ search_linux_map_for_section(proc_handle_t *handle, const char* secname, const c FILE* maps_file = fopen(maps_file_path, "r"); if (maps_file == NULL) { - PyErr_Format(PyExc_OSError, + int err = errno; + _set_debug_oserror_from_errno_with_filename(err, maps_file_path, "Cannot open process memory map file '%s' for PID %d section search: %s", - maps_file_path, handle->pid, strerror(errno)); + maps_file_path, handle->pid, strerror(err)); return 0; } @@ -787,26 +858,39 @@ search_linux_map_for_section(proc_handle_t *handle, const char* secname, const c } if (strstr(filename, substr)) { - if (PyErr_ExceptionMatches(PyExc_PermissionError)) { - retval = 0; - break; - } PyErr_Clear(); retval = search_elf_file_for_section(handle, secname, start, path); - if (retval - && (validator == NULL || validator(handle, retval))) - { + if (retval) { + if (validator == NULL || validator(handle, retval)) { + break; + } + if (_Py_RemoteDebug_HasPermissionError()) { + retval = 0; + break; + } + } + else if (_Py_RemoteDebug_HasPermissionError()) { break; } retval = 0; } } + if (retval == 0 && !PyErr_Occurred() && ferror(maps_file)) { + int err = errno; + _set_debug_oserror_from_errno_with_filename(err, maps_file_path, + "Failed to read process map file '%s' for PID %d section search: %s", + maps_file_path, handle->pid, strerror(err)); + } + PyMem_Free(line); if (fclose(maps_file) != 0) { - PyErr_Format(PyExc_OSError, - "Failed to close process map file '%s': %s", - maps_file_path, strerror(errno)); + if (!PyErr_Occurred()) { + int err = errno; + _set_debug_oserror_from_errno_with_filename(err, maps_file_path, + "Failed to close process map file '%s': %s", + maps_file_path, strerror(err)); + } retval = 0; } @@ -829,9 +913,9 @@ static int is_process_alive(HANDLE hProcess) { static void* analyze_pe(const wchar_t* mod_path, BYTE* remote_base, const char* secname) { HANDLE hFile = CreateFileW(mod_path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (hFile == INVALID_HANDLE_VALUE) { - PyErr_SetFromWindowsErr(0); DWORD error = GetLastError(); - PyErr_Format(PyExc_OSError, + PyErr_SetFromWindowsErr(error); + _set_debug_exception_cause(PyExc_OSError, "Cannot open PE file for section '%s' analysis (error %lu)", secname, error); return NULL; @@ -839,9 +923,9 @@ static void* analyze_pe(const wchar_t* mod_path, BYTE* remote_base, const char* HANDLE hMap = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, 0); if (!hMap) { - PyErr_SetFromWindowsErr(0); DWORD error = GetLastError(); - PyErr_Format(PyExc_OSError, + PyErr_SetFromWindowsErr(error); + _set_debug_exception_cause(PyExc_OSError, "Cannot create file mapping for PE file section '%s' analysis (error %lu)", secname, error); CloseHandle(hFile); @@ -850,9 +934,9 @@ static void* analyze_pe(const wchar_t* mod_path, BYTE* remote_base, const char* BYTE* mapView = (BYTE*)MapViewOfFile(hMap, FILE_MAP_READ, 0, 0, 0); if (!mapView) { - PyErr_SetFromWindowsErr(0); DWORD error = GetLastError(); - PyErr_Format(PyExc_OSError, + PyErr_SetFromWindowsErr(error); + _set_debug_exception_cause(PyExc_OSError, "Cannot map view of PE file for section '%s' analysis (error %lu)", secname, error); CloseHandle(hMap); @@ -910,9 +994,9 @@ search_windows_map_for_section(proc_handle_t* handle, const char* secname, const } while (hProcSnap == INVALID_HANDLE_VALUE && GetLastError() == ERROR_BAD_LENGTH); if (hProcSnap == INVALID_HANDLE_VALUE) { - PyErr_SetFromWindowsErr(0); DWORD error = GetLastError(); - PyErr_Format(PyExc_PermissionError, + PyErr_SetFromWindowsErr(error); + _set_debug_exception_cause(PyExc_OSError, "Unable to create module snapshot for PID %d section '%s' " "search (error %lu). Check permissions or PID validity", handle->pid, secname, error); @@ -923,17 +1007,46 @@ search_windows_map_for_section(proc_handle_t* handle, const char* secname, const moduleEntry.dwSize = sizeof(moduleEntry); void* runtime_addr = NULL; - for (BOOL hasModule = Module32FirstW(hProcSnap, &moduleEntry); hasModule; hasModule = Module32NextW(hProcSnap, &moduleEntry)) { + if (!Module32FirstW(hProcSnap, &moduleEntry)) { + DWORD error = GetLastError(); + PyErr_SetFromWindowsErr(error); + _set_debug_exception_cause(PyExc_OSError, + "Unable to enumerate modules for PID %d section '%s' " + "search (error %lu)", + handle->pid, secname, error); + CloseHandle(hProcSnap); + return 0; + } + + do { // Look for either python executable or DLL if (wcsstr(moduleEntry.szModule, substr)) { + PyErr_Clear(); void *candidate = analyze_pe(moduleEntry.szExePath, moduleEntry.modBaseAddr, secname); - if (candidate != NULL - && (validator == NULL || validator(handle, (uintptr_t)candidate))) - { - runtime_addr = candidate; + if (candidate != NULL) { + if (validator == NULL || validator(handle, (uintptr_t)candidate)) { + runtime_addr = candidate; + break; + } + if (_Py_RemoteDebug_HasPermissionError()) { + break; + } + } + else if (_Py_RemoteDebug_HasPermissionError()) { break; } } + } while (Module32NextW(hProcSnap, &moduleEntry)); + + if (runtime_addr == NULL && !PyErr_Occurred()) { + DWORD error = GetLastError(); + if (error != ERROR_NO_MORE_FILES) { + PyErr_SetFromWindowsErr(error); + _set_debug_exception_cause(PyExc_OSError, + "Module enumeration failed for PID %d section '%s' " + "search (error %lu)", + handle->pid, secname, error); + } } CloseHandle(hProcSnap); @@ -954,19 +1067,21 @@ _Py_RemoteDebug_GetPyRuntimeAddress(proc_handle_t* handle) address = search_windows_map_for_section(handle, "PyRuntime", L"python", _Py_RemoteDebug_ValidatePyRuntimeCookie); if (address == 0) { - // Error out: 'python' substring covers both executable and DLL - PyObject *exc = PyErr_GetRaisedException(); - PyErr_Format(PyExc_RuntimeError, - "Failed to find the PyRuntime section in process %d on Windows platform", - handle->pid); - _PyErr_ChainExceptions1(exc); + if (!_Py_RemoteDebug_HasPermissionError()) { + // Error out: 'python' substring covers both executable and DLL + PyObject *exc = PyErr_GetRaisedException(); + PyErr_Format(PyExc_RuntimeError, + "Failed to find the PyRuntime section in process %d on Windows platform", + handle->pid); + _PyErr_ChainExceptions1(exc); + } } #elif defined(__linux__) && HAVE_PROCESS_VM_READV // On Linux, search for 'python' in executable or DLL address = search_linux_map_for_section(handle, "PyRuntime", "python", _Py_RemoteDebug_ValidatePyRuntimeCookie); if (address == 0) { - if (!PyErr_ExceptionMatches(PyExc_PermissionError)) { + if (!_Py_RemoteDebug_HasPermissionError()) { // Error out: 'python' substring covers both executable and DLL PyObject *exc = PyErr_GetRaisedException(); PyErr_Format(PyExc_RuntimeError, @@ -982,17 +1097,19 @@ _Py_RemoteDebug_GetPyRuntimeAddress(proc_handle_t* handle) PyErr_Clear(); address = search_map_for_section(handle, "PyRuntime", *candidate, _Py_RemoteDebug_ValidatePyRuntimeCookie); - if (address != 0) { + if (address != 0 || _Py_RemoteDebug_HasPermissionError()) { break; } } if (address == 0) { - PyObject *exc = PyErr_GetRaisedException(); - PyErr_Format(PyExc_RuntimeError, - "Failed to find the PyRuntime section in process %d " - "on macOS platform (tried both libpython and python)", - handle->pid); - _PyErr_ChainExceptions1(exc); + if (!_Py_RemoteDebug_HasPermissionError()) { + PyObject *exc = PyErr_GetRaisedException(); + PyErr_Format(PyExc_RuntimeError, + "Failed to find the PyRuntime section in process %d " + "on macOS platform (tried both libpython and python)", + handle->pid); + _PyErr_ChainExceptions1(exc); + } } #else _set_debug_exception_cause(PyExc_RuntimeError, @@ -1013,9 +1130,9 @@ open_proc_mem_fd(proc_handle_t *handle) handle->memfd = open(mem_file_path, O_RDWR); if (handle->memfd == -1) { - PyErr_SetFromErrno(PyExc_OSError); - _set_debug_exception_cause(PyExc_OSError, - "failed to open file %s: %s", mem_file_path, strerror(errno)); + int err = errno; + _set_debug_oserror_from_errno_with_filename(err, mem_file_path, + "failed to open file %s: %s", mem_file_path, strerror(err)); return -1; } return 0; @@ -1026,6 +1143,9 @@ open_proc_mem_fd(proc_handle_t *handle) static int read_remote_memory_fallback(proc_handle_t *handle, uintptr_t remote_address, size_t len, void* dst) { + if (len == 0) { + return 0; + } if (handle->memfd == -1) { if (open_proc_mem_fd(handle) < 0) { return -1; @@ -1043,14 +1163,23 @@ read_remote_memory_fallback(proc_handle_t *handle, uintptr_t remote_address, siz read_bytes = preadv(handle->memfd, local, 1, offset); if (read_bytes < 0) { + int err = errno; + errno = err; PyErr_SetFromErrno(PyExc_OSError); _set_debug_exception_cause(PyExc_OSError, "preadv failed for PID %d at address 0x%lx " "(size %zu, partial read %zd bytes): %s", - handle->pid, remote_address + result, len - result, result, strerror(errno)); + handle->pid, remote_address + result, len - result, result, strerror(err)); return -1; } + if (read_bytes == 0) { + PyErr_Format(PyExc_OSError, + "preadv returned 0 bytes for PID %d at address 0x%lx " + "(size %zu, partial read %zd bytes)", + handle->pid, remote_address + result, len - result, result); + return -1; + } result += read_bytes; } while ((size_t)read_bytes != local[0].iov_len); return 0; @@ -1062,11 +1191,15 @@ read_remote_memory_fallback(proc_handle_t *handle, uintptr_t remote_address, siz static int _Py_RemoteDebug_ReadRemoteMemory(proc_handle_t *handle, uintptr_t remote_address, size_t len, void* dst) { + if (len == 0) { + return 0; + } #ifdef MS_WINDOWS SIZE_T read_bytes = 0; SIZE_T result = 0; do { if (!ReadProcessMemory(handle->hProcess, (LPCVOID)(remote_address + result), (char*)dst + result, len - result, &read_bytes)) { + DWORD error = GetLastError(); // Check if the process is still alive: we need to be able to tell our caller // that the process is dead and not just that the read failed. if (!is_process_alive(handle->hProcess)) { @@ -1074,14 +1207,20 @@ _Py_RemoteDebug_ReadRemoteMemory(proc_handle_t *handle, uintptr_t remote_address PyErr_SetFromErrno(PyExc_OSError); return -1; } - PyErr_SetFromWindowsErr(0); - DWORD error = GetLastError(); + PyErr_SetFromWindowsErr(error); _set_debug_exception_cause(PyExc_OSError, "ReadProcessMemory failed for PID %d at address 0x%lx " "(size %zu, partial read %zu bytes): Windows error %lu", handle->pid, remote_address + result, len - result, result, error); return -1; } + if (read_bytes == 0) { + PyErr_Format(PyExc_OSError, + "ReadProcessMemory returned 0 bytes for PID %d at address 0x%lx " + "(size %zu, partial read %zu bytes)", + handle->pid, remote_address + result, len - result, result); + return -1; + } result += read_bytes; } while (result < len); return 0; @@ -1102,31 +1241,40 @@ _Py_RemoteDebug_ReadRemoteMemory(proc_handle_t *handle, uintptr_t remote_address read_bytes = process_vm_readv(handle->pid, local, 1, remote, 1, 0); if (read_bytes < 0) { - if (errno == ENOSYS) { + int err = errno; + if (err == ENOSYS) { return read_remote_memory_fallback(handle, remote_address, len, dst); } + errno = err; PyErr_SetFromErrno(PyExc_OSError); - if (errno == ESRCH) { + if (err == ESRCH) { return -1; } _set_debug_exception_cause(PyExc_OSError, "process_vm_readv failed for PID %d at address 0x%lx " "(size %zu, partial read %zd bytes): %s", - handle->pid, remote_address + result, len - result, result, strerror(errno)); + handle->pid, remote_address + result, len - result, result, strerror(err)); return -1; } + if (read_bytes == 0) { + PyErr_Format(PyExc_OSError, + "process_vm_readv returned 0 bytes for PID %d at address 0x%lx " + "(size %zu, partial read %zd bytes)", + handle->pid, remote_address + result, len - result, result); + return -1; + } result += read_bytes; } while ((size_t)read_bytes != local[0].iov_len); return 0; #elif defined(__APPLE__) && defined(TARGET_OS_OSX) && TARGET_OS_OSX - Py_ssize_t result = -1; + mach_vm_size_t bytes_read = 0; kern_return_t kr = mach_vm_read_overwrite( handle->task, (mach_vm_address_t)remote_address, len, (mach_vm_address_t)dst, - (mach_vm_size_t*)&result); + &bytes_read); if (kr != KERN_SUCCESS) { switch (err_get_code(kr)) { @@ -1170,6 +1318,13 @@ _Py_RemoteDebug_ReadRemoteMemory(proc_handle_t *handle, uintptr_t remote_address } return -1; } + if (bytes_read != (mach_vm_size_t)len) { + PyErr_Format(PyExc_OSError, + "mach_vm_read_overwrite read %llu of %zu bytes for PID %d at " + "address 0x%lx", + (unsigned long long)bytes_read, len, handle->pid, remote_address); + return -1; + } return 0; #else Py_UNREACHABLE(); @@ -1181,6 +1336,9 @@ _Py_RemoteDebug_ReadRemoteMemory(proc_handle_t *handle, uintptr_t remote_address static int _Py_RemoteDebug_WriteRemoteMemoryFallback(proc_handle_t *handle, uintptr_t remote_address, size_t len, const void* src) { + if (len == 0) { + return 0; + } if (handle->memfd == -1) { if (open_proc_mem_fd(handle) < 0) { return -1; @@ -1198,10 +1356,19 @@ _Py_RemoteDebug_WriteRemoteMemoryFallback(proc_handle_t *handle, uintptr_t remot written = pwritev(handle->memfd, local, 1, offset); if (written < 0) { + int err = errno; + errno = err; PyErr_SetFromErrno(PyExc_OSError); return -1; } + if (written == 0) { + PyErr_Format(PyExc_OSError, + "pwritev wrote 0 bytes for PID %d at address 0x%lx " + "(size %zu, partial write %zd bytes)", + handle->pid, remote_address + result, len - result, result); + return -1; + } result += written; } while ((size_t)written != local[0].iov_len); return 0; @@ -1212,19 +1379,29 @@ _Py_RemoteDebug_WriteRemoteMemoryFallback(proc_handle_t *handle, uintptr_t remot UNUSED static int _Py_RemoteDebug_WriteRemoteMemory(proc_handle_t *handle, uintptr_t remote_address, size_t len, const void* src) { + if (len == 0) { + return 0; + } #ifdef MS_WINDOWS SIZE_T written = 0; SIZE_T result = 0; do { if (!WriteProcessMemory(handle->hProcess, (LPVOID)(remote_address + result), (const char*)src + result, len - result, &written)) { - PyErr_SetFromWindowsErr(0); DWORD error = GetLastError(); + PyErr_SetFromWindowsErr(error); _set_debug_exception_cause(PyExc_OSError, "WriteProcessMemory failed for PID %d at address 0x%lx " "(size %zu, partial write %zu bytes): Windows error %lu", handle->pid, remote_address + result, len - result, result, error); return -1; } + if (written == 0) { + PyErr_Format(PyExc_OSError, + "WriteProcessMemory wrote 0 bytes for PID %d at address 0x%lx " + "(size %zu, partial write %zu bytes)", + handle->pid, remote_address + result, len - result, result); + return -1; + } result += written; } while (result < len); return 0; @@ -1245,17 +1422,26 @@ _Py_RemoteDebug_WriteRemoteMemory(proc_handle_t *handle, uintptr_t remote_addres written = process_vm_writev(handle->pid, local, 1, remote, 1, 0); if (written < 0) { - if (errno == ENOSYS) { + int err = errno; + if (err == ENOSYS) { return _Py_RemoteDebug_WriteRemoteMemoryFallback(handle, remote_address, len, src); } + errno = err; PyErr_SetFromErrno(PyExc_OSError); _set_debug_exception_cause(PyExc_OSError, "process_vm_writev failed for PID %d at address 0x%lx " "(size %zu, partial write %zd bytes): %s", - handle->pid, remote_address + result, len - result, result, strerror(errno)); + handle->pid, remote_address + result, len - result, result, strerror(err)); return -1; } + if (written == 0) { + PyErr_Format(PyExc_OSError, + "process_vm_writev wrote 0 bytes for PID %d at address 0x%lx " + "(size %zu, partial write %zd bytes)", + handle->pid, remote_address + result, len - result, result); + return -1; + } result += written; } while ((size_t)written != local[0].iov_len); return 0;