diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2f8f97b34d25b5a..90c5d0fbec686aa 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -605,9 +605,6 @@ jobs: - check-name: Undefined behavior sanitizer: UBSan free-threading: false - - check-name: Memory - sanitizer: MSan - free-threading: false uses: ./.github/workflows/reusable-san.yml with: sanitizer: ${{ matrix.sanitizer }} diff --git a/.github/workflows/reusable-san.yml b/.github/workflows/reusable-san.yml index da6306a50cf7bc4..ad3232743874d6b 100644 --- a/.github/workflows/reusable-san.yml +++ b/.github/workflows/reusable-san.yml @@ -60,7 +60,7 @@ jobs: || '' }} - name: UBSan option setup - if: inputs.sanitizer == 'UBSan' + if: inputs.sanitizer != 'TSan' run: >- echo "UBSAN_OPTIONS=${SAN_LOG_OPTION} @@ -69,20 +69,6 @@ jobs: >> "$GITHUB_ENV" env: SAN_LOG_OPTION: log_path=${{ github.workspace }}/san_log - - name: MSan option setup - if: inputs.sanitizer == 'MSan' - run: | - echo "MSAN_OPTIONS=${SAN_LOG_OPTION} allocator_may_return_null=1 handle_segv=0" >> "$GITHUB_ENV" - # MSan reports false positives for memory initialized by libraries - # that are not built with MSan, so disable modules that use them. - # _remote_debugging links to libzstd directly, but we unpoision the memory. - { - echo '*disabled*' - echo '_bz2 _ctypes _curses _curses_panel _dbm _decimal _gdbm _hashlib' - echo '_lzma _sqlite3 _ssl _tkinter _uuid _zstd readline zlib' - } > Modules/Setup.local - env: - SAN_LOG_OPTION: log_path=${{ github.workspace }}/san_log - name: Add ccache to PATH run: | echo "PATH=/usr/lib/ccache:$PATH" >> "$GITHUB_ENV" @@ -107,8 +93,6 @@ jobs: # gh-157958: -O2 instead of the pydebug default -Og to avoid a clang 21 # compile-time blowup on some interpreter files. # (https://github.com/llvm/llvm-project/issues/179695) - # MSan uses --with-assertions instead of --with-pydebug because its - # hooks on the Python memory allocators hide uninitialized reads. - name: Configure CPython run: >- ./configure @@ -117,11 +101,9 @@ jobs: ${{ inputs.sanitizer == 'TSan' && '--with-thread-sanitizer' - || inputs.sanitizer == 'MSan' - && '--with-memory-sanitizer' || '--with-undefined-behavior-sanitizer --with-strict-overflow' }} - ${{ inputs.sanitizer == 'MSan' && '--with-assertions' || '--with-pydebug' }} + --with-pydebug ${{ inputs.sanitizer == 'TSan' && '--with-openssl="$OPENSSL_DIR" --with-openssl-rpath=auto' || '' }} ${{ inputs.free-threading && '--disable-gil' || '' }} - name: Build CPython diff --git a/Doc/using/configure.rst b/Doc/using/configure.rst index d29eb891523243a..3745c21a5677612 100644 --- a/Doc/using/configure.rst +++ b/Doc/using/configure.rst @@ -1015,10 +1015,6 @@ Debug options Enable MemorySanitizer allocation error detector, ``msan`` (default is no). - MSan reports false positives for memory initialized by libraries that are - not built with MSan, so either build all dependencies with MSan or disable - the extension modules that use them in :file:`Modules/Setup.local`. - .. versionadded:: 3.6 .. option:: --with-undefined-behavior-sanitizer diff --git a/Include/internal/pycore_interp_structs.h b/Include/internal/pycore_interp_structs.h index 36d05efc4ce4b60..58a15eb87d2aadb 100644 --- a/Include/internal/pycore_interp_structs.h +++ b/Include/internal/pycore_interp_structs.h @@ -219,7 +219,6 @@ struct gc_old_stats_buffer { struct gc_stats { struct gc_young_stats_buffer young; struct gc_old_stats_buffer old[2]; - uint32_t update_seq; }; struct _gc_runtime_state { diff --git a/Include/pyport.h b/Include/pyport.h index 92e65b4d6d1e2ae..73a3e6cdaf09200 100644 --- a/Include/pyport.h +++ b/Include/pyport.h @@ -554,7 +554,6 @@ extern "C" { # define _Py_MEMORY_SANITIZER # define _Py_NO_SANITIZE_MEMORY __attribute__((no_sanitize_memory)) # define _Py_MSAN_UNPOISON(PTR, SIZE) (__msan_unpoison(PTR, SIZE)) -# define _Py_MSAN_UNPOISON_STRING(STR) (__msan_unpoison_string(STR)) # endif # endif # if __has_feature(address_sanitizer) @@ -596,9 +595,6 @@ extern "C" { #ifndef _Py_MSAN_UNPOISON # define _Py_MSAN_UNPOISON(PTR, SIZE) #endif -#ifndef _Py_MSAN_UNPOISON_STRING -# define _Py_MSAN_UNPOISON_STRING(STR) -#endif /* AIX has __bool__ redefined in it's system header file. */ #if defined(_AIX) && defined(__bool__) diff --git a/Lib/asyncio/tools.py b/Lib/asyncio/tools.py index fd494912549bf8c..2ac1738d15c6c72 100644 --- a/Lib/asyncio/tools.py +++ b/Lib/asyncio/tools.py @@ -27,10 +27,6 @@ def __init__( # ─── indexing helpers ─────────────────────────────────────────── def _format_stack_entry(elem: str|FrameInfo) -> str: if not isinstance(elem, str): - if elem.location is None: - if elem.filename in ("", "~"): - return f"{elem.funcname}" - return f"{elem.funcname} {elem.filename}" if elem.location.lineno == 0 and elem.filename == "": return f"{elem.funcname}" else: @@ -194,7 +190,8 @@ def build_task_table(result): # Build coroutine stack string frames = [frame for coro in task_info.coroutine_stack for frame in coro.call_stack] - coro_stack = " -> ".join(x.funcname for x in frames) + coro_stack = " -> ".join(_format_stack_entry(x).split(" ")[0] + for x in frames) # Handle tasks with no awaiters if not task_info.awaited_by: @@ -205,7 +202,8 @@ def build_task_table(result): # Handle tasks with awaiters for coro_info in task_info.awaited_by: parent_id = coro_info.task_name - awaiter_frames = [x.funcname for x in coro_info.call_stack] + awaiter_frames = [_format_stack_entry(x).split(" ")[0] + for x in coro_info.call_stack] awaiter_chain = " -> ".join(awaiter_frames) awaiter_name = id2name.get(parent_id, "Unknown") parent_id_str = (hex(parent_id) if isinstance(parent_id, int) diff --git a/Lib/profiling/sampling/_sync_coordinator.py b/Lib/profiling/sampling/_sync_coordinator.py index bb0ee5b5317d697..a1cce314b33b191 100644 --- a/Lib/profiling/sampling/_sync_coordinator.py +++ b/Lib/profiling/sampling/_sync_coordinator.py @@ -168,11 +168,6 @@ def _execute_script(script_path: str, script_args: List[str], cwd: str) -> None: if not os.path.isfile(script_path): raise TargetError(f"Script not found: {script_path}") - script_dir = os.path.dirname(os.path.realpath(script_path)) - if script_dir in sys.path: - sys.path.remove(script_dir) - sys.path.insert(0, script_dir) - # Replace sys.argv to match original script call sys.argv = [script_path] + script_args diff --git a/Lib/profiling/sampling/binary_collector.py b/Lib/profiling/sampling/binary_collector.py index 3d6d988077cfa2a..7a35044b1ee1d76 100644 --- a/Lib/profiling/sampling/binary_collector.py +++ b/Lib/profiling/sampling/binary_collector.py @@ -1,6 +1,5 @@ """Thin Python wrapper around C binary writer for profiling data.""" -import sys import time import _remote_debugging @@ -82,7 +81,6 @@ def __init__(self, filename, sample_interval_usec, *, skip_idle=False, self.filename = filename self.sample_interval_usec = sample_interval_usec self.skip_idle = skip_idle - self.running = True compression_type = _resolve_compression(compression) start_time_us = int(time.monotonic() * 1_000_000) @@ -104,19 +102,9 @@ def collect(self, stack_frames, timestamp_us=None): timestamp_us: Optional timestamp in microseconds. If not provided, uses time.monotonic() to generate one. """ - if not self.running: - return if timestamp_us is None: timestamp_us = int(time.monotonic() * 1_000_000) - try: - self._writer.write_sample(stack_frames, timestamp_us) - except OverflowError as e: - if not self._writer.limit_reached: - raise - self.running = False - print(f"Warning: {e}; stopping early and keeping the data " - "collected so far.", - file=sys.stderr) + self._writer.write_sample(stack_frames, timestamp_us) def collect_failed_sample(self): """Record a failed sample attempt (no-op for binary format).""" @@ -155,5 +143,9 @@ def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): - """Finalize if the writer can still produce a valid file.""" - return self._writer.__exit__(exc_type, exc_val, exc_tb) + """Context manager exit - finalize unless there was an error.""" + if exc_type is None: + self._writer.finalize() + else: + self._writer.close() + return False diff --git a/Lib/profiling/sampling/heatmap_collector.py b/Lib/profiling/sampling/heatmap_collector.py index 38f4d7985ff6917..0361fd357b222dd 100644 --- a/Lib/profiling/sampling/heatmap_collector.py +++ b/Lib/profiling/sampling/heatmap_collector.py @@ -785,14 +785,14 @@ def _generate_file_html(self, output_path: Path, filename: str, line_counts: Dict[int, int], self_counts: Dict[int, int], file_stat: FileStats): """Generate HTML for a single source file with heatmap coloring.""" - source_lines = [f"# Source file not available: {filename}"] + # Read source file try: - path = Path(filename) - if path.is_file(): - source_lines = path.read_text( - encoding='utf-8', errors='replace').splitlines() - except (IOError, OSError): - pass + source_lines = Path(filename).read_text(encoding='utf-8', errors='replace').splitlines() + except (IOError, OSError) as e: + if not (filename.startswith('<') or filename.startswith('[') or + filename in ('~', '...', '.') or len(filename) < 2): + print(f"Warning: Could not read source file {filename}: {e}") + source_lines = [f"# Source file not available: {filename}"] # Generate HTML for each line max_samples = max(line_counts.values()) if line_counts else 1 diff --git a/Lib/profiling/sampling/stack_collector.py b/Lib/profiling/sampling/stack_collector.py index e420bb6d2e9b871..1610f4a36558825 100644 --- a/Lib/profiling/sampling/stack_collector.py +++ b/Lib/profiling/sampling/stack_collector.py @@ -60,18 +60,13 @@ def export(self, filename): lines.sort(key=lambda x: (-x[1], x[0])) - with open(filename, "w", - encoding="utf-8", errors="surrogatepass") as f: + with open(filename, "w") as f: for stack, count in lines: f.write(f"{stack} {count}\n") print(f"Collapsed stack output written to {filename}") return True -# Bounded by the unwinder's maximum captured stack depth (MAX_FRAMES). -_FLAMEGRAPH_RECURSION_MARGIN = 2000 - - class FlamegraphCollector(StackTraceCollector): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -170,41 +165,34 @@ def set_mode(self, mode): self.stats["mode"] = mode def export(self, filename): - # Converting the call tree recurses to the sampled stack depth. - old_limit = sys.getrecursionlimit() - sys.setrecursionlimit(old_limit + _FLAMEGRAPH_RECURSION_MARGIN) - try: - flamegraph_data = self._convert_to_flamegraph_format() - - # Debug output with string table statistics - num_functions = len(flamegraph_data.get("children", [])) - total_time = flamegraph_data.get("value", 0) - string_count = len(self._string_table) - s1 = "" if num_functions == 1 else "s" - s2 = "" if total_time == 1 else "s" - s3 = "" if string_count == 1 else "s" + flamegraph_data = self._convert_to_flamegraph_format() + + # Debug output with string table statistics + num_functions = len(flamegraph_data.get("children", [])) + total_time = flamegraph_data.get("value", 0) + string_count = len(self._string_table) + s1 = "" if num_functions == 1 else "s" + s2 = "" if total_time == 1 else "s" + s3 = "" if string_count == 1 else "s" + print( + f"Flamegraph data: {num_functions} root function{s1}, " + f"{total_time} total sample{s2}, " + f"{string_count} unique string{s3}" + ) + + if num_functions == 0: print( - f"Flamegraph data: {num_functions} root function{s1}, " - f"{total_time} total sample{s2}, " - f"{string_count} unique string{s3}" + "Warning: No functions found in profiling data. Check if sampling captured any data." ) + return False - if num_functions == 0: - print( - "Warning: No functions found in profiling data. " - "Check if sampling captured any data." - ) - return False - - html_content = self._create_flamegraph_html(flamegraph_data) + html_content = self._create_flamegraph_html(flamegraph_data) - with open(filename, "w", encoding="utf-8") as f: - f.write(html_content) + with open(filename, "w", encoding="utf-8") as f: + f.write(html_content) - print(f"Flamegraph saved to: {filename}") - return True - finally: - sys.setrecursionlimit(old_limit) + print(f"Flamegraph saved to: {filename}") + return True @staticmethod @functools.lru_cache(maxsize=None) diff --git a/Lib/test/test_asyncio/test_tools.py b/Lib/test/test_asyncio/test_tools.py index 2b8e4940333196b..df934164eb9fd60 100644 --- a/Lib/test/test_asyncio/test_tools.py +++ b/Lib/test/test_asyncio/test_tools.py @@ -1558,82 +1558,6 @@ def test_table_output_format(self): class TestAsyncioToolsEdgeCases(unittest.TestCase): - def test_frames_without_location_tree(self): - """Frames the unwinder could not fully read - should not crash.""" - input_ = [ - AwaitedInfo( - thread_id=1, - awaited_by=[ - TaskInfo( - task_id=1, - task_name="Task-A", - coroutine_stack=[ - CoroInfo( - call_stack=[ - FrameInfo("", "~", None), - FrameInfo("", "app.py", None), - FrameInfo("big", "big.py", None), - ], - task_name=1 - ) - ], - awaited_by=[] - ) - ] - ) - ] - self.assertEqual( - tools.build_async_tree(input_), - [[ - "└── (T) Task-A", - " └── big big.py", - " └── app.py", - " └── ", - ]], - ) - - def test_frames_without_location_table(self): - """Frame names are not truncated at the first space.""" - input_ = [ - AwaitedInfo( - thread_id=1, - awaited_by=[ - TaskInfo( - task_id=1, - task_name="Task-A", - coroutine_stack=[ - CoroInfo( - call_stack=[ - FrameInfo("", "~", None) - ], - task_name=1 - ) - ], - awaited_by=[ - CoroInfo( - call_stack=[ - FrameInfo("", "app.py", None) - ], - task_name=2 - ) - ] - ) - ] - ) - ] - self.assertEqual( - tools.build_task_table(input_), - [[ - 1, - "0x1", - "Task-A", - "", - "", - "Unknown", - "0x2", - ]], - ) - def test_task_awaits_self(self): """A task directly awaits itself - should raise a cycle.""" input_ = [ diff --git a/Lib/test/test_bytes.py b/Lib/test/test_bytes.py index d31d97ce616b6ce..c714fc2e76e83c5 100644 --- a/Lib/test/test_bytes.py +++ b/Lib/test/test_bytes.py @@ -508,15 +508,6 @@ def test_fromhex(self): self.type2test.fromhex(data) self.assertIn('at position %s' % pos, str(cm.exception)) - # gh-158583: Check for out of bounds reads (uninitialized bytes). - # Create an array from a list to not overallocate. - a = array.array('B', list(b'1234 ')) # Py_ISSPACE() loop - self.assertEqual(self.type2test.fromhex(a), b'\x12\x34') - - a = array.array('B', list(b'12345')) # Missing second digit - with self.assertRaises(ValueError): - self.type2test.fromhex(a) - def test_hex(self): self.assertRaises(TypeError, self.type2test.hex) self.assertRaises(TypeError, self.type2test.hex, 1) diff --git a/Lib/test/test_external_inspection.py b/Lib/test/test_external_inspection.py index eaec2943ecbaaf3..797b13af59f0f1e 100644 --- a/Lib/test/test_external_inspection.py +++ b/Lib/test/test_external_inspection.py @@ -340,40 +340,6 @@ def _run_script_and_get_trace( finally: _cleanup_sockets(client_socket, server_socket) - @contextmanager - def _target_process(self, script_body): - """Context manager for running a target process with socket sync.""" - port = find_unused_port() - script = f"""\ -import socket -sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) -sock.connect(('localhost', {port})) -{textwrap.dedent(script_body)} -""" - - with os_helper.temp_dir() as work_dir: - script_dir = os.path.join(work_dir, "script_pkg") - os.mkdir(script_dir) - - server_socket = _create_server_socket(port) - script_name = _make_test_script(script_dir, "script", script) - client_socket = None - - try: - with _managed_subprocess([sys.executable, script_name]) as p: - client_socket, _ = server_socket.accept() - server_socket.close() - server_socket = None - - def make_unwinder(cache_frames=True): - return RemoteUnwinder( - p.pid, all_threads=True, cache_frames=cache_frames - ) - - yield p, client_socket, make_unwinder - finally: - _cleanup_sockets(client_socket, server_socket) - def _find_frame_in_trace(self, stack_trace, predicate): """ Find a frame matching predicate in stack trace. @@ -492,22 +458,6 @@ async def main(): self.assertIn(main_name, names) self.assertEqual([len(n) for n in names if n.startswith("x")], [255]) - @skip_if_not_supported - def test_recursive_coroutine_stack_is_not_truncated(self): - # gh-158522 - async def rec(n): - if n: - return await rec(n - 1) - return [ - frame.funcname.rpartition(".")[2] - for task in RemoteUnwinder( - os.getpid()).get_async_stack_trace()[0].awaited_by - for coro in task.coroutine_stack - for frame in coro.call_stack - ] - - self.assertEqual(asyncio.run(rec(3)), ["rec"] * 4) - @skip_if_not_supported @unittest.skipIf( sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, @@ -1555,71 +1505,6 @@ def matches_awaited_by_pattern(task): finally: _cleanup_sockets(client_socket, server_socket) - @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", - ) - def test_async_awaited_by_skips_set_tombstones(self): - script_body = """\ - import asyncio - - class RemovedTask(asyncio.Task): - def __hash__(self): - return 0 - - class RemainingTask(asyncio.Task): - def __hash__(self): - return 1 - - async def main(): - victim = asyncio.current_task() - victim.set_name("victim") - removed = RemovedTask( - asyncio.sleep(10_000), name="removed" - ) - remaining = RemainingTask( - asyncio.sleep(10_000), name="remaining" - ) - - asyncio.future_add_to_awaited_by(victim, removed) - asyncio.future_add_to_awaited_by(victim, remaining) - - # Removing hash 0 leaves a dummy in slot 0 before the only - # active entry in slot 1. It must not count toward the set's - # used entries. - asyncio.future_discard_from_awaited_by(victim, removed) - - sock.sendall(b"ready") - sock.recv(16) - - asyncio.run(main()) - """ - - with self._target_process(script_body) as ( - _, - client_socket, - make_unwinder, - ): - _wait_for_signal(client_socket, b"ready") - - for method_name in ( - "get_async_stack_trace", - "get_all_awaited_by", - ): - with self.subTest(method=method_name): - unwinder = make_unwinder(cache_frames=False) - stack_trace = getattr(unwinder, method_name)() - relationships = self._get_awaited_by_relationships( - stack_trace - ) - self.assertEqual( - relationships["victim"], - {"remaining"}, - ) - - client_socket.sendall(b"done") - @skip_if_not_supported @unittest.skipIf( sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, @@ -3235,249 +3120,47 @@ def test_finally_no_exception_no_flag(self): self._check_exception_status(p, thread_tid, expect_exception=False) -@skip_if_not_supported -class TestExceptionDetectionInProcess(RemoteInspectionTestBase): - """gh-158539: HAS_EXCEPTION for handlers running in generators/coroutines. +@requires_remote_subprocess_debugging() +class TestFrameCaching(RemoteInspectionTestBase): + """Test that frame caching produces correct results. - ``TestExceptionDetectionScenarios`` samples a child process and therefore - needs subprocess debugging permissions. These tests inspect the current - process with ``RemoteUnwinder`` and only need self-inspection, so they also - run on macOS without special entitlements. + Uses socket-based synchronization for deterministic testing. + All tests verify cache reuse via object identity checks (assertIs). """ - @classmethod - def setUpClass(cls): - try: - RemoteUnwinder(os.getpid(), all_threads=True).get_stack_trace() - except PermissionError as exc: - raise unittest.SkipTest(f"self-inspection is unavailable: {exc}") - - def _check_running_handler( - self, target, expect_exception, *, mode=PROFILING_MODE_ALL, - skip_non_matching_threads=False, - ): - """Run *target* in a thread and check its HAS_EXCEPTION flag. - - *target* receives ``(ready, stop)`` events and must signal ``ready`` - only once it is executing inside the code region under test, then keep - running until ``stop`` is set. - """ - stop = threading.Event() - ready = threading.Event() - failure = [] - - def runner(): - try: - target(ready, stop) - except BaseException as exc: - failure.append(exc) - ready.set() - - thread = threading.Thread(target=runner, daemon=True) - thread.start() - try: - self.assertTrue(ready.wait(SHORT_TIMEOUT), "handler never started") - self.assertFalse(failure, f"handler raised {failure!r}") - - unwinder = RemoteUnwinder( - os.getpid(), - all_threads=True, - mode=mode, - skip_non_matching_threads=skip_non_matching_threads, - ) - observed = [] - for _ in busy_retry(SHORT_TIMEOUT): - with contextlib.suppress(*TRANSIENT_ERRORS): - statuses = self._get_thread_statuses(unwinder.get_stack_trace()) - status = statuses.get(thread.native_id) - if status is None: - continue - has_exception = bool(status & THREAD_STATUS_HAS_EXCEPTION) - observed.append(has_exception) - if has_exception == expect_exception: - break - self.assertTrue( - observed, "target thread status was never observed" - ) - self.assertIn( - expect_exception, - observed, - f"HAS_EXCEPTION was never {expect_exception} while the " - f"handler was running (observed {observed})", - ) - finally: - stop.set() - thread.join(SHORT_TIMEOUT) - - def _busy_until_stopped(self, ready, stop): - ready.set() - while not stop.is_set(): - time.sleep(0.001) - - def test_handler_in_function(self): - def target(ready, stop): - try: - raise ValueError("test") - except ValueError: - self._busy_until_stopped(ready, stop) - - self._check_running_handler(target, expect_exception=True) - - def test_handler_in_generator(self): - def target(ready, stop): - def gen(): - try: - raise ValueError("test") - except ValueError: - self._busy_until_stopped(ready, stop) - yield - - for _ in gen(): - pass - - self._check_running_handler(target, expect_exception=True) - - def test_handler_in_genexpr_callee(self): - def target(ready, stop): - def callee(): - try: - raise ValueError("test") - except ValueError: - self._busy_until_stopped(ready, stop) - - list(callee() for _ in range(1)) - - self._check_running_handler(target, expect_exception=True) - - def test_handler_in_coroutine(self): - async def coro(ready, stop): - try: - raise ValueError("test") - except ValueError: - self._busy_until_stopped(ready, stop) - - def target(ready, stop): - asyncio.run(coro(ready, stop)) - - self._check_running_handler(target, expect_exception=True) - - def test_handler_in_callee_from_coroutine(self): - def callee(ready, stop): - try: - raise ValueError("test") - except ValueError: - self._busy_until_stopped(ready, stop) - - async def coro(ready, stop): - callee(ready, stop) - - def target(ready, stop): - asyncio.run(coro(ready, stop)) - - self._check_running_handler(target, expect_exception=True) - - def test_outer_handler_while_generator_runs(self): - """A generator with no handler of its own must not hide the outer one. - - ``exc_info`` points at the generator's empty ``_PyErr_StackItem`` whose - ``previous_item`` is the thread's ``exc_state``, so the profiler has to - walk the chain to find the exception ``sys.exception()`` reports. - """ - def target(ready, stop): - def gen(): - self._busy_until_stopped(ready, stop) - yield - - try: - raise ValueError("outer") - except ValueError: - for _ in gen(): - pass - - self._check_running_handler(target, expect_exception=True) - - def test_generator_without_exception(self): - def target(ready, stop): - def gen(): - self._busy_until_stopped(ready, stop) - yield - - for _ in gen(): - pass + @contextmanager + def _target_process(self, script_body): + """Context manager for running a target process with socket sync.""" + port = find_unused_port() + script = f"""\ +import socket +sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +sock.connect(('localhost', {port})) +{textwrap.dedent(script_body)} +""" - self._check_running_handler(target, expect_exception=False) + with os_helper.temp_dir() as work_dir: + script_dir = os.path.join(work_dir, "script_pkg") + os.mkdir(script_dir) - def test_outer_handler_while_nested_generators_run(self): - def target(ready, stop): - def gen(depth): - if depth: - yield from gen(depth - 1) - else: - self._busy_until_stopped(ready, stop) - yield + server_socket = _create_server_socket(port) + script_name = _make_test_script(script_dir, "script", script) + client_socket = None try: - raise ValueError("outer") - except ValueError: - for _ in gen(32): - pass - - self._check_running_handler( - target, - expect_exception=True, - mode=PROFILING_MODE_EXCEPTION, - skip_non_matching_threads=True, - ) - - def test_generator_finally_after_except(self): - """The handled exception is cleared before the generator's finally.""" - def target(ready, stop): - def gen(): - try: - raise ValueError("test") - except ValueError: - pass - finally: - self._busy_until_stopped(ready, stop) - yield - - for _ in gen(): - pass - - self._check_running_handler(target, expect_exception=False) - - def test_exception_mode_filter_keeps_generator_handler(self): - """The exception-mode thread filter must not drop a generator handler. - - This mirrors what ``--mode=exception`` actually does: threads without - HAS_EXCEPTION are skipped before their stack is unwound. - """ - def target(ready, stop): - def gen(): - try: - raise ValueError("test") - except ValueError: - self._busy_until_stopped(ready, stop) - yield - - for _ in gen(): - pass - - self._check_running_handler( - target, - expect_exception=True, - mode=PROFILING_MODE_EXCEPTION, - skip_non_matching_threads=True, - ) - + with _managed_subprocess([sys.executable, script_name]) as p: + client_socket, _ = server_socket.accept() + server_socket.close() + server_socket = None -@requires_remote_subprocess_debugging() -class TestFrameCaching(RemoteInspectionTestBase): - """Test that frame caching produces correct results. + def make_unwinder(cache_frames=True): + return RemoteUnwinder( + p.pid, all_threads=True, cache_frames=cache_frames + ) - Uses socket-based synchronization for deterministic testing. - All tests verify cache reuse via object identity checks (assertIs). - """ + yield p, client_socket, make_unwinder + finally: + _cleanup_sockets(client_socket, server_socket) def _get_frames_with_retry(self, unwinder, required_funcs): """Get frames containing required_funcs, with retry for transient errors.""" @@ -4322,250 +4005,5 @@ def test_get_stats_disabled_raises(self): client_socket.sendall(b"done") -@requires_remote_subprocess_debugging() -@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", -) -class TestMetadataDegradation(RemoteInspectionTestBase): - """Tests for graceful degradation of oversized code-object metadata.""" - - def test_long_qualname_truncated_not_dropped(self): - """A qualname longer than 1024 chars is truncated instead of - failing the whole sample.""" - name = "f" * 1100 - src = f"def {name}(sample):\n return sample()\n" - ns = {} - exec(src, ns) - - trace = ns[name](RemoteUnwinder(os.getpid()).get_stack_trace) - frame = self._find_frame_in_trace( - trace, lambda f: f.funcname.startswith("fff") - ) - self.assertIsNotNone(frame) - self.assertEqual(frame.funcname, "f" * 1024) - - def test_long_filename_truncated(self): - """A filename longer than 1024 chars is truncated instead of - failing the whole sample.""" - src = "def g(sample):\n return sample()\n" - ns = {} - exec(compile(src, "x" * 1500 + ".py", "exec"), ns) - - trace = ns["g"](RemoteUnwinder(os.getpid()).get_stack_trace) - frame = self._find_frame_in_trace(trace, lambda f: f.funcname == "g") - self.assertIsNotNone(frame) - self.assertEqual(frame.filename, "x" * 1024) - - def test_oversized_linetable_degrades_to_no_location(self): - """A linetable over MAX_LINETABLE_SIZE degrades to a frame without - location instead of failing the whole sample.""" - src = ( - "def big(sample):\n" - + " x = 1\n" * 20_000 - + " return sample()\n" - ) - ns = {} - exec(compile(src, "big_linetable.py", "exec"), ns) - big = ns["big"] - self.assertGreater(len(big.__code__.co_linetable), 64 * 1024) - - trace = big(RemoteUnwinder(os.getpid()).get_stack_trace) - frame = self._find_frame_in_trace( - trace, lambda f: f.funcname == "big" - ) - self.assertIsNone(frame.location) - self.assertEqual(frame.filename, "big_linetable.py") - - @unittest.skipIf( - sys.platform == "win32", - "Process death maps to ProcessLookupError only on POSIX platforms", - ) - def test_dead_process_raises_not_degrades(self): - """Death of the target raises ProcessLookupError instead of - degrading to synthetic frames.""" - script_body = """\ - import time - sock.sendall(b"ready") - time.sleep(10_000) - """ - with self._target_process(script_body) as (p, client_socket, make_unwinder): - _wait_for_signal(client_socket, b"ready") - unwinder = make_unwinder() - _get_stack_trace_with_retry(unwinder) - - p.kill() - p.wait() - - for _ in busy_retry(SHORT_TIMEOUT, error=False): - try: - unwinder.get_stack_trace() - except ProcessLookupError: - break - except RuntimeError: - continue - else: - self.fail("ProcessLookupError never raised for dead process") - - -@requires_remote_subprocess_debugging() -class TestFrameChainLimits(RemoteInspectionTestBase): - """Frame chain walks abort instead of looping/overflowing on deep chains.""" - - # Limits plus one, to exceed them (must match MAX_FRAME_CHAIN_DEPTH / - # MAX_TASK_WAITER_WALK_TASKS from _remote_debugging.h) - FRAME_CHAIN_DEPTH = 1024 + 512 + 1 - TASK_WAITER_WALK_TASKS = 2**14 + 1 - - def _assert_unwinder_limit_error(self, unwind, expected_substring): - """Call unwind() until it raises the frame chain limit error. - - unwind must construct the RemoteUnwinder and call it, so that - transient RuntimeErrors from either step are retried; a successful - call means the limit never triggered and fails immediately. - """ - last_error = None - for _ in busy_retry(SHORT_TIMEOUT, error=False): - try: - unwind() - except TRANSIENT_ERRORS as e: - if expected_substring in str(e): - return - last_error = e - continue - self.fail( - "frame chain limit did not trigger; call returned a result" - ) - self.fail( - f"frame chain limit never raised; last transient error: " - f"{last_error!r}" - ) - - @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", - ) - def test_get_stack_trace_deep_frame_chain_aborts(self): - """Test that a frame chain deeper than the limit aborts the - synchronous stack walk instead of walking it indefinitely.""" - script_body = f"""\ - import sys - sys.setrecursionlimit({self.FRAME_CHAIN_DEPTH * 2}) - - def recurse(n): - if n <= 0: - sock.sendall(b"ready") - sock.recv(16) - return - recurse(n - 1) - - recurse({self.FRAME_CHAIN_DEPTH}) - """ - with self._target_process(script_body) as (p, client_socket, _): - _wait_for_signal(client_socket, b"ready") - self._assert_unwinder_limit_error( - lambda: RemoteUnwinder(p.pid).get_stack_trace(), - "Too many stack frames", - ) - client_socket.sendall(b"done") - - @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", - ) - def test_get_async_stack_trace_deep_task_waiter_chain_aborts(self): - """Test that a task waiter chain deeper than the limit aborts - the walk instead of overflowing the C stack.""" - script_body = f"""\ - import asyncio - - async def chain(n): - if n <= 0: - sock.sendall(b"ready") - sock.recv(16) - return - - task = asyncio.create_task(chain(n - 1)) - await task - - asyncio.run(chain({self.TASK_WAITER_WALK_TASKS})) - """ - with self._target_process(script_body) as (p, client_socket, _): - _wait_for_signal(client_socket, b"ready") - self._assert_unwinder_limit_error( - lambda: RemoteUnwinder(p.pid).get_async_stack_trace(), - "Too many task waiters", - ) - client_socket.sendall(b"done") - - @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", - ) - def test_get_async_stack_trace_deep_frame_chain_aborts(self): - """Test that a frame chain deeper than the limit aborts the async - stack walk instead of walking it indefinitely.""" - script_body = f"""\ - import sys, asyncio - sys.setrecursionlimit({self.FRAME_CHAIN_DEPTH * 2}) - - def recurse(n): - if n <= 0: - sock.sendall(b"ready") - sock.recv(16) - return - recurse(n - 1) - - async def deep(): - recurse({self.FRAME_CHAIN_DEPTH}) - - asyncio.run(deep()) - """ - with self._target_process(script_body) as (p, client_socket, _): - _wait_for_signal(client_socket, b"ready") - self._assert_unwinder_limit_error( - lambda: RemoteUnwinder(p.pid).get_async_stack_trace(), - "Too many async stack frames", - ) - client_socket.sendall(b"done") - - @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", - ) - def test_get_all_awaited_by_deep_coro_chain_aborts(self): - """Test that a coroutine await chain deeper than the limit aborts - the walk instead of overflowing the C stack.""" - script_body = f"""\ - import sys, asyncio - sys.setrecursionlimit({self.FRAME_CHAIN_DEPTH * 2}) - - async def chain(n): - if n <= 0: - await asyncio.sleep(10_000) - return - await chain(n - 1) - - async def main(): - task = asyncio.create_task(chain({self.FRAME_CHAIN_DEPTH})) - await asyncio.sleep(0) - sock.sendall(b"ready") - await task - - asyncio.run(main()) - """ - with self._target_process(script_body) as (p, client_socket, _): - _wait_for_signal(client_socket, b"ready") - self._assert_unwinder_limit_error( - lambda: RemoteUnwinder(p.pid).get_all_awaited_by(), - "Too many coroutine frames", - ) - - if __name__ == "__main__": unittest.main() diff --git a/Lib/test/test_faulthandler.py b/Lib/test/test_faulthandler.py index 82b347c8f8c045b..5a493a4fd956802 100644 --- a/Lib/test/test_faulthandler.py +++ b/Lib/test/test_faulthandler.py @@ -34,8 +34,8 @@ def skip_if_sanitizer_signal(signame): - return support.skip_if_sanitizer(f"TSan/UBSan/MSan intercepts {signame}", - thread=True, ub=True, memory=True) + return support.skip_if_sanitizer(f"TSAN/UBSan itercepts {signame}", + thread=True, ub=True) def expected_traceback(lineno1, lineno2, header, min_count=1): diff --git a/Lib/test/test_profiling/test_heatmap.py b/Lib/test/test_profiling/test_heatmap.py index 1b4de0caa3d00fc..ee27fdd3fa3053c 100644 --- a/Lib/test/test_profiling/test_heatmap.py +++ b/Lib/test/test_profiling/test_heatmap.py @@ -4,7 +4,6 @@ import shutil import tempfile import unittest -from unittest import mock from collections import namedtuple from pathlib import Path @@ -616,43 +615,13 @@ def test_export_file_html_has_line_numbers(self): html_files = [f for f in os.listdir(output_path) if f.startswith('file_') and f.endswith('.html')] - self.assertEqual(len(html_files), 1) - with open(os.path.join(output_path, html_files[0]), 'r', encoding='utf-8') as f: - content = f.read() - - # Should have line-related content - self.assertIn('line-', content) - - def test_export_skips_nonexistent_source(self): - self.check_unavailable_source(os.path.join(self.test_dir, 'missing.py')) + if html_files: + with open(os.path.join(output_path, html_files[0]), 'r', encoding='utf-8') as f: + content = f.read() - def test_export_skips_directory_source(self): - self.check_unavailable_source(self.test_dir) + # Should have line-related content + self.assertIn('line-', content) - @unittest.skipUnless(hasattr(os, 'mkfifo'), 'requires os.mkfifo') - def test_export_skips_fifo_source(self): - filename = os.path.join(self.test_dir, 'source.fifo') - os.mkfifo(filename) - self.check_unavailable_source(filename) - - def check_unavailable_source(self, filename): - collector = HeatmapCollector(sample_interval_usec=100) - frames = [(filename, (1, 1, -1, -1), 'f', None)] - collector.process_frames(frames, thread_id=1) - output_path = Path(self.test_dir) / 'unavailable_source' - read_text = Path.read_text - - def check_read(path, *args, **kwargs): - self.assertNotEqual(path, Path(filename), - 'Non-regular source must not be opened') - return read_text(path, *args, **kwargs) - - with (captured_stdout(), captured_stderr(), - mock.patch.object(Path, 'read_text', check_read)): - collector.export(output_path) - content = (output_path / collector.file_index[filename]).read_text( - encoding='utf-8') - self.assertIn('Source file not available', content) class MockFrameInfo: """Mock FrameInfo for testing. 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 66a9c68a61c47dc..ff944b3163ec3e8 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 @@ -7,11 +7,8 @@ import struct import tempfile import unittest -from unittest import mock from collections import defaultdict -from test.support import captured_stderr - try: import _remote_debugging from _remote_debugging import ( @@ -668,26 +665,6 @@ def test_same_line_different_columns(self): collector, count = self.roundtrip(samples) self.assertEqual(count, 3) - def test_synthetic_frames_roundtrip(self): - """Degraded/sentinel frames (location=None) survive the binary format.""" - frames = [ - FrameInfo(("~", None, name, None)) - for name in ( - "", - "", - "", - "", - "", - ) - ] - frames.append(FrameInfo(("app.py", None, "", None))) - frames.append(FrameInfo(("", None, "real_func", None))) - samples = [[make_interpreter(0, [make_thread(1, frames)])]] - - collector, count = self.roundtrip(samples) - self.assertEqual(count, 1) - self.assert_samples_equal(samples, collector) - class TestBinaryEdgeCases(BinaryFormatTestBase): """Tests for edge cases in binary format.""" @@ -1054,142 +1031,6 @@ def test_writer_total_samples_after_close_returns_zero(self): w.close() self.assertEqual(w.total_samples, 0) - def test_binary_collector_stops_gracefully_on_overflow(self): - """OverflowError from the writer stops collection via the running - protocol instead of propagating and corrupting the file. - See gh-151292.""" - with tempfile.NamedTemporaryFile(suffix=".bin", delete=False) as f: - filename = f.name - self.temp_files.append(filename) - - collector = BinaryCollector(filename, 1000, compression="none") - self.assertTrue(collector.running) - - sample = [ - make_interpreter(0, [make_thread(1, [make_frame("a.py", 1, "f")])]) - ] - - # Collect real samples first, then hit the limit. - for i in range(3): - collector.collect(sample, timestamp_us=(i + 1) * 1000) - self.assertTrue(collector.running) - - bad = [make_interpreter(2**32, sample[0].threads)] - with captured_stderr() as stderr: - collector.collect(bad, timestamp_us=4000) - collector.collect(sample, timestamp_us=5000) - - self.assertFalse(collector.running) - self.assertTrue(collector._writer.limit_reached) - self.assertEqual(stderr.getvalue().count("Warning:"), 1) - self.assertIn("interpreter_id", stderr.getvalue()) - - collector.export(None) - - self.assertEqual(collector.total_samples, 3) - - reader_collector = RawCollector() - with BinaryReader(filename) as reader: - self.assertEqual(reader.replay_samples(reader_collector), 3) - - def test_interpreter_id_overflow_rejected(self): - """An interpreter_id wider than u32 raises OverflowError before any - writer state is mutated: subsequent valid samples are still accepted - and finalize produces a readable file.""" - with tempfile.NamedTemporaryFile(suffix=".bin", delete=False) as f: - filename = f.name - self.temp_files.append(filename) - - good = [ - make_interpreter(0, [make_thread(1, [make_frame("a.py", 1, "f")])]) - ] - bad = [ - make_interpreter(2**32, [make_thread(1, [make_frame("a.py", 1, "f")])]) - ] - - writer = _remote_debugging.BinaryWriter(filename, 1000, 0, compression=0) - writer.write_sample(good, 1000) - with self.assertRaises(OverflowError): - writer.write_sample(bad, 2000) - writer.write_sample(good, 3000) - writer.finalize() - self.assertEqual(writer.total_samples, 2) - - reader_collector = RawCollector() - with BinaryReader(filename) as reader: - self.assertEqual(reader.replay_samples(reader_collector), 2) - - def test_writer_finalizes_after_format_limit(self): - for compression in (0, 1) if ZSTD_AVAILABLE else (0,): - with self.subTest(compression=compression): - with tempfile.NamedTemporaryFile(suffix=".bin", delete=False) as f: - filename = f.name - self.temp_files.append(filename) - good = [make_interpreter(0, [ - make_thread(1, [make_frame("a.py", 1, "f")]) - ])] - bad = [make_interpreter(2**32, good[0].threads)] - writer = _remote_debugging.BinaryWriter( - filename, 1000, 0, compression=compression - ) - with self.assertRaises(OverflowError): - with writer: - writer.write_sample(good, 1000) - writer.write_sample(good, 2000) - # The first interpreter is committed before the limit. - writer.write_sample(good + bad, 3000) - self.assertEqual(writer.total_samples, 3) - with BinaryReader(filename) as reader: - self.assertEqual(reader.replay_samples(RawCollector()), 3) - - def test_collector_does_not_swallow_unrelated_overflow(self): - class BadStatus: - def __index__(self): - raise OverflowError("status conversion failed") - - with tempfile.NamedTemporaryFile(suffix=".bin", delete=False) as f: - filename = f.name - self.temp_files.append(filename) - collector = BinaryCollector(filename, 1000, compression="none") - self.addCleanup(collector._writer.close) - sample = [make_interpreter(0, [make_thread(1, [], BadStatus())])] - with captured_stderr() as stderr: - with self.assertRaisesRegex(OverflowError, "status conversion failed"): - collector.collect(sample, timestamp_us=1000) - self.assertEqual(stderr.getvalue(), "") - self.assertFalse(collector._writer.limit_reached) - with self.assertRaisesRegex(ValueError, "broken"): - collector.export() - with self.assertRaisesRegex(ValueError, "broken"): - collector._writer.write_sample([], 2000) - # Closing a broken writer must not attempt to finalize it. - collector.__exit__(None, None, None) - - def test_collector_finalizes_after_external_exception(self): - with tempfile.NamedTemporaryFile(suffix=".bin", delete=False) as f: - filename = f.name - self.temp_files.append(filename) - with self.assertRaisesRegex(RuntimeError, "sampling failed"): - with BinaryCollector(filename, 1000, compression="none") as collector: - collector.collect([make_interpreter(0, [make_thread(1, [])])]) - raise RuntimeError("sampling failed") - self.assertEqual(collector.total_samples, 1) - with BinaryReader(filename) as reader: - self.assertEqual(reader.replay_samples(RawCollector()), 1) - - @unittest.skipUnless(os.path.exists("/dev/full"), "requires /dev/full") - def test_finalize_failure_breaks_writer(self): - writer = _remote_debugging.BinaryWriter("/dev/full", 1000, 0) - self.addCleanup(writer.close) - writer.write_sample([make_interpreter(0, [make_thread(1, [])])], 1000) - with self.assertRaises(OSError): - writer.finalize() - self.assertFalse(writer.limit_reached) - with self.assertRaisesRegex(ValueError, "broken"): - writer.finalize() - with self.assertRaisesRegex(ValueError, "broken"): - writer.write_sample([], 2000) - class TestBinaryFormatValidation(BinaryFormatTestBase): """Tests for malformed binary files.""" @@ -1504,77 +1345,6 @@ def test_alternating_threads_status_changes(self): self.assertEqual(count, 100) self.assert_samples_equal(samples, collector) - def test_rle_alternating_status_batches_correctly(self): - """A repeat record whose status alternates every sample replays as N - single-status batches with the right cumulative timestamps.""" - class BatchCollector: - def __init__(self): - self.batches = [] - - def collect(self, stack_frames, timestamps_us): - for interp in stack_frames: - for thread in interp.threads: - self.batches.append( - (thread.status, list(timestamps_us)) - ) - - def export(self, filename): - pass - - num_samples = 2000 - frame = make_frame("rle.py", 42, "rle_func") - with tempfile.NamedTemporaryFile(suffix=".bin", delete=False) as f: - filename = f.name - self.temp_files.append(filename) - - writer = BinaryCollector(filename, 1000, compression="none") - expected = [] - for i in range(num_samples): - status = THREAD_STATUS_HAS_GIL if i % 2 else 0 - ts = 1000 + i - expected.append((status, [ts])) - sample = [ - make_interpreter(0, [make_thread(1, [frame], status)]) - ] - writer.collect(sample, timestamp_us=ts) - writer.export(None) - - collector = BatchCollector() - with BinaryReader(filename) as reader: - count = reader.replay_samples(collector) - - self.assertEqual(count, num_samples) - self.assertEqual(len(collector.batches), num_samples) - self.assertEqual(collector.batches, expected) - - - def test_rle_long_run_splits_batches(self): - # Construct a single repeat record larger than the writer's buffer. - num_samples = 8193 - filename = self.create_binary_file([], compression="none") - data = bytearray(pathlib.Path(filename).read_bytes()) - record = (struct.pack("=QIB", 1, 0, 0) # STACK_REPEAT - + b"\x81\x40" # 8193 as a varint - + b"\x01\x00" * num_samples) # delta=1, status=0 - data[64:64] = record - struct.pack_into("=Q", data, 12, 0) # start timestamp - struct.pack_into("=Q", data, 28, num_samples) - struct.pack_into("=I", data, 36, 1) # thread count - for offset in (40, 48): # string and frame table offsets - old_offset = struct.unpack_from("=Q", data, offset)[0] - struct.pack_into("=Q", data, offset, old_offset + len(record)) - struct.pack_into("=Q", data, len(data) - 24, len(data)) - pathlib.Path(filename).write_bytes(data) - - collector = mock.Mock() - with BinaryReader(filename) as reader: - count = reader.replay_samples(collector) - batches = [call.args[1] for call in collector.collect.call_args_list] - self.assertEqual(count, num_samples) - self.assertEqual([len(batch) for batch in batches], [8192, 1]) - self.assertEqual([ts for batch in batches for ts in batch], - list(range(1, num_samples + 1))) - class TestBinaryStress(BinaryFormatTestBase): """Randomized stress tests for binary format.""" diff --git a/Lib/test/test_profiling/test_sampling_profiler/test_blocking.py b/Lib/test/test_profiling/test_sampling_profiler/test_blocking.py index 84b9e7aca0ffbd6..0a5541c733d4c77 100644 --- a/Lib/test/test_profiling/test_sampling_profiler/test_blocking.py +++ b/Lib/test/test_profiling/test_sampling_profiler/test_blocking.py @@ -5,7 +5,6 @@ import subprocess import sys import textwrap -import time import unittest from unittest import mock @@ -214,59 +213,3 @@ def test_run_blocking_exits_after_target_process_exits(self): replay.returncode, 0, f"stdout:\n{replay.stdout}\nstderr:\n{replay.stderr}", ) - - -@requires_remote_subprocess_debugging() -@unittest.skipUnless(sys.platform == "win32", "Windows only") -class TestBlockingModeSuspension(unittest.TestCase): - def test_all_threads_stop_before_pause_returns(self): - import mmap - - tag = f"cpython_blocking_{os.getpid()}_{id(self)}" - script = textwrap.dedent(f''' - import mmap - import struct - import threading - - memory = mmap.mmap(-1, 16, tagname={tag!r}) - - def worker(offset): - counter = 0 - while True: - counter += 1 - struct.pack_into("q", memory, offset, counter) - - for offset in (0, 8): - threading.Thread(target=worker, args=(offset,), daemon=True).start() - _test_sock.sendall(b"working") - _test_sock.recv(1) - ''') - with mmap.mmap(-1, 16, tagname=tag) as memory: - with test_subprocess(script, wait_for_working=True) as subproc: - unwinder = _remote_debugging.RemoteUnwinder( - subproc.process.pid, all_threads=True) - deadline = time.monotonic() + SHORT_TIMEOUT - while not all(memory[offset:offset + 8] != bytes(8) - for offset in (0, 8)): - self.assertLess(time.monotonic(), deadline, - "Worker threads did not start") - time.sleep(0.001) - for _ in range(100): - self.assertTrue(unwinder.pause_threads()) - try: - before = memory[:] - self.assertFalse(unwinder.pause_threads()) - unwinder.get_stack_trace() - time.sleep(0.001) - self.assertEqual(memory[:], before, - "Target memory changed while paused") - finally: - unwinder.resume_threads() - self.assertFalse(unwinder.resume_threads()) - before = memory[:] - deadline = time.monotonic() + SHORT_TIMEOUT - while any(memory[offset:offset + 8] == before[offset:offset + 8] - for offset in (0, 8)): - self.assertLess(time.monotonic(), deadline, - "Worker threads did not resume") - time.sleep(0.001) diff --git a/Lib/test/test_profiling/test_sampling_profiler/test_collectors.py b/Lib/test/test_profiling/test_sampling_profiler/test_collectors.py index eb58c29dd361d3a..533ca36376e5698 100644 --- a/Lib/test/test_profiling/test_sampling_profiler/test_collectors.py +++ b/Lib/test/test_profiling/test_sampling_profiler/test_collectors.py @@ -4,11 +4,10 @@ import marshal import opcode import os -import sys import tempfile import unittest -from test.support import is_emscripten, set_recursion_limit +from test.support import is_emscripten try: import _remote_debugging # noqa: F401 @@ -467,28 +466,6 @@ def test_collapsed_stack_collector_export(self): self.assertIn(stack1_expected, lines) self.assertIn(stack2_expected, lines) - def test_collapsed_stack_collector_export_non_ascii_names(self): - # gh-156810: frame names are written verbatim, so the output must be - # opened with an encoding that can represent non-ASCII and - # surrogate-escaped (undecodable-path) names. - collapsed_out = tempfile.NamedTemporaryFile(delete=False) - self.addCleanup(close_and_unlink, collapsed_out) - - collector = CollapsedStackCollector(1000) - frame = MockFrameInfo("/tmp/ba\udc80d.py", 5, "计算") - collector.collect([ - MockInterpreterInfo(0, [MockThreadInfo(1, [frame])]) - ]) - - with captured_stdout(), captured_stderr(): - collector.export(collapsed_out.name) - - with open(collapsed_out.name, encoding="utf-8", - errors="surrogatepass") as f: - content = f.read() - self.assertIn("计算", content) - self.assertIn("ba\udc80d.py", content) - def test_flamegraph_collector_basic(self): """Test basic FlamegraphCollector functionality.""" collector = FlamegraphCollector(1000) @@ -611,38 +588,6 @@ def test_flamegraph_collector_empty_export_fails(self): self.assertFalse(export_ok) self.assertEqual(os.path.getsize(flamegraph_out.name), 0) - def test_flamegraph_deep_stack_export(self): - flamegraph_out = tempfile.NamedTemporaryFile( - suffix=".html", delete=False - ) - self.addCleanup(close_and_unlink, flamegraph_out) - - collector = FlamegraphCollector(1000) - # Deeper than the default recursion limit. - frames = [MockFrameInfo("f.py", i + 1, f"f{i}") for i in range(1536)] - collector.collect( - [MockInterpreterInfo(0, [MockThreadInfo(1, frames)])]) - - with set_recursion_limit(1000), captured_stdout(), captured_stderr(): - export_ok = collector.export(flamegraph_out.name) - self.assertEqual(sys.getrecursionlimit(), 1000) - - self.assertTrue(export_ok) - self.assertGreater(os.path.getsize(flamegraph_out.name), 0) - - def test_flamegraph_export_restores_recursion_limit(self): - collector = FlamegraphCollector(1000) - frame = MockFrameInfo("f.py", 1, "f") - with set_recursion_limit(500), captured_stdout(), captured_stderr(): - self.assertFalse(collector.export(None)) - self.assertEqual(sys.getrecursionlimit(), 500) - collector.collect([ - MockInterpreterInfo(0, [MockThreadInfo(1, [ - frame, MockFrameInfo("f.py", 2, "caller")])])]) - with self.assertRaises(TypeError): - collector.export(None) - self.assertEqual(sys.getrecursionlimit(), 500) - def test_gecko_collector_basic(self): """Test basic GeckoCollector functionality.""" collector = GeckoCollector(1000) diff --git a/Lib/test/test_profiling/test_sampling_profiler/test_sync_coordinator.py b/Lib/test/test_profiling/test_sampling_profiler/test_sync_coordinator.py deleted file mode 100644 index 936c01e206f9fbe..000000000000000 --- a/Lib/test/test_profiling/test_sampling_profiler/test_sync_coordinator.py +++ /dev/null @@ -1,113 +0,0 @@ -"""Tests for the sampling profiler's synchronization coordinator.""" - -import os -import socket -import subprocess -import sys -import tempfile -import unittest - -from test.support import SHORT_TIMEOUT, os_helper, requires_subprocess - - -@requires_subprocess() -class TestSyncCoordinatorScriptExecution(unittest.TestCase): - """Tests for how the coordinator executes a target script.""" - - def run_coordinator(self, cwd, target, *target_args): - """Execute *target* from *cwd* with the sync coordinator. - - Returns the ``(stdout, stderr)`` captured from the coordinator. - """ - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server: - server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - server.bind(("127.0.0.1", 0)) - server.listen(1) - server.settimeout(SHORT_TIMEOUT) - port = server.getsockname()[1] - - cmd = ( - sys.executable, - "-m", - "profiling.sampling._sync_coordinator", - str(port), - cwd, - target, - ) + tuple(target_args) - process = subprocess.Popen( - cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - ) - try: - conn, _ = server.accept() - with conn: - # _signal_readiness() sends b"ready" before running target. - self.assertEqual(conn.recv(64), b"ready") - stdout, stderr = process.communicate(timeout=SHORT_TIMEOUT) - finally: - if process.poll() is None: - process.kill() - process.wait() - return stdout, stderr - - def test_script_directory_on_sys_path(self): - # gh-158540: The coordinator must put the directory containing the - # script (not the working directory) on sys.path, matching - # ``python script.py``, so that modules next to the script can be - # imported even when the coordinator runs from another directory. - with tempfile.TemporaryDirectory() as tmpdir: - script_dir = os.path.join(tmpdir, "sub") - os.mkdir(script_dir) - with open(os.path.join(script_dir, "helper.py"), "w") as f: - f.write("message = 'helper imported'\n") - with open(os.path.join(script_dir, "where.py"), "w") as f: - f.write( - "import os\n" - "import sys\n" - "print('PATH0:', os.path.realpath(sys.path[0]))\n" - "import helper\n" - "print('HELPER:', helper.message)\n" - ) - - stdout, stderr = self.run_coordinator( - tmpdir, os.path.join("sub", "where.py") - ) - - self.assertNotIn("ModuleNotFoundError", stderr) - self.assertIn("HELPER: helper imported", stdout) - self.assertIn(f"PATH0: {os.path.realpath(script_dir)}", stdout) - - @os_helper.skip_unless_symlink - def test_symlinked_script_uses_real_directory(self): - # gh-158540: ``python script.py`` resolves symlinks when computing - # sys.path[0], so a symlinked script must import modules next to the - # real script, not next to the link. - with tempfile.TemporaryDirectory() as tmpdir: - script_dir = os.path.join(tmpdir, "sub") - os.mkdir(script_dir) - with open(os.path.join(script_dir, "helper.py"), "w") as f: - f.write("message = 'helper imported'\n") - with open(os.path.join(script_dir, "where.py"), "w") as f: - f.write( - "import os\n" - "import sys\n" - "print('PATH0:', os.path.realpath(sys.path[0]))\n" - "import helper\n" - "print('HELPER:', helper.message)\n" - ) - os.symlink( - os.path.join("sub", "where.py"), - os.path.join(tmpdir, "link.py"), - ) - - stdout, stderr = self.run_coordinator(tmpdir, "link.py") - - self.assertNotIn("ModuleNotFoundError", stderr) - self.assertIn("HELPER: helper imported", stdout) - self.assertIn(f"PATH0: {os.path.realpath(script_dir)}", stdout) - - -if __name__ == "__main__": - unittest.main() diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-03-16-14-26.gh-issue-158583.7EUMvS.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-03-16-14-26.gh-issue-158583.7EUMvS.rst deleted file mode 100644 index c94fcc58add88c5..000000000000000 --- a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-03-16-14-26.gh-issue-158583.7EUMvS.rst +++ /dev/null @@ -1,2 +0,0 @@ -:meth:`bytes.fromhex` and :meth:`bytearray.fromhex`: Fix uninitialized -memory read. Patch by Victor Stinner. diff --git a/Misc/NEWS.d/next/Library/2026-07-01-18-00-00.gh-issue-152721.rlequad.rst b/Misc/NEWS.d/next/Library/2026-07-01-18-00-00.gh-issue-152721.rlequad.rst deleted file mode 100644 index 4dac0ed245bd672..000000000000000 --- a/Misc/NEWS.d/next/Library/2026-07-01-18-00-00.gh-issue-152721.rlequad.rst +++ /dev/null @@ -1,2 +0,0 @@ -Fix quadratic replay time in the :mod:`profiling.sampling` binary reader when a -profile's run-length-encoded samples alternate thread status. diff --git a/Misc/NEWS.d/next/Library/2026-07-02-15-26-51.gh-issue-151292.nmnQlp.rst b/Misc/NEWS.d/next/Library/2026-07-02-15-26-51.gh-issue-151292.nmnQlp.rst deleted file mode 100644 index 8825a70047eedd4..000000000000000 --- a/Misc/NEWS.d/next/Library/2026-07-02-15-26-51.gh-issue-151292.nmnQlp.rst +++ /dev/null @@ -1,3 +0,0 @@ -Fix ``profiling.sampling --binary`` leaving unreadable profile files when -the binary format reaches a size limit. Preserve collected samples when the -writer can still finalize safely. Patch by Maurycy Pawłowski-Wieroński. diff --git a/Misc/NEWS.d/next/Library/2026-07-08-22-18-04.gh-issue-153364.JBFHEg.rst b/Misc/NEWS.d/next/Library/2026-07-08-22-18-04.gh-issue-153364.JBFHEg.rst deleted file mode 100644 index b60a75b1c59738e..000000000000000 --- a/Misc/NEWS.d/next/Library/2026-07-08-22-18-04.gh-issue-153364.JBFHEg.rst +++ /dev/null @@ -1,3 +0,0 @@ -Make frame, coroutine and task-waiter walks iterative and bounded, avoiding -potential hangs and stack overflows. Fix asyncio task inspection when -awaited-by sets contain removed entries. Patch by Maurycy Pawłowski-Wieroński. diff --git a/Misc/NEWS.d/next/Library/2026-07-17-12-00-00.gh-issue-153838.HmCp5s.rst b/Misc/NEWS.d/next/Library/2026-07-17-12-00-00.gh-issue-153838.HmCp5s.rst deleted file mode 100644 index faecab8cff3e7b1..000000000000000 --- a/Misc/NEWS.d/next/Library/2026-07-17-12-00-00.gh-issue-153838.HmCp5s.rst +++ /dev/null @@ -1,2 +0,0 @@ -Skip non-regular files in the ``profiling.sampling`` heatmap exporter -instead of reading them unconditionally. Patch by tonghuaroot. diff --git a/Misc/NEWS.d/next/Library/2026-07-19-22-08-05.gh-issue-154194.1bqRdx.rst b/Misc/NEWS.d/next/Library/2026-07-19-22-08-05.gh-issue-154194.1bqRdx.rst deleted file mode 100644 index 9971cd3f8dc25bd..000000000000000 --- a/Misc/NEWS.d/next/Library/2026-07-19-22-08-05.gh-issue-154194.1bqRdx.rst +++ /dev/null @@ -1,3 +0,0 @@ -Fix the sampling profiler dropping entire samples when a non-fatal read fails; -frames now keep any readable metadata, and long funcnames and filenames are -truncated instead. Patch by Maurycy Pawłowski-Wieroński. diff --git a/Misc/NEWS.d/next/Library/2026-08-15-10-20-40.gh-issue-155811.knP-YB.rst b/Misc/NEWS.d/next/Library/2026-08-15-10-20-40.gh-issue-155811.knP-YB.rst deleted file mode 100644 index 2032fd74380db8b..000000000000000 --- a/Misc/NEWS.d/next/Library/2026-08-15-10-20-40.gh-issue-155811.knP-YB.rst +++ /dev/null @@ -1,3 +0,0 @@ -Add a sequence counter to GC statistics to prevent :mod:`!_remote_debugging` -returning inconsistent snapshots caused by non-atomic reads. Patch by Maurycy -Pawłowski-Wieroński. diff --git a/Misc/NEWS.d/next/Library/2026-08-29-11-37-40.gh-issue-156545.fLaMe1.rst b/Misc/NEWS.d/next/Library/2026-08-29-11-37-40.gh-issue-156545.fLaMe1.rst deleted file mode 100644 index cc4f57ff42adabb..000000000000000 --- a/Misc/NEWS.d/next/Library/2026-08-29-11-37-40.gh-issue-156545.fLaMe1.rst +++ /dev/null @@ -1,3 +0,0 @@ -Fix the sampling profiler's flamegraph export so that profiling a deeply -recursive program no longer fails with :exc:`RecursionError` instead of -producing a flamegraph. Patch by tonghuaroot. diff --git a/Misc/NEWS.d/next/Library/2026-09-02-17-28-16.gh-issue-156810.cLpEnc.rst b/Misc/NEWS.d/next/Library/2026-09-02-17-28-16.gh-issue-156810.cLpEnc.rst deleted file mode 100644 index 610b0b4fcdc1baa..000000000000000 --- a/Misc/NEWS.d/next/Library/2026-09-02-17-28-16.gh-issue-156810.cLpEnc.rst +++ /dev/null @@ -1,4 +0,0 @@ -Fix a :exc:`UnicodeEncodeError` crash in the sampling profiler's -collapsed-stack export (``--collapsed``) when a sampled frame's function or -file name contains non-ASCII or surrogate-escaped characters. The output file -is now written as UTF-8. diff --git a/Misc/NEWS.d/next/Library/2026-09-30-20-09-46.gh-issue-158522.L6j7tU.rst b/Misc/NEWS.d/next/Library/2026-09-30-20-09-46.gh-issue-158522.L6j7tU.rst deleted file mode 100644 index 30c2df4bacd4a15..000000000000000 --- a/Misc/NEWS.d/next/Library/2026-09-30-20-09-46.gh-issue-158522.L6j7tU.rst +++ /dev/null @@ -1,2 +0,0 @@ -Fix :mod:`profiling.sampling` showing a single frame for a task whose -coroutine recurses. Patch by Timofei Ivankov. diff --git a/Misc/NEWS.d/next/Library/2026-09-30-22-10-00.gh-issue-158540.kQ9vXm.rst b/Misc/NEWS.d/next/Library/2026-09-30-22-10-00.gh-issue-158540.kQ9vXm.rst deleted file mode 100644 index 9cbd698cf9a96e1..000000000000000 --- a/Misc/NEWS.d/next/Library/2026-09-30-22-10-00.gh-issue-158540.kQ9vXm.rst +++ /dev/null @@ -1,4 +0,0 @@ -Fix :mod:`profiling.sampling` not adding the directory containing the -profiled script to :data:`sys.path`, which prevented the script from -importing modules placed next to it. The script's directory is now added, -matching the behavior of ``python script.py``. diff --git a/Misc/NEWS.d/next/Library/2026-10-01-14-00-00.gh-issue-158539.q1w2e3.rst b/Misc/NEWS.d/next/Library/2026-10-01-14-00-00.gh-issue-158539.q1w2e3.rst deleted file mode 100644 index b1ab9c8e017fb26..000000000000000 --- a/Misc/NEWS.d/next/Library/2026-10-01-14-00-00.gh-issue-158539.q1w2e3.rst +++ /dev/null @@ -1,5 +0,0 @@ -Fix :mod:`profiling.sampling` exception mode discarding samples for code -running inside an ``except`` block in a generator or coroutine. The remote -debugger now follows ``tstate->exc_info`` and its ``previous_item`` chain -instead of only reading the embedded ``exc_state``, matching the exception -that :func:`sys.exception` reports. diff --git a/Misc/NEWS.d/next/Library/2026-10-04-20-00-00.gh-issue-158552.windows.rst b/Misc/NEWS.d/next/Library/2026-10-04-20-00-00.gh-issue-158552.windows.rst deleted file mode 100644 index e46f9edcebe09bf..000000000000000 --- a/Misc/NEWS.d/next/Library/2026-10-04-20-00-00.gh-issue-158552.windows.rst +++ /dev/null @@ -1,3 +0,0 @@ -Wait for Windows threads to finish suspending before reading target memory -in the sampling profiler's blocking mode. Resume the target if waiting for -suspension fails. diff --git a/Modules/_remote_debugging/_remote_debugging.h b/Modules/_remote_debugging/_remote_debugging.h index 79fa4a92e745bd0..fa37fb7b2167ecf 100644 --- a/Modules/_remote_debugging/_remote_debugging.h +++ b/Modules/_remote_debugging/_remote_debugging.h @@ -105,7 +105,6 @@ typedef struct { #ifdef MS_WINDOWS #define STATUS_SUCCESS ((NTSTATUS)0x00000000L) #define STATUS_INFO_LENGTH_MISMATCH ((NTSTATUS)0xC0000004L) -#define STATUS_NO_MORE_ENTRIES ((NTSTATUS)0x8000001AL) typedef enum _WIN32_THREADSTATE { WIN32_THREADSTATE_INITIALIZED = 0, WIN32_THREADSTATE_READY = 1, @@ -148,8 +147,6 @@ typedef enum _WIN32_THREADSTATE { #define MAX_STACK_CHUNK_SIZE (16 * 1024 * 1024) /* 16 MB max for stack chunks */ #define MAX_LONG_DIGITS 64 /* Allows values up to ~2^1920 */ #define MAX_SET_TABLE_SIZE (1 << 20) /* 1 million entries max for set iteration */ -#define MAX_FRAME_CHAIN_DEPTH (1024 + 512) /* Iteration bound for frame chain walks */ -#define MAX_TASK_WAITER_WALK_TASKS (1 << 14) /* Total-task bound for waiter walks */ #ifndef MAX #define MAX(a, b) ((a) > (b) ? (a) : (b)) @@ -183,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 && !_Py_RemoteDebug_IsFatalReadError()) { \ + if (unwinder->debug && !_Py_RemoteDebug_HasPermissionError()) { \ _set_debug_exception_cause(exc_type, message); \ } \ } while (0) @@ -481,6 +478,12 @@ typedef int (*thread_processor_func)( void *context ); +typedef int (*set_entry_processor_func)( + RemoteUnwinderObject *unwinder, + uintptr_t key_addr, + void *context +); + typedef int (*interpreter_processor_func)( RuntimeOffsets *offsets, uintptr_t interpreter_state_addr, @@ -525,6 +528,7 @@ extern int validate_debug_offsets(struct _Py_DebugOffsets *debug_offsets); * ============================================================================ */ extern int read_ptr(RemoteUnwinderObject *unwinder, uintptr_t address, uintptr_t *result); +extern int read_Py_ssize_t(RemoteUnwinderObject *unwinder, uintptr_t address, Py_ssize_t *result); extern int read_char(RemoteUnwinderObject *unwinder, uintptr_t address, char *result); extern int read_py_ptr(RemoteUnwinderObject *unwinder, uintptr_t address, uintptr_t *ptr_addr); @@ -725,11 +729,33 @@ extern int parse_task( PyObject *render_to ); +extern int parse_coro_chain( + RemoteUnwinderObject *unwinder, + uintptr_t coro_address, + PyObject *render_to +); + extern int parse_async_frame_chain( RemoteUnwinderObject *unwinder, PyObject *calls, uintptr_t address_of_thread, - uintptr_t running_task_frame + uintptr_t running_task_code_obj +); + +/* Set iteration */ +extern int iterate_set_entries( + RemoteUnwinderObject *unwinder, + uintptr_t set_addr, + set_entry_processor_func processor, + void *context +); + +/* Task awaited_by processing */ +extern int process_task_awaited_by( + RemoteUnwinderObject *unwinder, + uintptr_t task_address, + set_entry_processor_func processor, + void *context ); extern int process_single_task_node( @@ -739,16 +765,22 @@ extern int process_single_task_node( PyObject *result ); +extern int process_task_and_waiters( + RemoteUnwinderObject *unwinder, + uintptr_t task_addr, + PyObject *result +); + extern int find_running_task_in_thread( RemoteUnwinderObject *unwinder, uintptr_t thread_state_addr, uintptr_t *running_task_addr ); -extern int get_task_frame( +extern int get_task_code_object( RemoteUnwinderObject *unwinder, uintptr_t task_addr, - uintptr_t *frame_addr + uintptr_t *code_obj_addr ); extern int append_awaited_by( diff --git a/Modules/_remote_debugging/asyncio.c b/Modules/_remote_debugging/asyncio.c index c8d79774a47a738..44a9a3cbce0061a 100644 --- a/Modules/_remote_debugging/asyncio.c +++ b/Modules/_remote_debugging/asyncio.c @@ -116,11 +116,12 @@ ensure_async_debug_offsets(RemoteUnwinderObject *unwinder) * SET ITERATION FUNCTIONS * ============================================================================ */ -static int +int iterate_set_entries( RemoteUnwinderObject *unwinder, uintptr_t set_addr, - PyObject *awaited_by + set_entry_processor_func processor, + void *context ) { char set_object[SIZEOF_SET_OBJ]; if (_Py_RemoteDebug_PagedReadRemoteMemory(&unwinder->handle, set_addr, @@ -145,22 +146,28 @@ iterate_set_entries( Py_ssize_t i = 0; Py_ssize_t els = 0; while (i < set_len && els < num_els) { - setentry entry; - if (_Py_RemoteDebug_PagedReadRemoteMemory( - &unwinder->handle, table_ptr, sizeof(entry), &entry) < 0) - { - set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read set entry"); + uintptr_t key_addr; + if (read_py_ptr(unwinder, table_ptr, &key_addr) < 0) { + set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read set entry key"); return -1; } - uintptr_t key_addr = (uintptr_t)entry.key; - if (key_addr != 0 && entry.hash != -1) { - if (parse_task(unwinder, key_addr, awaited_by) < 0) { + if ((void*)key_addr != NULL) { + Py_ssize_t ref_cnt; + if (read_Py_ssize_t(unwinder, table_ptr, &ref_cnt) < 0) { + set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read set entry ref count"); return -1; } - els++; + + if (ref_cnt) { + // Process this valid set entry + if (processor(unwinder, key_addr, context) < 0) { + return -1; + } + els++; + } } - table_ptr += sizeof(entry); + table_ptr += sizeof(void*) * 2; i++; } @@ -241,14 +248,12 @@ parse_task_name( * ============================================================================ */ static int -get_awaited_coro_address( +handle_yield_from_frame( RemoteUnwinderObject *unwinder, uintptr_t gi_iframe_addr, uintptr_t gen_type_addr, - uintptr_t *next_coro + PyObject *render_to ) { - *next_coro = 0; - // Read the entire interpreter frame at once char iframe[SIZEOF_INTERP_FRAME]; int err = _Py_RemoteDebug_PagedReadRemoteMemory( @@ -304,7 +309,11 @@ get_awaited_coro_address( doesn't match the type of whatever it points to in its cr_await. */ - *next_coro = gi_await_addr; + err = parse_coro_chain(unwinder, gi_await_addr, render_to); + if (err) { + set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to parse coroutine chain in yield_from"); + return -1; + } } } } @@ -312,7 +321,7 @@ get_awaited_coro_address( return 0; } -static int +int parse_coro_chain( RemoteUnwinderObject *unwinder, uintptr_t coro_address, @@ -320,64 +329,49 @@ parse_coro_chain( ) { assert((void*)coro_address != NULL); - for (size_t depth = 0; (void*)coro_address != NULL; depth++) { - if (depth >= MAX_FRAME_CHAIN_DEPTH) { - PyErr_SetString(PyExc_RuntimeError, - "Too many coroutine frames (possible infinite loop)"); - set_exception_cause(unwinder, PyExc_RuntimeError, - "Coroutine chain depth limit exceeded"); - return -1; - } - - // Read the entire generator object at once - char gen_object[SIZEOF_GEN_OBJ]; - int err = _Py_RemoteDebug_PagedReadRemoteMemory( - &unwinder->handle, - coro_address, - SIZEOF_GEN_OBJ, - gen_object); - if (err < 0) { - set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read generator object in coro chain"); - return -1; - } + // Read the entire generator object at once + char gen_object[SIZEOF_GEN_OBJ]; + int err = _Py_RemoteDebug_PagedReadRemoteMemory( + &unwinder->handle, + coro_address, + SIZEOF_GEN_OBJ, + gen_object); + if (err < 0) { + set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read generator object in coro chain"); + return -1; + } - int8_t frame_state = GET_MEMBER(int8_t, gen_object, unwinder->debug_offsets.gen_object.gi_frame_state); - if (frame_state == FRAME_CLEARED) { - return 0; - } + int8_t frame_state = GET_MEMBER(int8_t, gen_object, unwinder->debug_offsets.gen_object.gi_frame_state); + if (frame_state == FRAME_CLEARED) { + return 0; + } - uintptr_t gen_type_addr = GET_MEMBER(uintptr_t, gen_object, unwinder->debug_offsets.pyobject.ob_type); + uintptr_t gen_type_addr = GET_MEMBER(uintptr_t, gen_object, unwinder->debug_offsets.pyobject.ob_type); - PyObject* name = NULL; + PyObject* name = NULL; - // Parse the previous frame using the gi_iframe from local copy - uintptr_t prev_frame; - uintptr_t gi_iframe_addr = coro_address + (uintptr_t)unwinder->debug_offsets.gen_object.gi_iframe; - uintptr_t address_of_code_object = 0; - if (parse_frame_object(unwinder, &name, gi_iframe_addr, &address_of_code_object, &prev_frame) < 0) { - set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to parse frame object in coro chain"); - return -1; - } + // Parse the previous frame using the gi_iframe from local copy + uintptr_t prev_frame; + uintptr_t gi_iframe_addr = coro_address + (uintptr_t)unwinder->debug_offsets.gen_object.gi_iframe; + uintptr_t address_of_code_object = 0; + if (parse_frame_object(unwinder, &name, gi_iframe_addr, &address_of_code_object, &prev_frame) < 0) { + set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to parse frame object in coro chain"); + return -1; + } - if (!name) { - return 0; - } + if (!name) { + return 0; + } - if (PyList_Append(render_to, name)) { - Py_DECREF(name); - set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to append frame to coro chain"); - return -1; - } + if (PyList_Append(render_to, name)) { Py_DECREF(name); + set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to append frame to coro chain"); + return -1; + } + Py_DECREF(name); - if (frame_state != FRAME_SUSPENDED_YIELD_FROM) { - return 0; - } - - if (get_awaited_coro_address(unwinder, gi_iframe_addr, gen_type_addr, - &coro_address) < 0) { - return -1; - } + if (frame_state == FRAME_SUSPENDED_YIELD_FROM) { + return handle_yield_from_frame(unwinder, gi_iframe_addr, gen_type_addr, render_to); } return 0; @@ -519,11 +513,35 @@ parse_task( * TASK AWAITED_BY PROCESSING * ============================================================================ */ +// Forward declaration for mutual recursion +static int process_waiter_task(RemoteUnwinderObject *unwinder, uintptr_t key_addr, void *context); + +// Processor function for parsing tasks in sets +static int +process_task_parser( + RemoteUnwinderObject *unwinder, + uintptr_t key_addr, + void *context +) { + PyObject *awaited_by = (PyObject *)context; + return parse_task(unwinder, key_addr, awaited_by); +} + static int parse_task_awaited_by( RemoteUnwinderObject *unwinder, uintptr_t task_address, PyObject *awaited_by +) { + return process_task_awaited_by(unwinder, task_address, process_task_parser, awaited_by); +} + +int +process_task_awaited_by( + RemoteUnwinderObject *unwinder, + uintptr_t task_address, + set_entry_processor_func processor, + void *context ) { // Read the entire TaskObj at once char task_obj[SIZEOF_TASK_OBJ]; @@ -542,10 +560,10 @@ parse_task_awaited_by( char awaited_by_is_a_set = GET_MEMBER(char, task_obj, unwinder->async_debug_offsets.asyncio_task_object.task_awaited_by_is_set); if (awaited_by_is_a_set) { - return iterate_set_entries(unwinder, task_ab_addr, awaited_by); + return iterate_set_entries(unwinder, task_ab_addr, processor, context); } else { // Single task waiting - return parse_task(unwinder, task_ab_addr, awaited_by); + return processor(unwinder, task_ab_addr, context); } } @@ -640,40 +658,30 @@ process_single_task_node( return -1; } -static int -process_task_waiters( +int +process_task_and_waiters( RemoteUnwinderObject *unwinder, + uintptr_t task_addr, PyObject *result ) { - for (Py_ssize_t i = 0; i < PyList_GET_SIZE(result); i++) { - PyObject *task_info = PyList_GET_ITEM(result, i); - PyObject *waiters = PyStructSequence_GET_ITEM(task_info, 3); - for (Py_ssize_t j = 0; j < PyList_GET_SIZE(waiters); j++) { - if (PyList_GET_SIZE(result) >= MAX_TASK_WAITER_WALK_TASKS) { - PyErr_SetString(PyExc_RuntimeError, - "Too many task waiters (possible infinite loop)"); - set_exception_cause(unwinder, PyExc_RuntimeError, - "Task waiter walk size limit exceeded"); - return -1; - } - PyObject *waiter = PyList_GET_ITEM(waiters, j); - // CoroInfo item 1 holds the waiter task address stored by parse_task(). - PyObject *task_id = PyStructSequence_GET_ITEM(waiter, 1); - void *task_ptr = PyLong_AsVoidPtr(task_id); - if (task_ptr == NULL && PyErr_Occurred()) { - set_exception_cause(unwinder, PyExc_RuntimeError, - "Failed to parse waiter task ID"); - return -1; - } - if (process_single_task_node( - unwinder, (uintptr_t)task_ptr, NULL, result) < 0) - { - return -1; - } - } + // First, add this task to the result + if (process_single_task_node(unwinder, task_addr, NULL, result) < 0) { + return -1; } - return 0; + // Now find all tasks that are waiting for this task and process them + return process_task_awaited_by(unwinder, task_addr, process_waiter_task, result); +} + +// Processor function for task waiters +static int +process_waiter_task( + RemoteUnwinderObject *unwinder, + uintptr_t key_addr, + void *context +) { + PyObject *result = (PyObject *)context; + return process_task_and_waiters(unwinder, key_addr, result); } /* ============================================================================ @@ -716,7 +724,7 @@ find_running_task_in_thread( } int -get_task_frame(RemoteUnwinderObject *unwinder, uintptr_t task_addr, uintptr_t *frame_addr) { +get_task_code_object(RemoteUnwinderObject *unwinder, uintptr_t task_addr, uintptr_t *code_obj_addr) { uintptr_t running_coro_addr = 0; if(read_py_ptr( @@ -733,7 +741,21 @@ get_task_frame(RemoteUnwinderObject *unwinder, uintptr_t task_addr, uintptr_t *f return -1; } - *frame_addr = running_coro_addr + (uintptr_t)unwinder->debug_offsets.gen_object.gi_iframe; + // note: genobject's gi_iframe is an embedded struct so the address to + // the offset leads directly to its first field: f_executable + if (read_py_ptr( + unwinder, + running_coro_addr + (uintptr_t)unwinder->debug_offsets.gen_object.gi_iframe, code_obj_addr) < 0) { + set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read running task code object"); + return -1; + } + + if (*code_obj_addr == 0) { + PyErr_SetString(PyExc_RuntimeError, "Running task code object is NULL"); + set_exception_cause(unwinder, PyExc_RuntimeError, "Running task code object address is NULL"); + return -1; + } + return 0; } @@ -746,7 +768,7 @@ parse_async_frame_chain( RemoteUnwinderObject *unwinder, PyObject *calls, uintptr_t address_of_thread, - uintptr_t running_task_frame + uintptr_t running_task_code_obj ) { uintptr_t address_of_current_frame; if (find_running_frame(unwinder, address_of_thread, &address_of_current_frame) < 0) { @@ -754,17 +776,9 @@ parse_async_frame_chain( return -1; } - size_t frame_count = 0; while ((void*)address_of_current_frame != NULL) { - if (++frame_count > MAX_FRAME_CHAIN_DEPTH) { - PyErr_SetString(PyExc_RuntimeError, "Too many async stack frames (possible infinite loop)"); - set_exception_cause(unwinder, PyExc_RuntimeError, "Async frame chain iteration limit exceeded"); - return -1; - } PyObject* frame_info = NULL; uintptr_t address_of_code_object; - - uintptr_t this_frame = address_of_current_frame; int res = parse_frame_object( unwinder, &frame_info, @@ -790,9 +804,7 @@ parse_async_frame_chain( Py_DECREF(frame_info); - // Stop at the task's own frame. Code objects are shared by - // recursive calls, so they cannot identify it. - if (this_frame == running_task_frame) { + if (address_of_code_object == running_task_code_obj) { break; } } @@ -928,8 +940,8 @@ process_running_task_chain( uintptr_t thread_state_addr, PyObject *result ) { - uintptr_t running_task_frame = 0; - if(get_task_frame(unwinder, running_task_addr, &running_task_frame) < 0) { + uintptr_t running_task_code_obj = 0; + if(get_task_code_object(unwinder, running_task_addr, &running_task_code_obj) < 0) { return -1; } @@ -961,12 +973,12 @@ process_running_task_chain( } // Add the chain from the current frame to this task - if (parse_async_frame_chain(unwinder, frame_chain, thread_state_addr, running_task_frame) < 0) { + if (parse_async_frame_chain(unwinder, frame_chain, thread_state_addr, running_task_code_obj) < 0) { return -1; } // Now find all tasks that are waiting for this task and process them - if (process_task_waiters(unwinder, result) < 0) { + if (process_task_awaited_by(unwinder, running_task_addr, process_waiter_task, result) < 0) { return -1; } diff --git a/Modules/_remote_debugging/binary_io.h b/Modules/_remote_debugging/binary_io.h index 6a2c5b795823e15..c936d3372e5acda 100644 --- a/Modules/_remote_debugging/binary_io.h +++ b/Modules/_remote_debugging/binary_io.h @@ -290,18 +290,9 @@ typedef struct { size_t pending_rle_samples; } ThreadEntry; -/* Limit errors occur before emitting an incomplete sample. Other write - * failures may leave partial records and must prevent finalization. */ -typedef enum { - BINARY_WRITER_OPEN, - BINARY_WRITER_LIMIT_REACHED, - BINARY_WRITER_BROKEN, -} BinaryWriterState; - /* Main binary writer structure */ typedef struct { FILE *fp; - BinaryWriterState state; /* Write buffer for batched I/O */ uint8_t *write_buffer; diff --git a/Modules/_remote_debugging/binary_io_reader.c b/Modules/_remote_debugging/binary_io_reader.c index 80627db913ea21b..9625ee6f301f05f 100644 --- a/Modules/_remote_debugging/binary_io_reader.c +++ b/Modules/_remote_debugging/binary_io_reader.c @@ -19,10 +19,6 @@ #include #endif -#ifdef _Py_MEMORY_SANITIZER -# include -#endif - /* ============================================================================ * CONSTANTS FOR BINARY FORMAT SIZES * ============================================================================ */ @@ -33,9 +29,6 @@ /* Progress callback frequency */ #define PROGRESS_CALLBACK_INTERVAL 1000 -/* Cap per-batch RLE samples to bound the timestamp list (gh-151378) */ -#define MAX_RLE_BATCH_SAMPLES 8192 - /* ============================================================================ * BINARY READER IMPLEMENTATION * ============================================================================ */ @@ -322,7 +315,6 @@ reader_decompress_samples(BinaryReader *reader, const uint8_t *data) return -1; } - _Py_MSAN_UNPOISON(output.dst, output.pos); total_output += output.pos; } @@ -1086,6 +1078,21 @@ emit_sample(RemoteDebuggingState *state, PyObject *collector, return 0; } +/* Helper to trim timestamp list and emit batch. Returns 0 on success, -1 on error. */ +static int +emit_batch(RemoteDebuggingState *state, PyObject *collector, + uint64_t thread_id, uint32_t interpreter_id, uint8_t status, + const uint32_t *frame_indices, size_t stack_depth, + BinaryReader *reader, PyObject *timestamps_list, Py_ssize_t actual_size) +{ + /* Trim list to actual size */ + if (PyList_SetSlice(timestamps_list, actual_size, PyList_GET_SIZE(timestamps_list), NULL) < 0) { + return -1; + } + return emit_sample(state, collector, thread_id, interpreter_id, status, + frame_indices, stack_depth, reader, timestamps_list); +} + /* Helper to invoke progress callback, returns -1 on error */ static inline int invoke_progress_callback(PyObject *callback, Py_ssize_t current, uint64_t total) @@ -1214,18 +1221,17 @@ binary_reader_replay(BinaryReader *reader, PyObject *collector, PyObject *progre ts->prev_timestamp += delta; /* Start new batch on first sample or status change */ - if (i == 0 || status != batch_status - || batch_idx >= MAX_RLE_BATCH_SAMPLES) { + if (i == 0 || status != batch_status) { if (timestamps_list) { - int rc = emit_sample(state, collector, thread_id, interpreter_id, - batch_status, ts->current_stack, ts->current_stack_depth, - reader, timestamps_list); + int rc = emit_batch(state, collector, thread_id, interpreter_id, + batch_status, ts->current_stack, ts->current_stack_depth, + reader, timestamps_list, batch_idx); Py_DECREF(timestamps_list); if (rc < 0) { return -1; } } - timestamps_list = PyList_New(0); + timestamps_list = PyList_New(count - i); if (!timestamps_list) { return -1; } @@ -1238,20 +1244,14 @@ binary_reader_replay(BinaryReader *reader, PyObject *collector, PyObject *progre Py_DECREF(timestamps_list); return -1; } - int append_rc = PyList_Append(timestamps_list, ts_obj); - Py_DECREF(ts_obj); - if (append_rc < 0) { - Py_DECREF(timestamps_list); - return -1; - } - batch_idx++; + PyList_SET_ITEM(timestamps_list, batch_idx++, ts_obj); } /* Emit final batch */ if (timestamps_list) { - int rc = emit_sample(state, collector, thread_id, interpreter_id, - batch_status, ts->current_stack, ts->current_stack_depth, - reader, timestamps_list); + int rc = emit_batch(state, collector, thread_id, interpreter_id, + batch_status, ts->current_stack, ts->current_stack_depth, + reader, timestamps_list, batch_idx); Py_DECREF(timestamps_list); if (rc < 0) { return -1; diff --git a/Modules/_remote_debugging/binary_io_writer.c b/Modules/_remote_debugging/binary_io_writer.c index 1794017053da503..6af81515e7131d1 100644 --- a/Modules/_remote_debugging/binary_io_writer.c +++ b/Modules/_remote_debugging/binary_io_writer.c @@ -19,10 +19,6 @@ #include #endif -#ifdef _Py_MEMORY_SANITIZER -# include -#endif - /* ============================================================================ * CONSTANTS FOR BINARY FORMAT SIZES * ============================================================================ */ @@ -239,7 +235,6 @@ writer_flush_buffer(BinaryWriter *writer) return -1; } - _Py_MSAN_UNPOISON(writer->zstd.compressed_buffer, output.pos); if (output.pos > 0) { if (fwrite_checked_allow_threads(writer->zstd.compressed_buffer, output.pos, writer->fp) < 0) { return -1; @@ -376,7 +371,6 @@ writer_intern_string(BinaryWriter *writer, PyObject *string, uint32_t *index) } if (writer->string_count >= UINT32_MAX) { - writer->state = BINARY_WRITER_LIMIT_REACHED; PyErr_SetString(PyExc_OverflowError, "too many strings for binary format"); return -1; @@ -386,9 +380,6 @@ writer_intern_string(BinaryWriter *writer, PyObject *string, uint32_t *index) (void **)&writer->string_lengths, &writer->string_capacity, sizeof(char *), sizeof(size_t)) < 0) { - if (PyErr_ExceptionMatches(PyExc_OverflowError)) { - writer->state = BINARY_WRITER_LIMIT_REACHED; - } return -1; } } @@ -399,7 +390,6 @@ writer_intern_string(BinaryWriter *writer, PyObject *string, uint32_t *index) return -1; } if ((uintmax_t)str_len > UINT32_MAX) { - writer->state = BINARY_WRITER_LIMIT_REACHED; PyErr_Format(PyExc_OverflowError, "string length %zd exceeds binary format maximum %u", str_len, UINT32_MAX); @@ -448,16 +438,12 @@ writer_intern_frame(BinaryWriter *writer, const FrameEntry *entry, uint32_t *ind } if (writer->frame_count >= UINT32_MAX) { - writer->state = BINARY_WRITER_LIMIT_REACHED; 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) { - if (PyErr_ExceptionMatches(PyExc_OverflowError)) { - writer->state = BINARY_WRITER_LIMIT_REACHED; - } return -1; } @@ -501,7 +487,6 @@ writer_get_or_create_thread_entry(BinaryWriter *writer, uint64_t thread_id, } if (writer->thread_count >= UINT32_MAX) { - writer->state = BINARY_WRITER_LIMIT_REACHED; PyErr_SetString(PyExc_OverflowError, "too many threads for binary format"); return NULL; @@ -511,9 +496,6 @@ writer_get_or_create_thread_entry(BinaryWriter *writer, uint64_t thread_id, &writer->thread_capacity, sizeof(ThreadEntry)); if (!new_entries) { - if (PyErr_ExceptionMatches(PyExc_OverflowError)) { - writer->state = BINARY_WRITER_LIMIT_REACHED; - } return NULL; } writer->thread_entries = new_entries; @@ -946,12 +928,6 @@ static int process_thread_sample(BinaryWriter *writer, PyObject *thread_info, uint32_t interpreter_id, uint64_t timestamp_us) { - if (writer->total_samples == UINT64_MAX) { - writer->state = BINARY_WRITER_LIMIT_REACHED; - PyErr_SetString(PyExc_OverflowError, "too many samples for binary format"); - return -1; - } - PyObject *thread_id_obj = PyStructSequence_GET_ITEM(thread_info, 0); PyObject *status_obj = PyStructSequence_GET_ITEM(thread_info, 1); PyObject *frame_list = PyStructSequence_GET_ITEM(thread_info, 2); @@ -974,6 +950,7 @@ process_thread_sample(BinaryWriter *writer, PyObject *thread_info, /* Calculate timestamp delta */ uint64_t delta = timestamp_us - entry->prev_timestamp; + entry->prev_timestamp = timestamp_us; /* Process frames and build current stack */ uint32_t curr_stack[MAX_STACK_DEPTH]; @@ -1029,7 +1006,6 @@ process_thread_sample(BinaryWriter *writer, PyObject *thread_info, entry->prev_stack_depth = curr_depth; } - entry->prev_timestamp = timestamp_us; writer->total_samples++; return 0; } @@ -1049,16 +1025,15 @@ binary_writer_write_sample(BinaryWriter *writer, PyObject *stack_frames, uint64_ PyObject *interp_id_obj = PyStructSequence_GET_ITEM(interp_info, 0); PyObject *threads = PyStructSequence_GET_ITEM(interp_info, 1); - unsigned long long interp_id_long = PyLong_AsUnsignedLongLong(interp_id_obj); - if (interp_id_long == (unsigned long long)-1 && PyErr_Occurred()) { + unsigned long interp_id_long = PyLong_AsUnsignedLong(interp_id_obj); + if (interp_id_long == (unsigned long)-1 && PyErr_Occurred()) { return -1; } /* Bounds check: interpreter_id is stored as uint32_t in binary format */ if (interp_id_long > UINT32_MAX) { - writer->state = BINARY_WRITER_LIMIT_REACHED; PyErr_Format(PyExc_OverflowError, - "interpreter_id %llu exceeds maximum value %u", - interp_id_long, UINT32_MAX); + "interpreter_id %lu exceeds maximum value %lu", + interp_id_long, (unsigned long)UINT32_MAX); return -1; } uint32_t interpreter_id = (uint32_t)interp_id_long; @@ -1109,7 +1084,6 @@ binary_writer_finalize(BinaryWriter *writer) return -1; } - _Py_MSAN_UNPOISON(writer->zstd.compressed_buffer, output.pos); if (output.pos > 0) { if (fwrite_checked_allow_threads(writer->zstd.compressed_buffer, output.pos, writer->fp) < 0) { return -1; diff --git a/Modules/_remote_debugging/code_objects.c b/Modules/_remote_debugging/code_objects.c index 2789c95e6e321b8..6ad058850196269 100644 --- a/Modules/_remote_debugging/code_objects.c +++ b/Modules/_remote_debugging/code_objects.c @@ -346,7 +346,6 @@ parse_code_object(RemoteUnwinderObject *unwinder, PyObject *func = NULL; PyObject *file = NULL; PyObject *linetable = NULL; - int code_metadata_incomplete = 0; #ifdef Py_GIL_DISABLED // In free threading builds, code object addresses might have the low bit set @@ -370,59 +369,30 @@ parse_code_object(RemoteUnwinderObject *unwinder, if (_Py_RemoteDebug_PagedReadRemoteMemory( &unwinder->handle, real_address, SIZEOF_CODE_OBJ, code_object) < 0) { - if (_Py_RemoteDebug_IsFatalReadError()) { - goto error; - } - PyErr_Clear(); - func = PyUnicode_FromString(""); - if (!func) { - goto error; - } - file = Py_NewRef(_Py_LATIN1_CHR('~')); - goto degraded; + set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read code object"); + goto error; } func = read_py_str(unwinder, GET_MEMBER(uintptr_t, code_object, unwinder->debug_offsets.code_object.qualname), 1024); if (!func) { - if (_Py_RemoteDebug_IsFatalReadError()) { - goto error; - } - PyErr_Clear(); - func = PyUnicode_FromString(""); - if (!func) { - goto error; - } - code_metadata_incomplete = 1; + set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read function name from code object"); + goto error; } file = read_py_str(unwinder, GET_MEMBER(uintptr_t, code_object, unwinder->debug_offsets.code_object.filename), 1024); if (!file) { - if (_Py_RemoteDebug_IsFatalReadError()) { - goto error; - } - PyErr_Clear(); - file = PyUnicode_FromString(""); - if (!file) { - goto error; - } - code_metadata_incomplete = 1; - } - - if (code_metadata_incomplete) { - goto degraded; + set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read filename from code object"); + goto error; } linetable = read_py_bytes(unwinder, GET_MEMBER(uintptr_t, code_object, unwinder->debug_offsets.code_object.linetable), MAX_LINETABLE_SIZE); if (!linetable) { - if (_Py_RemoteDebug_IsFatalReadError()) { - goto error; - } - PyErr_Clear(); - goto degraded; + set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read linetable from code object"); + goto error; } meta = PyMem_RawMalloc(sizeof(CachedCodeMetadata)); @@ -591,18 +561,6 @@ parse_code_object(RemoteUnwinderObject *unwinder, *result = tuple; return 0; -degraded: { - PyObject *degraded_tuple = make_frame_info(unwinder, file, Py_None, - func, Py_None); - Py_CLEAR(func); - Py_CLEAR(file); - if (!degraded_tuple) { - return -1; - } - *result = degraded_tuple; - return 0; -} - error: Py_XDECREF(func); Py_XDECREF(file); diff --git a/Modules/_remote_debugging/debug_offsets_validation.h b/Modules/_remote_debugging/debug_offsets_validation.h index 66c35e5522a9c7d..c0c01a0a639e196 100644 --- a/Modules/_remote_debugging/debug_offsets_validation.h +++ b/Modules/_remote_debugging/debug_offsets_validation.h @@ -48,15 +48,6 @@ static_assert( PY_REMOTE_ASYNC_DEBUG_OFFSETS_TOTAL_SIZE, "Update _remote_debugging validation for _Py_AsyncioModuleDebugOffsets"); -/* Derive unexported offsets from adjacent fields to keep the debug-offset - * table compatible across patch releases. */ -static_assert(offsetof(PyThreadState, exc_info) == - offsetof(PyThreadState, current_exception) + sizeof(uintptr_t), - "exc_info must immediately follow current_exception"); -static_assert(offsetof(_PyErr_StackItem, previous_item) == - offsetof(_PyErr_StackItem, exc_value) + sizeof(uintptr_t), - "previous_item must immediately follow exc_value"); - /* * This logic lives in a private header because it is shared by module.c and * asyncio.c. Keep the helpers static inline so they stay local to those users @@ -258,7 +249,6 @@ validate_fixed_field( #define PY_REMOTE_DEBUG_RUNTIME_STATE_FIELDS(APPLY, buffer_size) \ APPLY(runtime_state, interpreters_head, sizeof(uintptr_t), _Alignof(uintptr_t), buffer_size) -/* current_exception also covers the adjacent exc_info pointer. */ #define PY_REMOTE_DEBUG_THREAD_STATE_FIELDS(APPLY, buffer_size) \ APPLY(thread_state, native_thread_id, sizeof(unsigned long), _Alignof(long), buffer_size); \ APPLY(thread_state, interp, sizeof(uintptr_t), _Alignof(uintptr_t), buffer_size); \ @@ -266,7 +256,7 @@ validate_fixed_field( APPLY(thread_state, status, FIELD_SIZE(PyThreadState, _status), _Alignof(unsigned int), buffer_size); \ APPLY(thread_state, holds_gil, sizeof(int), _Alignof(int), buffer_size); \ APPLY(thread_state, gil_requested, sizeof(int), _Alignof(int), buffer_size); \ - APPLY(thread_state, current_exception, 2 * sizeof(uintptr_t), _Alignof(uintptr_t), buffer_size); \ + APPLY(thread_state, current_exception, sizeof(uintptr_t), _Alignof(uintptr_t), buffer_size); \ APPLY(thread_state, thread_id, sizeof(unsigned long), _Alignof(long), buffer_size); \ APPLY(thread_state, next, sizeof(uintptr_t), _Alignof(uintptr_t), buffer_size); \ APPLY(thread_state, current_frame, sizeof(uintptr_t), _Alignof(uintptr_t), buffer_size); \ @@ -361,11 +351,10 @@ _PyRemoteDebug_ValidateDebugOffsetsLayout(struct _Py_DebugOffsets *debug_offsets PY_REMOTE_DEBUG_THREAD_STATE_FIELDS( PY_REMOTE_DEBUG_VALIDATE_FIELD, SIZEOF_THREAD_STATE); - /* exc_value also covers the adjacent previous_item pointer. */ PY_REMOTE_DEBUG_VALIDATE_FIXED_FIELD( err_stackitem, exc_value, - 2 * sizeof(uintptr_t), + sizeof(uintptr_t), _Alignof(uintptr_t), sizeof(_PyErr_StackItem)); PY_REMOTE_DEBUG_VALIDATE_NESTED_FIELD( diff --git a/Modules/_remote_debugging/frames.c b/Modules/_remote_debugging/frames.c index fde3474a0539190..46968acc6ff1feb 100644 --- a/Modules/_remote_debugging/frames.c +++ b/Modules/_remote_debugging/frames.c @@ -305,7 +305,9 @@ process_frame_chain( uintptr_t frame_addr = ctx->frame_addr; uintptr_t prev_frame_addr = 0; uintptr_t last_frame_addr = 0; + const size_t MAX_FRAMES = 1024 + 512; size_t frame_count = 0; + assert(MAX_FRAMES > 0 && MAX_FRAMES < 10000); ctx->stopped_at_cached_frame = 0; ctx->last_frame_visited = 0; @@ -316,12 +318,12 @@ process_frame_chain( uintptr_t stackpointer = 0; last_frame_addr = frame_addr; - if (++frame_count > MAX_FRAME_CHAIN_DEPTH) { + if (++frame_count > MAX_FRAMES) { PyErr_SetString(PyExc_RuntimeError, "Too many stack frames (possible infinite loop)"); set_exception_cause(unwinder, PyExc_RuntimeError, "Frame chain iteration limit exceeded"); return -1; } - assert(frame_count <= MAX_FRAME_CHAIN_DEPTH); + assert(frame_count <= MAX_FRAMES); if (ctx->chunks && ctx->chunks->count > 0) { if (parse_frame_from_chunks(unwinder, &frame, frame_addr, &next_frame_addr, &stackpointer, ctx->chunks) == 0) { diff --git a/Modules/_remote_debugging/gc_stats.c b/Modules/_remote_debugging/gc_stats.c index 23fa879b5032837..d5d05edb8ecf5ee 100644 --- a/Modules/_remote_debugging/gc_stats.c +++ b/Modules/_remote_debugging/gc_stats.c @@ -103,42 +103,12 @@ get_gc_stats_from_interpreter_state(RuntimeOffsets *offsets, } struct gc_stats stats; - uintptr_t sequence_address = gc_stats_addr - + offsetof(struct gc_stats, update_seq); - /* A short GC update may finish before a second attempt. */ - for (int attempt = 0; attempt < 2; attempt++) { - uint32_t before; - if (_Py_RemoteDebug_ReadRemoteMemory(&offsets->handle, - sequence_address, - sizeof(before), &before) < 0) { - set_exception_cause(offsets, PyExc_RuntimeError, - "Failed to read GC update sequence"); - return -1; - } - if (_Py_RemoteDebug_ReadRemoteMemory(&offsets->handle, - gc_stats_addr, - sizeof(stats), - &stats) < 0) { - set_exception_cause(offsets, PyExc_RuntimeError, "Failed to read GC state"); - return -1; - } - - uint32_t after; - if (_Py_RemoteDebug_ReadRemoteMemory(&offsets->handle, - sequence_address, - sizeof(after), &after) < 0) { - set_exception_cause(offsets, PyExc_RuntimeError, - "Failed to read GC update sequence"); - return -1; - } - if (before == after && before == stats.update_seq && !(after & 1)) { - break; - } - if (attempt == 1) { - PyErr_SetString(PyExc_RuntimeError, - "GC stats changed while being read; retry later"); - return -1; - } + if (_Py_RemoteDebug_ReadRemoteMemory(&offsets->handle, + gc_stats_addr, + sizeof(stats), + &stats) < 0) { + set_exception_cause(offsets, PyExc_RuntimeError, "Failed to read GC state"); + return -1; } if (read_gc_stats(&stats, iid, ctx->result, diff --git a/Modules/_remote_debugging/module.c b/Modules/_remote_debugging/module.c index 03b398eb248e8fd..5dff0f844c8bd6a 100644 --- a/Modules/_remote_debugging/module.c +++ b/Modules/_remote_debugging/module.c @@ -1789,15 +1789,7 @@ _remote_debugging_BinaryWriter_write_sample_impl(BinaryWriterObject *self, return NULL; } - if (self->writer->state == BINARY_WRITER_BROKEN) { - PyErr_SetString(PyExc_ValueError, "Writer is broken"); - return NULL; - } - self->writer->state = BINARY_WRITER_OPEN; if (binary_writer_write_sample(self->writer, stack_frames, timestamp_us) < 0) { - if (self->writer->state != BINARY_WRITER_LIMIT_REACHED) { - self->writer->state = BINARY_WRITER_BROKEN; - } return NULL; } @@ -1860,12 +1852,7 @@ _remote_debugging_BinaryWriter_set_stats_impl(BinaryWriterObject *self, static int binary_writer_finalize_and_cache(BinaryWriterObject *self) { - if (self->writer->state == BINARY_WRITER_BROKEN) { - PyErr_SetString(PyExc_ValueError, "Writer is broken"); - return -1; - } if (binary_writer_finalize(self->writer) < 0) { - self->writer->state = BINARY_WRITER_BROKEN; return -1; } self->cached_total_samples = self->writer->total_samples; @@ -1946,7 +1933,8 @@ _remote_debugging_BinaryWriter___exit___impl(BinaryWriterObject *self, /*[clinic end generated code: output=61831f47c72a53c6 input=12334ce1009af37f]*/ { if (self->writer) { - if (self->writer->state != BINARY_WRITER_BROKEN) { + /* Only finalize on normal exit (no exception) */ + if (exc_type == Py_None) { if (binary_writer_finalize_and_cache(self) < 0) { if (self->writer) { binary_writer_destroy(self->writer); @@ -1995,17 +1983,8 @@ BinaryWriter_get_total_samples(PyObject *op, void *closure) return PyLong_FromUnsignedLongLong(self->writer->total_samples); } -static PyObject * -BinaryWriter_get_limit_reached(PyObject *op, void *closure) -{ - BinaryWriter *writer = BinaryWriter_CAST(op)->writer; - return PyBool_FromLong(writer && writer->state == BINARY_WRITER_LIMIT_REACHED); -} - static PyGetSetDef BinaryWriter_getset[] = { {"total_samples", BinaryWriter_get_total_samples, NULL, "Total samples written", NULL}, - {"limit_reached", BinaryWriter_get_limit_reached, NULL, - "A format limit was reached; the collected samples can still be finalized", NULL}, {NULL} }; diff --git a/Modules/_remote_debugging/object_reading.c b/Modules/_remote_debugging/object_reading.c index 743970db4ddc88a..56d9f80a80fd0f5 100644 --- a/Modules/_remote_debugging/object_reading.c +++ b/Modules/_remote_debugging/object_reading.c @@ -25,6 +25,7 @@ read_##type_name(RemoteUnwinderObject *unwinder, uintptr_t address, c_type *resu } DEFINE_MEMORY_READER(ptr, uintptr_t, "Failed to read pointer from remote memory") +DEFINE_MEMORY_READER(Py_ssize_t, Py_ssize_t, "Failed to read Py_ssize_t from remote memory") DEFINE_MEMORY_READER(char, char, "Failed to read char from remote memory") int diff --git a/Modules/_remote_debugging/threads.c b/Modules/_remote_debugging/threads.c index 198134fe6cfbea7..04c70cc96d6bd1e 100644 --- a/Modules/_remote_debugging/threads.c +++ b/Modules/_remote_debugging/threads.c @@ -17,9 +17,6 @@ #include #endif -/* Bound traversal of corrupted remote exception chains. */ -#define MAX_EXCEPTION_CHAIN_DEPTH (2 << 15) - /* ============================================================================ * THREAD ITERATION FUNCTIONS * ============================================================================ */ @@ -439,49 +436,16 @@ unwind_stack_for_thread( has_exception = 1; } - // Generators and coroutines use their own exception stack items. - // Follow exc_info to find the innermost handler, as sys.exception() does. + // Check exc_state.exc_value (exception being handled in except block) + // exc_state is embedded in PyThreadState, so we read it directly from + // the thread state buffer. This catches most cases; nested exception + // handlers where exc_info points elsewhere are rare. if (!has_exception) { - uintptr_t exc_info = GET_MEMBER(uintptr_t, ts, - unwinder->debug_offsets.thread_state.current_exception + - sizeof(uintptr_t)); - uintptr_t exc_state_addr = - *current_tstate + unwinder->debug_offsets.thread_state.exc_state; - uintptr_t exc_value_offset = - unwinder->debug_offsets.err_stackitem.exc_value; - uintptr_t previous_item_offset = - exc_value_offset + sizeof(uintptr_t); - - for (int depth = 0; exc_info != 0 && depth < MAX_EXCEPTION_CHAIN_DEPTH; - depth++) - { - if (exc_info == exc_state_addr) { - // Bottom of the chain: the stack item embedded in the thread - // state, which is already in the local thread state buffer. - uintptr_t exc_value = GET_MEMBER(uintptr_t, ts, - unwinder->debug_offsets.thread_state.exc_state + - exc_value_offset); - if (exc_value != 0) { - has_exception = 1; - } - break; - } - uintptr_t exc_value = 0; - if (read_ptr(unwinder, exc_info + exc_value_offset, &exc_value) < 0) { - PyErr_Clear(); // Best effort: treat as no active exception - break; - } - if (exc_value != 0) { - has_exception = 1; - break; - } - uintptr_t previous_item = 0; - if (read_ptr(unwinder, exc_info + previous_item_offset, - &previous_item) < 0) { - PyErr_Clear(); - break; - } - exc_info = previous_item; + uintptr_t exc_value = GET_MEMBER(uintptr_t, ts, + unwinder->debug_offsets.thread_state.exc_state + + unwinder->debug_offsets.err_stackitem.exc_value); + if (exc_value != 0) { + has_exception = 1; } } @@ -864,89 +828,6 @@ _Py_RemoteDebug_ResumeAllThreads(RemoteUnwinderObject *unwinder, _Py_RemoteDebug #elif defined(MS_WINDOWS) -static int -wait_for_threads_to_stop(RemoteUnwinderObject *unwinder) -{ - typedef NTSTATUS (NTAPI *NtGetNextThreadFunc)( - HANDLE, HANDLE, ACCESS_MASK, ULONG, ULONG, PHANDLE); - static NtGetNextThreadFunc pNtGetNextThread = NULL; - static int tried_load = 0; - - if (!tried_load) { - HMODULE hNtdll = GetModuleHandleW(L"ntdll.dll"); - if (hNtdll) { - pNtGetNextThread = (NtGetNextThreadFunc)GetProcAddress( - hNtdll, "NtGetNextThread"); - } - tried_load = 1; - } - if (pNtGetNextThread == NULL) { - PyErr_SetString(PyExc_RuntimeError, "NtGetNextThread not available"); - return -1; - } - - HANDLE previous = NULL; - int result = -1; - for (;;) { - HANDLE next = NULL; - // Enumerate with the available access, then obtain context access - // separately so threads that deny it are not silently skipped. - NTSTATUS status = pNtGetNextThread(unwinder->handle.hProcess, - previous, MAXIMUM_ALLOWED, 0, 0, &next); - if (previous != NULL) { - CloseHandle(previous); - } - previous = next; - if (status == STATUS_NO_MORE_ENTRIES) { - break; - } - if (status < 0) { - if (!is_process_alive(unwinder->handle.hProcess)) { - PyErr_Format(PyExc_ProcessLookupError, - "Process %d has terminated", unwinder->handle.pid); - } - else { - PyErr_Format(PyExc_RuntimeError, - "NtGetNextThread failed: 0x%lx", status); - } - goto done; - } - HANDLE thread; - if (!DuplicateHandle(GetCurrentProcess(), next, GetCurrentProcess(), - &thread, THREAD_GET_CONTEXT | SYNCHRONIZE, - FALSE, 0)) { - PyErr_SetFromWindowsErr(GetLastError()); - goto done; - } - // Suspension is asynchronous. Reading the context waits for the - // thread to stop before we start reading the target's memory. - CONTEXT context = {.ContextFlags = CONTEXT_CONTROL}; - if (!GetThreadContext(thread, &context)) { - DWORD error = GetLastError(); - int exited = WaitForSingleObject(thread, 0) == WAIT_OBJECT_0; - CloseHandle(thread); - if (exited) { - continue; - } - PyErr_SetFromWindowsErr(error); - goto done; - } - CloseHandle(thread); - } - if (!is_process_alive(unwinder->handle.hProcess)) { - PyErr_Format(PyExc_ProcessLookupError, - "Process %d has terminated", unwinder->handle.pid); - goto done; - } - result = 0; - -done: - if (previous != NULL) { - CloseHandle(previous); - } - return result; -} - void _Py_RemoteDebug_InitThreadsState(RemoteUnwinderObject *unwinder, _Py_RemoteDebug_ThreadsState *st) { @@ -977,12 +858,6 @@ _Py_RemoteDebug_StopAllThreads(RemoteUnwinderObject *unwinder, _Py_RemoteDebug_T if (status >= 0) { st->hProcess = unwinder->handle.hProcess; st->suspended = 1; - if (wait_for_threads_to_stop(unwinder) < 0) { - // pause_threads() has not yet set threads_stopped, so its caller - // will not resume the process when we return an error. - _Py_RemoteDebug_ResumeAllThreads(unwinder, st); - return -1; - } _Py_RemoteDebug_ClearCache(&unwinder->handle); return 0; } diff --git a/Modules/_testinternalcapi.c b/Modules/_testinternalcapi.c index 02c3d4433b79e99..124e06e5302f45e 100644 --- a/Modules/_testinternalcapi.c +++ b/Modules/_testinternalcapi.c @@ -438,7 +438,7 @@ next_frame_pointer_is_valid(uintptr_t *frame_pointer, uintptr_t *next_fp, #endif } -static PyObject * _Py_NO_SANITIZE_MEMORY +static PyObject * manual_unwind_from_fp(uintptr_t *frame_pointer) { uintptr_t stack_min = 0; @@ -2049,8 +2049,8 @@ check_pyobject_forbidden_bytes_is_freed(PyObject *self, static PyObject * check_pyobject_freed_is_freed(PyObject *self, PyObject *Py_UNUSED(args)) { - /* ASan, MSan or TSan would report an error. */ -#if defined(_Py_ADDRESS_SANITIZER) || defined(_Py_THREAD_SANITIZER) || defined(_Py_MEMORY_SANITIZER) + /* ASan or TSan would report an use-after-free error */ +#if defined(_Py_ADDRESS_SANITIZER) || defined(_Py_THREAD_SANITIZER) Py_RETURN_NONE; #else PyObject *op = PyObject_CallNoArgs((PyObject *)&PyBaseObject_Type); diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index b30ec5789643c8a..0b42b059541d351 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -10137,7 +10137,6 @@ os_getlogin_impl(PyObject *module) errno = old_errno; } else { - _Py_MSAN_UNPOISON(name, sizeof(name)); result = PyUnicode_DecodeFSDefault(name); } #else diff --git a/Modules/socketmodule.c b/Modules/socketmodule.c index 53d380eb4626c57..fc870aaa5c1c2e4 100644 --- a/Modules/socketmodule.c +++ b/Modules/socketmodule.c @@ -754,9 +754,7 @@ set_herror(socket_state *state, int h_error) PyObject *v; #ifdef HAVE_HSTRERROR - const char *errmsg = hstrerror(h_error); - _Py_MSAN_UNPOISON_STRING(errmsg); - v = Py_BuildValue("(iN)", h_error, decode_error_message(errmsg)); + v = Py_BuildValue("(iN)", h_error, decode_error_message(hstrerror(h_error))); #else v = Py_BuildValue("(is)", h_error, "host not found"); #endif @@ -783,9 +781,7 @@ set_gaierror(socket_state *state, int error) #endif #ifdef HAVE_GAI_STRERROR - const char *errmsg = gai_strerror(error); - _Py_MSAN_UNPOISON_STRING(errmsg); - v = Py_BuildValue("(iN)", error, decode_error_message(errmsg)); + v = Py_BuildValue("(iN)", error, decode_error_message(gai_strerror(error))); #else v = Py_BuildValue("(is)", error, "getaddrinfo failed"); #endif @@ -6424,7 +6420,6 @@ socket_getservbyport(PyObject *self, PyObject *args) PyErr_SetString(PyExc_OSError, "port/proto not found"); return NULL; } - _Py_MSAN_UNPOISON_STRING(sp->s_name); return PyUnicode_FromString(sp->s_name); } diff --git a/Objects/bytesobject.c b/Objects/bytesobject.c index 5ed28c18bb91fde..a537f084129eea5 100644 --- a/Objects/bytesobject.c +++ b/Objects/bytesobject.c @@ -2691,10 +2691,9 @@ _PyBytes_FromHex(PyObject *string, int use_bytearray) if (Py_ISSPACE(*str)) { do { str++; - if (str >= end) { - goto done; - } } while (Py_ISSPACE(*str)); + if (str >= end) + break; } top = _PyLong_DigitValue[*str]; @@ -2702,16 +2701,16 @@ _PyBytes_FromHex(PyObject *string, int use_bytearray) invalid_char = str - start; goto error; } - str++; - if (str >= end) { - invalid_char = -1; - goto error; - } bot = _PyLong_DigitValue[*str]; if (bot >= 16) { - invalid_char = str - start; + /* Check if we had a second digit */ + if (str >= end){ + invalid_char = -1; + } else { + invalid_char = str - start; + } goto error; } str++; @@ -2719,7 +2718,6 @@ _PyBytes_FromHex(PyObject *string, int use_bytearray) *buf++ = (unsigned char)((top << 4) + bot); } - done: if (view.obj != NULL) { PyBuffer_Release(&view); } diff --git a/Python/gc.c b/Python/gc.c index bb20dae5a6543fa..201c621bcc3cb9b 100644 --- a/Python/gc.c +++ b/Python/gc.c @@ -1399,13 +1399,6 @@ gc_get_prev_stats(GCState *gcstate, int gen) static void add_stats(GCState *gcstate, int gen, struct gc_generation_stats *stats) { - struct gc_stats *generation_stats = gcstate->generation_stats; - uint32_t seq = _Py_atomic_load_uint32_relaxed(&generation_stats->update_seq); - assert((seq & 1) == 0); - /* Odd seq tells the reader that an update is in progress. */ - _Py_atomic_store_uint32_relaxed(&generation_stats->update_seq, seq + 1); - _Py_atomic_fence_seq_cst(); - struct gc_generation_stats *prev_stats = gc_get_prev_stats(gcstate, gen); struct gc_generation_stats *cur_stats = gc_get_stats(gcstate, gen); @@ -1419,8 +1412,9 @@ add_stats(GCState *gcstate, int gen, struct gc_generation_stats *stats) cur_stats->duration += stats->duration; cur_stats->heap_size = stats->heap_size; + /* Publish ts_stop last so remote readers do not select a partially + updated stats record as the latest collection. */ cur_stats->ts_stop = stats->ts_stop; - _Py_atomic_store_uint32_release(&generation_stats->update_seq, seq + 2); } /* This is the main function. Read this to understand how the diff --git a/Python/gc_free_threading.c b/Python/gc_free_threading.c index f408f239ab16931..8e27649bfd69417 100644 --- a/Python/gc_free_threading.c +++ b/Python/gc_free_threading.c @@ -2282,12 +2282,6 @@ gc_collect_main(PyThreadState *tstate, int generation, _PyGC_Reason reason) } /* Update stats */ - struct gc_stats *generation_stats = gcstate->generation_stats; - uint32_t seq = _Py_atomic_load_uint32_relaxed(&generation_stats->update_seq); - assert((seq & 1) == 0); - /* Odd seq tells the reader that an update is in progress. */ - _Py_atomic_store_uint32_relaxed(&generation_stats->update_seq, seq + 1); - _Py_atomic_fence_seq_cst(); struct gc_generation_stats *stats = get_stats(gcstate, generation); stats->ts_start = start; stats->ts_stop = stop; @@ -2296,7 +2290,6 @@ gc_collect_main(PyThreadState *tstate, int generation, _PyGC_Reason reason) stats->uncollectable += n; stats->duration += duration; stats->candidates += state.candidates; - _Py_atomic_store_uint32_release(&generation_stats->update_seq, seq + 2); GC_STAT_ADD(generation, objects_collected, m); #ifdef Py_STATS diff --git a/Python/instrumentation.c b/Python/instrumentation.c index 88e23b59db1ce6a..646fc15c6872e56 100644 --- a/Python/instrumentation.c +++ b/Python/instrumentation.c @@ -1690,7 +1690,6 @@ allocate_instrumentation_data(PyCodeObject *code) } monitoring->local_monitors = (_Py_LocalMonitors){ 0 }; monitoring->active_monitors = (_Py_LocalMonitors){ 0 }; - memset(monitoring->tool_versions, 0, sizeof(monitoring->tool_versions)); monitoring->tools = NULL; monitoring->lines = NULL; monitoring->line_tools = NULL; diff --git a/Python/remote_debug.h b/Python/remote_debug.h index 30cad806a6f210b..16229df936ffef6 100644 --- a/Python/remote_debug.h +++ b/Python/remote_debug.h @@ -107,18 +107,9 @@ _Py_RemoteDebug_HasPermissionError(void) && PyErr_ExceptionMatches(PyExc_PermissionError); } -static inline int -_Py_RemoteDebug_IsFatalReadError(void) -{ - return _Py_RemoteDebug_HasPermissionError() - || PyErr_ExceptionMatches(PyExc_MemoryError) - || PyErr_ExceptionMatches(PyExc_ProcessLookupError) - || (PyErr_Occurred() && !PyErr_ExceptionMatches(PyExc_Exception)); -} - #define _set_debug_exception_cause(exception, format, ...) \ do { \ - if (!_Py_RemoteDebug_IsFatalReadError()) { \ + if (!_Py_RemoteDebug_HasPermissionError()) { \ PyThreadState *tstate = _PyThreadState_GET(); \ if (!_PyErr_Occurred(tstate)) { \ _PyErr_Format(tstate, exception, format, ##__VA_ARGS__); \ diff --git a/configure b/configure index 3e08f8af8650b58..a68a98c5a4de4a0 100755 --- a/configure +++ b/configure @@ -16392,7 +16392,7 @@ int main(void) { return 2; } - ffi_arg rc = 0; + ffi_arg rc; ffi_call(&cif, FFI_FN(z_is_expected), &rc, values); return !rc; } diff --git a/configure.ac b/configure.ac index f5365b0ae194d0a..ed8b53dd3a4d282 100644 --- a/configure.ac +++ b/configure.ac @@ -4360,7 +4360,7 @@ int main(void) { return 2; } - ffi_arg rc = 0; + ffi_arg rc; ffi_call(&cif, FFI_FN(z_is_expected), &rc, values); return !rc; }