diff options
27 files changed, 319 insertions, 137 deletions
diff --git a/Doc/library/faulthandler.rst b/Doc/library/faulthandler.rst index 1977f4d3ba3..677966a8b2e 100644 --- a/Doc/library/faulthandler.rst +++ b/Doc/library/faulthandler.rst @@ -228,6 +228,41 @@ handler: Fatal Python error: Segmentation fault Current thread 0x00007fb899f39700 (most recent call first): - File "/home/python/cpython/Lib/ctypes/__init__.py", line 486 in string_at + File "/opt/python/Lib/ctypes/__init__.py", line 486 in string_at File "<stdin>", line 1 in <module> + + Current thread's C stack trace (most recent call first): + Binary file "/opt/python/python", at _Py_DumpStack+0x42 [0x5b27f7d7147e] + Binary file "/opt/python/python", at +0x32dcbd [0x5b27f7d85cbd] + Binary file "/opt/python/python", at +0x32df8a [0x5b27f7d85f8a] + Binary file "/usr/lib/libc.so.6", at +0x3def0 [0x77b73226bef0] + Binary file "/usr/lib/libc.so.6", at +0x17ef9c [0x77b7323acf9c] + Binary file "/opt/python/build/lib.linux-x86_64-3.15/_ctypes.cpython-315d-x86_64-linux-gnu.so", at +0xcdf6 [0x77b7315dddf6] + Binary file "/usr/lib/libffi.so.8", at +0x7976 [0x77b73158f976] + Binary file "/usr/lib/libffi.so.8", at +0x413c [0x77b73158c13c] + Binary file "/usr/lib/libffi.so.8", at ffi_call+0x12e [0x77b73158ef0e] + Binary file "/opt/python/build/lib.linux-x86_64-3.15/_ctypes.cpython-315d-x86_64-linux-gnu.so", at +0x15a33 [0x77b7315e6a33] + Binary file "/opt/python/build/lib.linux-x86_64-3.15/_ctypes.cpython-315d-x86_64-linux-gnu.so", at +0x164fa [0x77b7315e74fa] + Binary file "/opt/python/build/lib.linux-x86_64-3.15/_ctypes.cpython-315d-x86_64-linux-gnu.so", at +0xc624 [0x77b7315dd624] + Binary file "/opt/python/python", at _PyObject_MakeTpCall+0xce [0x5b27f7b73883] + Binary file "/opt/python/python", at +0x11bab6 [0x5b27f7b73ab6] + Binary file "/opt/python/python", at PyObject_Vectorcall+0x23 [0x5b27f7b73b04] + Binary file "/opt/python/python", at _PyEval_EvalFrameDefault+0x490c [0x5b27f7cbb302] + Binary file "/opt/python/python", at +0x2818e6 [0x5b27f7cd98e6] + Binary file "/opt/python/python", at +0x281aab [0x5b27f7cd9aab] + Binary file "/opt/python/python", at PyEval_EvalCode+0xc5 [0x5b27f7cd9ba3] + Binary file "/opt/python/python", at +0x255957 [0x5b27f7cad957] + Binary file "/opt/python/python", at +0x255ab4 [0x5b27f7cadab4] + Binary file "/opt/python/python", at _PyEval_EvalFrameDefault+0x6c3e [0x5b27f7cbd634] + Binary file "/opt/python/python", at +0x2818e6 [0x5b27f7cd98e6] + Binary file "/opt/python/python", at +0x281aab [0x5b27f7cd9aab] + Binary file "/opt/python/python", at +0x11b6e1 [0x5b27f7b736e1] + Binary file "/opt/python/python", at +0x11d348 [0x5b27f7b75348] + Binary file "/opt/python/python", at +0x11d626 [0x5b27f7b75626] + Binary file "/opt/python/python", at PyObject_Call+0x20 [0x5b27f7b7565e] + Binary file "/opt/python/python", at +0x32a67a [0x5b27f7d8267a] + Binary file "/opt/python/python", at +0x32a7f8 [0x5b27f7d827f8] + Binary file "/opt/python/python", at +0x32ac1b [0x5b27f7d82c1b] + Binary file "/opt/python/python", at Py_RunMain+0x31 [0x5b27f7d82ebe] + <truncated rest of calls> Segmentation fault diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index a74d414ae4b..8260f8ddc4e 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -2640,7 +2640,9 @@ pty sqlite3 ------- -* Remove :data:`!version` and :data:`!version_info` from :mod:`sqlite3`. +* Remove :data:`!version` and :data:`!version_info` from :mod:`sqlite3`; + use :data:`~sqlite3.sqlite_version` and :data:`~sqlite3.sqlite_version_info` + for the actual version number of the runtime SQLite library. (Contributed by Hugo van Kemenade in :gh:`118924`.) * Disallow using a sequence of parameters with named placeholders. diff --git a/Include/internal/mimalloc/mimalloc/types.h b/Include/internal/mimalloc/mimalloc/types.h index a04169f7fb8..a17f637fe68 100644 --- a/Include/internal/mimalloc/mimalloc/types.h +++ b/Include/internal/mimalloc/mimalloc/types.h @@ -705,7 +705,7 @@ void _mi_stat_counter_increase(mi_stat_counter_t* stat, size_t amount); // Thread Local data // ------------------------------------------------------ -// A "span" is is an available range of slices. The span queues keep +// A "span" is an available range of slices. The span queues keep // track of slice spans of at most the given `slice_count` (but more than the previous size class). typedef struct mi_span_queue_s { mi_slice_t* first; diff --git a/Include/internal/pycore_global_objects_fini_generated.h b/Include/internal/pycore_global_objects_fini_generated.h index e118b86db50..c461bc1786d 100644 --- a/Include/internal/pycore_global_objects_fini_generated.h +++ b/Include/internal/pycore_global_objects_fini_generated.h @@ -1136,6 +1136,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(offset_src)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(on_type_read)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(onceregistry)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(only_active_thread)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(only_keys)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(oparg)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(opcode)); diff --git a/Include/internal/pycore_global_strings.h b/Include/internal/pycore_global_strings.h index 36f3d23d095..72c2051bd97 100644 --- a/Include/internal/pycore_global_strings.h +++ b/Include/internal/pycore_global_strings.h @@ -627,6 +627,7 @@ struct _Py_global_strings { STRUCT_FOR_ID(offset_src) STRUCT_FOR_ID(on_type_read) STRUCT_FOR_ID(onceregistry) + STRUCT_FOR_ID(only_active_thread) STRUCT_FOR_ID(only_keys) STRUCT_FOR_ID(oparg) STRUCT_FOR_ID(opcode) diff --git a/Include/internal/pycore_runtime_init_generated.h b/Include/internal/pycore_runtime_init_generated.h index d172cc1485d..d378fcae26c 100644 --- a/Include/internal/pycore_runtime_init_generated.h +++ b/Include/internal/pycore_runtime_init_generated.h @@ -1134,6 +1134,7 @@ extern "C" { INIT_ID(offset_src), \ INIT_ID(on_type_read), \ INIT_ID(onceregistry), \ + INIT_ID(only_active_thread), \ INIT_ID(only_keys), \ INIT_ID(oparg), \ INIT_ID(opcode), \ diff --git a/Include/internal/pycore_unicodeobject_generated.h b/Include/internal/pycore_unicodeobject_generated.h index 0a9be4e41ac..e516211f6c6 100644 --- a/Include/internal/pycore_unicodeobject_generated.h +++ b/Include/internal/pycore_unicodeobject_generated.h @@ -2296,6 +2296,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(only_active_thread); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(only_keys); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); diff --git a/Include/refcount.h b/Include/refcount.h index 034c453f449..65a4e63a8b0 100644 --- a/Include/refcount.h +++ b/Include/refcount.h @@ -30,7 +30,7 @@ increase and decrease the objects reference count. In order to offer sufficient resilience to C extensions using the stable ABI compiled against 3.11 or earlier, we set the initial value near the -middle of the range (2**31, 2**32). That way the the refcount can be +middle of the range (2**31, 2**32). That way the refcount can be off by ~1 billion without affecting immortality. Reference count increases will use saturated arithmetic, taking advantage of diff --git a/InternalDocs/garbage_collector.md b/InternalDocs/garbage_collector.md index 4da6cd47dc8..9c35684c945 100644 --- a/InternalDocs/garbage_collector.md +++ b/InternalDocs/garbage_collector.md @@ -286,7 +286,7 @@ object, the GC does not process it twice. Notice that an object that was marked as "tentatively unreachable" and was later moved back to the reachable list will be visited again by the garbage collector -as now all the references that that object has need to be processed as well. This +as now all the references that the object has need to be processed as well. This process is really a breadth first search over the object graph. Once all the objects are scanned, the GC knows that all container objects in the tentatively unreachable list are really unreachable and can thus be garbage collected. diff --git a/Lib/idlelib/configdialog.py b/Lib/idlelib/configdialog.py index 4d2adb48570..e618ef07a90 100644 --- a/Lib/idlelib/configdialog.py +++ b/Lib/idlelib/configdialog.py @@ -435,7 +435,7 @@ class FontPage(Frame): self.font_name.set(font.lower()) def set_samples(self, event=None): - """Update update both screen samples with the font settings. + """Update both screen samples with the font settings. Called on font initialization and change events. Accesses font_name, font_size, and font_bold Variables. diff --git a/Lib/idlelib/debugger.py b/Lib/idlelib/debugger.py index d90dbcd11f9..1fae1d4b0ad 100644 --- a/Lib/idlelib/debugger.py +++ b/Lib/idlelib/debugger.py @@ -1,6 +1,6 @@ """Debug user code with a GUI interface to a subclass of bdb.Bdb. -The Idb idb and Debugger gui instances each need a reference to each +The Idb instance 'idb' and Debugger instance 'gui' need references to each other or to an rpc proxy for each other. If IDLE is started with '-n', so that user code and idb both run in the diff --git a/Lib/idlelib/editor.py b/Lib/idlelib/editor.py index c76db20c587..17b498f63ba 100644 --- a/Lib/idlelib/editor.py +++ b/Lib/idlelib/editor.py @@ -1649,7 +1649,7 @@ class IndentSearcher: self.finished = 1 def run(self): - """Return 2 lines containing block opener and and indent. + """Return 2 lines containing block opener and indent. Either the indent line or both may be None. """ diff --git a/Lib/idlelib/idle_test/htest.py b/Lib/idlelib/idle_test/htest.py index a7293774eec..b63ff9ec287 100644 --- a/Lib/idlelib/idle_test/htest.py +++ b/Lib/idlelib/idle_test/htest.py @@ -337,7 +337,7 @@ _tree_widget_spec = { 'file': 'tree', 'kwds': {}, 'msg': "The canvas is scrollable.\n" - "Click on folders up to to the lowest level." + "Click on folders up to the lowest level." } _undo_delegator_spec = { diff --git a/Lib/platform.py b/Lib/platform.py index e7f180fc5ac..da15bb4717b 100644 --- a/Lib/platform.py +++ b/Lib/platform.py @@ -612,6 +612,9 @@ def system_alias(system, release, version): ### Various internal helpers +# Table for cleaning up characters in filenames. +_SIMPLE_SUBSTITUTIONS = str.maketrans(r' /\:;"()', r'_-------') + def _platform(*args): """ Helper to format the platform string in a filename @@ -621,28 +624,13 @@ def _platform(*args): platform = '-'.join(x.strip() for x in filter(len, args)) # Cleanup some possible filename obstacles... - platform = platform.replace(' ', '_') - platform = platform.replace('/', '-') - platform = platform.replace('\\', '-') - platform = platform.replace(':', '-') - platform = platform.replace(';', '-') - platform = platform.replace('"', '-') - platform = platform.replace('(', '-') - platform = platform.replace(')', '-') + platform = platform.translate(_SIMPLE_SUBSTITUTIONS) # No need to report 'unknown' information... platform = platform.replace('unknown', '') # Fold '--'s and remove trailing '-' - while True: - cleaned = platform.replace('--', '-') - if cleaned == platform: - break - platform = cleaned - while platform and platform[-1] == '-': - platform = platform[:-1] - - return platform + return re.sub(r'-{2,}', '-', platform).rstrip('-') def _node(default=''): diff --git a/Lib/test/test_external_inspection.py b/Lib/test/test_external_inspection.py index 90214e814f2..0f31c225e68 100644 --- a/Lib/test/test_external_inspection.py +++ b/Lib/test/test_external_inspection.py @@ -7,7 +7,7 @@ import socket import threading from asyncio import staggered, taskgroups, base_events, tasks from unittest.mock import ANY -from test.support import os_helper, SHORT_TIMEOUT, busy_retry +from test.support import os_helper, SHORT_TIMEOUT, busy_retry, requires_gil_enabled from test.support.script_helper import make_script from test.support.socket_helper import find_unused_port @@ -876,6 +876,126 @@ class TestGetStackTrace(unittest.TestCase): ], ) + @skip_if_not_supported + @unittest.skipIf( + sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, + "Test only runs on Linux with process_vm_readv support", + ) + @requires_gil_enabled("Free threaded builds don't have an 'active thread'") + def test_only_active_thread(self): + # Test that only_active_thread parameter works correctly + port = find_unused_port() + script = textwrap.dedent( + f"""\ + import time, sys, socket, threading + + # Connect to the test process + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect(('localhost', {port})) + + def worker_thread(name, barrier, ready_event): + barrier.wait() # Synchronize thread start + ready_event.wait() # Wait for main thread signal + # Sleep to keep thread alive + time.sleep(10_000) + + def main_work(): + # Do busy work to hold the GIL + sock.sendall(b"working\\n") + count = 0 + while count < 100000000: + count += 1 + if count % 10000000 == 0: + pass # Keep main thread busy + sock.sendall(b"done\\n") + + # Create synchronization primitives + num_threads = 3 + barrier = threading.Barrier(num_threads + 1) # +1 for main thread + ready_event = threading.Event() + + # Start worker threads + threads = [] + for i in range(num_threads): + t = threading.Thread(target=worker_thread, args=(f"Worker-{{i}}", barrier, ready_event)) + t.start() + threads.append(t) + + # Wait for all threads to be ready + barrier.wait() + + # Signal ready to parent process + sock.sendall(b"ready\\n") + + # Signal threads to start waiting + ready_event.set() + + # Give threads time to start sleeping + time.sleep(0.1) + + # Now do busy work to hold the GIL + main_work() + """ + ) + + with os_helper.temp_dir() as work_dir: + script_dir = os.path.join(work_dir, "script_pkg") + os.mkdir(script_dir) + + # Create a socket server to communicate with the target process + server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server_socket.bind(("localhost", port)) + server_socket.settimeout(SHORT_TIMEOUT) + server_socket.listen(1) + + script_name = _make_test_script(script_dir, "script", script) + client_socket = None + try: + p = subprocess.Popen([sys.executable, script_name]) + client_socket, _ = server_socket.accept() + server_socket.close() + + # Wait for ready signal + response = b"" + while b"ready" not in response: + response += client_socket.recv(1024) + + # Wait for the main thread to start its busy work + while b"working" not in response: + response += client_socket.recv(1024) + + # Get stack trace with all threads + unwinder_all = RemoteUnwinder(p.pid, all_threads=True) + all_traces = unwinder_all.get_stack_trace() + + # Get stack trace with only GIL holder + unwinder_gil = RemoteUnwinder(p.pid, only_active_thread=True) + gil_traces = unwinder_gil.get_stack_trace() + + except PermissionError: + self.skipTest( + "Insufficient permissions to read the stack trace" + ) + finally: + if client_socket is not None: + client_socket.close() + p.kill() + p.terminate() + p.wait(timeout=SHORT_TIMEOUT) + + # Verify we got multiple threads in all_traces + self.assertGreater(len(all_traces), 1, "Should have multiple threads") + + # Verify we got exactly one thread in gil_traces + self.assertEqual(len(gil_traces), 1, "Should have exactly one GIL holder") + + # The GIL holder should be in the all_traces list + gil_thread_id = gil_traces[0][0] + all_thread_ids = [trace[0] for trace in all_traces] + self.assertIn(gil_thread_id, all_thread_ids, + "GIL holder should be among all threads") + if __name__ == "__main__": unittest.main() diff --git a/Lib/test/test_iter.py b/Lib/test/test_iter.py index 1b9f3cf7624..18e4b676c53 100644 --- a/Lib/test/test_iter.py +++ b/Lib/test/test_iter.py @@ -1147,7 +1147,7 @@ class TestCase(unittest.TestCase): def test_exception_locations(self): # The location of an exception raised from __init__ or - # __next__ should should be the iterator expression + # __next__ should be the iterator expression def init_raises(): try: diff --git a/Lib/test/test_listcomps.py b/Lib/test/test_listcomps.py index cffdeeacc5d..70148dc30fc 100644 --- a/Lib/test/test_listcomps.py +++ b/Lib/test/test_listcomps.py @@ -716,7 +716,7 @@ class ListComprehensionTest(unittest.TestCase): def test_exception_locations(self): # The location of an exception raised from __init__ or - # __next__ should should be the iterator expression + # __next__ should be the iterator expression def init_raises(): try: diff --git a/Lib/test/test_platform.py b/Lib/test/test_platform.py index 3688cc4267b..479649053ab 100644 --- a/Lib/test/test_platform.py +++ b/Lib/test/test_platform.py @@ -133,6 +133,22 @@ class PlatformTest(unittest.TestCase): for terse in (False, True): res = platform.platform(aliased, terse) + def test__platform(self): + for src, res in [ + ('foo bar', 'foo_bar'), + ( + '1/2\\3:4;5"6(7)8(7)6"5;4:3\\2/1', + '1-2-3-4-5-6-7-8-7-6-5-4-3-2-1' + ), + ('--', ''), + ('-f', '-f'), + ('-foo----', '-foo'), + ('--foo---', '-foo'), + ('---foo--', '-foo'), + ]: + with self.subTest(src=src): + self.assertEqual(platform._platform(src), res) + def test_system(self): res = platform.system() diff --git a/Misc/ACKS b/Misc/ACKS index 6ab50763fea..d1490e1e46c 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -478,6 +478,7 @@ Dean Draayer Fred L. Drake, Jr. Mehdi Drissi Derk Drukker +Weilin Du John DuBois Paul Dubois Jacques Ducasse diff --git a/Misc/NEWS.d/next/Security/2025-06-27-21-23-19.gh-issue-136053.QZxcee.rst b/Misc/NEWS.d/next/Security/2025-06-27-21-23-19.gh-issue-136053.QZxcee.rst new file mode 100644 index 00000000000..93caed3aa3b --- /dev/null +++ b/Misc/NEWS.d/next/Security/2025-06-27-21-23-19.gh-issue-136053.QZxcee.rst @@ -0,0 +1 @@ +:mod:`marshal`: fix a possible crash when deserializing :class:`slice` objects. diff --git a/Modules/_remote_debugging_module.c b/Modules/_remote_debugging_module.c index c2421cac6bd..ce7189637c2 100644 --- a/Modules/_remote_debugging_module.c +++ b/Modules/_remote_debugging_module.c @@ -64,12 +64,14 @@ #endif #ifdef Py_GIL_DISABLED -#define INTERP_STATE_MIN_SIZE MAX(MAX(offsetof(PyInterpreterState, _code_object_generation) + sizeof(uint64_t), \ - offsetof(PyInterpreterState, tlbc_indices.tlbc_generation) + sizeof(uint32_t)), \ - offsetof(PyInterpreterState, threads.head) + sizeof(void*)) +#define INTERP_STATE_MIN_SIZE MAX(MAX(MAX(offsetof(PyInterpreterState, _code_object_generation) + sizeof(uint64_t), \ + offsetof(PyInterpreterState, tlbc_indices.tlbc_generation) + sizeof(uint32_t)), \ + offsetof(PyInterpreterState, threads.head) + sizeof(void*)), \ + offsetof(PyInterpreterState, _gil.last_holder) + sizeof(PyThreadState*)) #else -#define INTERP_STATE_MIN_SIZE MAX(offsetof(PyInterpreterState, _code_object_generation) + sizeof(uint64_t), \ - offsetof(PyInterpreterState, threads.head) + sizeof(void*)) +#define INTERP_STATE_MIN_SIZE MAX(MAX(offsetof(PyInterpreterState, _code_object_generation) + sizeof(uint64_t), \ + offsetof(PyInterpreterState, threads.head) + sizeof(void*)), \ + offsetof(PyInterpreterState, _gil.last_holder) + sizeof(PyThreadState*)) #endif #define INTERP_STATE_BUFFER_SIZE MAX(INTERP_STATE_MIN_SIZE, 256) @@ -206,6 +208,7 @@ typedef struct { uint64_t code_object_generation; _Py_hashtable_t *code_object_cache; int debug; + int only_active_thread; RemoteDebuggingState *cached_state; // Cached module state #ifdef Py_GIL_DISABLED // TLBC cache invalidation tracking @@ -2496,6 +2499,7 @@ _remote_debugging.RemoteUnwinder.__init__ pid: int * all_threads: bool = False + only_active_thread: bool = False debug: bool = False Initialize a new RemoteUnwinder object for debugging a remote Python process. @@ -2504,6 +2508,8 @@ Args: pid: Process ID of the target Python process to debug all_threads: If True, initialize state for all threads in the process. If False, only initialize for the main thread. + only_active_thread: If True, only sample the thread holding the GIL. + Cannot be used together with all_threads=True. debug: If True, chain exceptions to explain the sequence of events that lead to the exception. @@ -2514,15 +2520,33 @@ Raises: PermissionError: If access to the target process is denied OSError: If unable to attach to the target process or access its memory RuntimeError: If unable to read debug information from the target process + ValueError: If both all_threads and only_active_thread are True [clinic start generated code]*/ static int _remote_debugging_RemoteUnwinder___init___impl(RemoteUnwinderObject *self, int pid, int all_threads, + int only_active_thread, int debug) -/*[clinic end generated code: output=3982f2a7eba49334 input=48a762566b828e91]*/ +/*[clinic end generated code: output=13ba77598ecdcbe1 input=8f8f12504e17da04]*/ { + // Validate that all_threads and only_active_thread are not both True + if (all_threads && only_active_thread) { + PyErr_SetString(PyExc_ValueError, + "all_threads and only_active_thread cannot both be True"); + return -1; + } + +#ifdef Py_GIL_DISABLED + if (only_active_thread) { + PyErr_SetString(PyExc_ValueError, + "only_active_thread is not supported when Py_GIL_DISABLED is not defined"); + return -1; + } +#endif + self->debug = debug; + self->only_active_thread = only_active_thread; self->cached_state = NULL; if (_Py_RemoteDebug_InitProcHandle(&self->handle, pid) < 0) { set_exception_cause(self, PyExc_RuntimeError, "Failed to initialize process handle"); @@ -2602,13 +2626,18 @@ _remote_debugging_RemoteUnwinder___init___impl(RemoteUnwinderObject *self, @critical_section _remote_debugging.RemoteUnwinder.get_stack_trace -Returns a list of stack traces for all threads in the target process. +Returns a list of stack traces for threads in the target process. Each element in the returned list is a tuple of (thread_id, frame_list), where: - thread_id is the OS thread identifier - frame_list is a list of tuples (function_name, filename, line_number) representing the Python stack frames for that thread, ordered from most recent to oldest +The threads returned depend on the initialization parameters: +- If only_active_thread was True: returns only the thread holding the GIL +- If all_threads was True: returns all threads +- Otherwise: returns only the main thread + Example: [ (1234, [ @@ -2632,7 +2661,7 @@ Raises: static PyObject * _remote_debugging_RemoteUnwinder_get_stack_trace_impl(RemoteUnwinderObject *self) -/*[clinic end generated code: output=666192b90c69d567 input=331dbe370578badf]*/ +/*[clinic end generated code: output=666192b90c69d567 input=f756f341206f9116]*/ { PyObject* result = NULL; // Read interpreter state into opaque buffer @@ -2655,6 +2684,28 @@ _remote_debugging_RemoteUnwinder_get_stack_trace_impl(RemoteUnwinderObject *self _Py_hashtable_clear(self->code_object_cache); } + // If only_active_thread is true, we need to determine which thread holds the GIL + PyThreadState* gil_holder = NULL; + if (self->only_active_thread) { + // The GIL state is already in interp_state_buffer, just read from there + // Check if GIL is locked + int gil_locked = GET_MEMBER(int, interp_state_buffer, + self->debug_offsets.interpreter_state.gil_runtime_state_locked); + + if (gil_locked) { + // Get the last holder (current holder when GIL is locked) + gil_holder = GET_MEMBER(PyThreadState*, interp_state_buffer, + self->debug_offsets.interpreter_state.gil_runtime_state_holder); + } else { + // GIL is not locked, return empty list + result = PyList_New(0); + if (!result) { + set_exception_cause(self, PyExc_MemoryError, "Failed to create empty result list"); + } + goto exit; + } + } + #ifdef Py_GIL_DISABLED // Check TLBC generation and invalidate cache if needed uint32_t current_tlbc_generation = GET_MEMBER(uint32_t, interp_state_buffer, @@ -2666,7 +2717,10 @@ _remote_debugging_RemoteUnwinder_get_stack_trace_impl(RemoteUnwinderObject *self #endif uintptr_t current_tstate; - if (self->tstate_addr == 0) { + if (self->only_active_thread && gil_holder != NULL) { + // We have the GIL holder, process only that thread + current_tstate = (uintptr_t)gil_holder; + } else if (self->tstate_addr == 0) { // Get threads head from buffer current_tstate = GET_MEMBER(uintptr_t, interp_state_buffer, self->debug_offsets.interpreter_state.threads_head); @@ -2700,10 +2754,14 @@ _remote_debugging_RemoteUnwinder_get_stack_trace_impl(RemoteUnwinderObject *self if (self->tstate_addr) { break; } + + // If we're only processing the GIL holder, we're done after one iteration + if (self->only_active_thread && gil_holder != NULL) { + break; + } } exit: - _Py_RemoteDebug_ClearCache(&self->handle); return result; } @@ -2827,11 +2885,9 @@ _remote_debugging_RemoteUnwinder_get_all_awaited_by_impl(RemoteUnwinderObject *s goto result_err; } - _Py_RemoteDebug_ClearCache(&self->handle); return result; result_err: - _Py_RemoteDebug_ClearCache(&self->handle); Py_XDECREF(result); return NULL; } @@ -2898,11 +2954,9 @@ _remote_debugging_RemoteUnwinder_get_async_stack_trace_impl(RemoteUnwinderObject goto cleanup; } - _Py_RemoteDebug_ClearCache(&self->handle); return result; cleanup: - _Py_RemoteDebug_ClearCache(&self->handle); Py_XDECREF(result); return NULL; } @@ -2928,7 +2982,6 @@ RemoteUnwinder_dealloc(PyObject *op) } #endif if (self->handle.pid != 0) { - _Py_RemoteDebug_ClearCache(&self->handle); _Py_RemoteDebug_CleanupProcHandle(&self->handle); } PyObject_Del(self); diff --git a/Modules/clinic/_remote_debugging_module.c.h b/Modules/clinic/_remote_debugging_module.c.h index 5c313a2d664..e80b24b54c0 100644 --- a/Modules/clinic/_remote_debugging_module.c.h +++ b/Modules/clinic/_remote_debugging_module.c.h @@ -10,7 +10,8 @@ preserve #include "pycore_modsupport.h" // _PyArg_UnpackKeywords() PyDoc_STRVAR(_remote_debugging_RemoteUnwinder___init____doc__, -"RemoteUnwinder(pid, *, all_threads=False, debug=False)\n" +"RemoteUnwinder(pid, *, all_threads=False, only_active_thread=False,\n" +" debug=False)\n" "--\n" "\n" "Initialize a new RemoteUnwinder object for debugging a remote Python process.\n" @@ -19,6 +20,8 @@ PyDoc_STRVAR(_remote_debugging_RemoteUnwinder___init____doc__, " pid: Process ID of the target Python process to debug\n" " all_threads: If True, initialize state for all threads in the process.\n" " If False, only initialize for the main thread.\n" +" only_active_thread: If True, only sample the thread holding the GIL.\n" +" Cannot be used together with all_threads=True.\n" " debug: If True, chain exceptions to explain the sequence of events that\n" " lead to the exception.\n" "\n" @@ -28,11 +31,13 @@ PyDoc_STRVAR(_remote_debugging_RemoteUnwinder___init____doc__, "Raises:\n" " PermissionError: If access to the target process is denied\n" " OSError: If unable to attach to the target process or access its memory\n" -" RuntimeError: If unable to read debug information from the target process"); +" RuntimeError: If unable to read debug information from the target process\n" +" ValueError: If both all_threads and only_active_thread are True"); static int _remote_debugging_RemoteUnwinder___init___impl(RemoteUnwinderObject *self, int pid, int all_threads, + int only_active_thread, int debug); static int @@ -41,7 +46,7 @@ _remote_debugging_RemoteUnwinder___init__(PyObject *self, PyObject *args, PyObje int return_value = -1; #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) - #define NUM_KEYWORDS 3 + #define NUM_KEYWORDS 4 static struct { PyGC_Head _this_is_not_used; PyObject_VAR_HEAD @@ -50,7 +55,7 @@ _remote_debugging_RemoteUnwinder___init__(PyObject *self, PyObject *args, PyObje } _kwtuple = { .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) .ob_hash = -1, - .ob_item = { &_Py_ID(pid), &_Py_ID(all_threads), &_Py_ID(debug), }, + .ob_item = { &_Py_ID(pid), &_Py_ID(all_threads), &_Py_ID(only_active_thread), &_Py_ID(debug), }, }; #undef NUM_KEYWORDS #define KWTUPLE (&_kwtuple.ob_base.ob_base) @@ -59,19 +64,20 @@ _remote_debugging_RemoteUnwinder___init__(PyObject *self, PyObject *args, PyObje # define KWTUPLE NULL #endif // !Py_BUILD_CORE - static const char * const _keywords[] = {"pid", "all_threads", "debug", NULL}; + static const char * const _keywords[] = {"pid", "all_threads", "only_active_thread", "debug", NULL}; static _PyArg_Parser _parser = { .keywords = _keywords, .fname = "RemoteUnwinder", .kwtuple = KWTUPLE, }; #undef KWTUPLE - PyObject *argsbuf[3]; + PyObject *argsbuf[4]; PyObject * const *fastargs; Py_ssize_t nargs = PyTuple_GET_SIZE(args); Py_ssize_t noptargs = nargs + (kwargs ? PyDict_GET_SIZE(kwargs) : 0) - 1; int pid; int all_threads = 0; + int only_active_thread = 0; int debug = 0; fastargs = _PyArg_UnpackKeywords(_PyTuple_CAST(args)->ob_item, nargs, kwargs, NULL, &_parser, @@ -95,12 +101,21 @@ _remote_debugging_RemoteUnwinder___init__(PyObject *self, PyObject *args, PyObje goto skip_optional_kwonly; } } - debug = PyObject_IsTrue(fastargs[2]); + if (fastargs[2]) { + only_active_thread = PyObject_IsTrue(fastargs[2]); + if (only_active_thread < 0) { + goto exit; + } + if (!--noptargs) { + goto skip_optional_kwonly; + } + } + debug = PyObject_IsTrue(fastargs[3]); if (debug < 0) { goto exit; } skip_optional_kwonly: - return_value = _remote_debugging_RemoteUnwinder___init___impl((RemoteUnwinderObject *)self, pid, all_threads, debug); + return_value = _remote_debugging_RemoteUnwinder___init___impl((RemoteUnwinderObject *)self, pid, all_threads, only_active_thread, debug); exit: return return_value; @@ -110,13 +125,18 @@ PyDoc_STRVAR(_remote_debugging_RemoteUnwinder_get_stack_trace__doc__, "get_stack_trace($self, /)\n" "--\n" "\n" -"Returns a list of stack traces for all threads in the target process.\n" +"Returns a list of stack traces for threads in the target process.\n" "\n" "Each element in the returned list is a tuple of (thread_id, frame_list), where:\n" "- thread_id is the OS thread identifier\n" "- frame_list is a list of tuples (function_name, filename, line_number) representing\n" " the Python stack frames for that thread, ordered from most recent to oldest\n" "\n" +"The threads returned depend on the initialization parameters:\n" +"- If only_active_thread was True: returns only the thread holding the GIL\n" +"- If all_threads was True: returns all threads\n" +"- Otherwise: returns only the main thread\n" +"\n" "Example:\n" " [\n" " (1234, [\n" @@ -253,4 +273,4 @@ _remote_debugging_RemoteUnwinder_get_async_stack_trace(PyObject *self, PyObject return return_value; } -/*[clinic end generated code: output=774ec34aa653402d input=a9049054013a1b77]*/ +/*[clinic end generated code: output=a37ab223d5081b16 input=a9049054013a1b77]*/ diff --git a/Python/marshal.c b/Python/marshal.c index afbef6ee679..15dd25d6268 100644 --- a/Python/marshal.c +++ b/Python/marshal.c @@ -1656,6 +1656,9 @@ r_object(RFILE *p) case TYPE_SLICE: { Py_ssize_t idx = r_ref_reserve(flag, p); + if (idx < 0) { + break; + } PyObject *stop = NULL; PyObject *step = NULL; PyObject *start = r_object(p); diff --git a/Python/remote_debug.h b/Python/remote_debug.h index 8f9b6cd4c49..d1fcb478d2b 100644 --- a/Python/remote_debug.h +++ b/Python/remote_debug.h @@ -110,14 +110,6 @@ get_page_size(void) { return page_size; } -typedef struct page_cache_entry { - uintptr_t page_addr; // page-aligned base address - char *data; - int valid; - struct page_cache_entry *next; -} page_cache_entry_t; - -#define MAX_PAGES 1024 // Define a platform-independent process handle structure typedef struct { @@ -129,27 +121,9 @@ typedef struct { #elif defined(__linux__) int memfd; #endif - page_cache_entry_t pages[MAX_PAGES]; Py_ssize_t page_size; } proc_handle_t; -static void -_Py_RemoteDebug_FreePageCache(proc_handle_t *handle) -{ - for (int i = 0; i < MAX_PAGES; i++) { - PyMem_RawFree(handle->pages[i].data); - handle->pages[i].data = NULL; - handle->pages[i].valid = 0; - } -} - -UNUSED static void -_Py_RemoteDebug_ClearCache(proc_handle_t *handle) -{ - for (int i = 0; i < MAX_PAGES; i++) { - handle->pages[i].valid = 0; - } -} #if defined(__APPLE__) && defined(TARGET_OS_OSX) && TARGET_OS_OSX static mach_port_t pid_to_task(pid_t pid); @@ -178,10 +152,6 @@ _Py_RemoteDebug_InitProcHandle(proc_handle_t *handle, pid_t pid) { handle->memfd = -1; #endif handle->page_size = get_page_size(); - for (int i = 0; i < MAX_PAGES; i++) { - handle->pages[i].data = NULL; - handle->pages[i].valid = 0; - } return 0; } @@ -200,7 +170,6 @@ _Py_RemoteDebug_CleanupProcHandle(proc_handle_t *handle) { } #endif handle->pid = 0; - _Py_RemoteDebug_FreePageCache(handle); } #if defined(__APPLE__) && defined(TARGET_OS_OSX) && TARGET_OS_OSX @@ -1066,53 +1035,6 @@ _Py_RemoteDebug_PagedReadRemoteMemory(proc_handle_t *handle, size_t size, void *out) { - size_t page_size = handle->page_size; - uintptr_t page_base = addr & ~(page_size - 1); - size_t offset_in_page = addr - page_base; - - if (offset_in_page + size > page_size) { - return _Py_RemoteDebug_ReadRemoteMemory(handle, addr, size, out); - } - - // Search for valid cached page - for (int i = 0; i < MAX_PAGES; i++) { - page_cache_entry_t *entry = &handle->pages[i]; - if (entry->valid && entry->page_addr == page_base) { - memcpy(out, entry->data + offset_in_page, size); - return 0; - } - } - - // Find reusable slot - for (int i = 0; i < MAX_PAGES; i++) { - page_cache_entry_t *entry = &handle->pages[i]; - if (!entry->valid) { - if (entry->data == NULL) { - entry->data = PyMem_RawMalloc(page_size); - if (entry->data == NULL) { - _set_debug_exception_cause(PyExc_MemoryError, - "Cannot allocate %zu bytes for page cache entry " - "during read from PID %d at address 0x%lx", - page_size, handle->pid, addr); - return -1; - } - } - - if (_Py_RemoteDebug_ReadRemoteMemory(handle, page_base, page_size, entry->data) < 0) { - // Try to just copy the exact ammount as a fallback - PyErr_Clear(); - goto fallback; - } - - entry->page_addr = page_base; - entry->valid = 1; - memcpy(out, entry->data + offset_in_page, size); - return 0; - } - } - -fallback: - // Cache full — fallback to uncached read return _Py_RemoteDebug_ReadRemoteMemory(handle, addr, size, out); } diff --git a/Tools/inspection/benchmark_external_inspection.py b/Tools/inspection/benchmark_external_inspection.py index 62182194c1a..0ac7ac4d385 100644 --- a/Tools/inspection/benchmark_external_inspection.py +++ b/Tools/inspection/benchmark_external_inspection.py @@ -174,6 +174,7 @@ def benchmark(unwinder, duration_seconds=10): total_work_time = 0.0 start_time = time.perf_counter() end_time = start_time + duration_seconds + total_attempts = 0 colors = get_colors(can_colorize()) @@ -183,6 +184,7 @@ def benchmark(unwinder, duration_seconds=10): try: while time.perf_counter() < end_time: + total_attempts += 1 work_start = time.perf_counter() try: stack_trace = unwinder.get_stack_trace() @@ -194,7 +196,6 @@ def benchmark(unwinder, duration_seconds=10): work_end = time.perf_counter() total_work_time += work_end - work_start - total_attempts = sample_count + fail_count if total_attempts % 10000 == 0: avg_work_time_us = (total_work_time / total_attempts) * 1e6 work_rate = ( @@ -221,7 +222,6 @@ def benchmark(unwinder, duration_seconds=10): actual_end_time = time.perf_counter() wall_time = actual_end_time - start_time - total_attempts = sample_count + fail_count # Return final statistics return { @@ -346,6 +346,13 @@ Available code examples: help="Code example to benchmark (default: basic)", ) + parser.add_argument( + "--threads", + choices=["all", "main", "only_active"], + default="all", + help="Which threads to include in the benchmark (default: all)", + ) + return parser.parse_args() @@ -419,8 +426,15 @@ def main(): # Create unwinder and run benchmark print(f"{colors.BLUE}Initializing unwinder...{colors.RESET}") try: + kwargs = {} + if args.threads == "all": + kwargs["all_threads"] = True + elif args.threads == "main": + kwargs["all_threads"] = False + elif args.threads == "only_active": + kwargs["only_active_thread"] = True unwinder = _remote_debugging.RemoteUnwinder( - process.pid, all_threads=True + process.pid, **kwargs ) results = benchmark(unwinder, duration_seconds=args.duration) finally: diff --git a/Tools/scripts/summarize_stats.py b/Tools/scripts/summarize_stats.py index 68cfad3f92c..905af9dcfd8 100644 --- a/Tools/scripts/summarize_stats.py +++ b/Tools/scripts/summarize_stats.py @@ -492,7 +492,7 @@ class Stats: ): (trace_too_long, attempts), Doc( "Trace too short", - "A potential trace is abandoned because it it too short.", + "A potential trace is abandoned because it is too short.", ): (trace_too_short, attempts), Doc( "Inner loop found", "A trace is truncated because it has an inner loop" diff --git a/iOS/README.rst b/iOS/README.rst index 13b88514493..f0979ba152e 100644 --- a/iOS/README.rst +++ b/iOS/README.rst @@ -196,7 +196,7 @@ simulator build with a deployment target of 15.4. Merge thin frameworks into fat frameworks ----------------------------------------- -Once you've built a ``Python.framework`` for each ABI and and architecture, you +Once you've built a ``Python.framework`` for each ABI and architecture, you must produce a "fat" framework for each ABI that contains all the architectures for that ABI. |