From 64b1fd171b513c2b4cb2ea3119716ab887753254 Mon Sep 17 00:00:00 2001 From: maurycy <5383+maurycy@users.noreply.github.com> Date: Thu, 2 Jul 2026 14:59:59 +0200 Subject: [PATCH 1/4] the kolektor --- Lib/profiling/sampling/binary_collector.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Lib/profiling/sampling/binary_collector.py b/Lib/profiling/sampling/binary_collector.py index afbbc8292690678..cdbb76b878b1d17 100644 --- a/Lib/profiling/sampling/binary_collector.py +++ b/Lib/profiling/sampling/binary_collector.py @@ -1,5 +1,6 @@ """Thin Python wrapper around C binary writer for profiling data.""" +import sys import time import _remote_debugging @@ -61,6 +62,7 @@ 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) @@ -81,7 +83,13 @@ def collect(self, stack_frames, timestamp_us=None): """ if timestamp_us is None: timestamp_us = int(time.monotonic() * 1_000_000) - self._writer.write_sample(stack_frames, timestamp_us) + try: + self._writer.write_sample(stack_frames, timestamp_us) + except OverflowError as e: + self.running = False + print(f"Warning: {e}; stopping early and keeping the data " + "collected so far.", + file=sys.stderr) def collect_failed_sample(self): """Record a failed sample attempt (no-op for binary format).""" From 87114cfa8d4dca1b325161b6e33b7eb187684f35 Mon Sep 17 00:00:00 2001 From: maurycy <5383+maurycy@users.noreply.github.com> Date: Thu, 2 Jul 2026 15:04:37 +0200 Subject: [PATCH 2/4] test --- .../test_binary_format.py | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) 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 56dac2cf86a7647..433e9490c077494 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 @@ -9,6 +9,8 @@ import unittest from collections import defaultdict +from test.support import captured_stderr + try: import _remote_debugging from _remote_debugging import ( @@ -994,6 +996,74 @@ 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) + + real_writer = collector._writer + + class _OverflowingWriter: + def write_sample(self, stack_frames, timestamp_us): + raise OverflowError("too many samples for binary format") + + collector._writer = _OverflowingWriter() + with captured_stderr() as stderr: + collector.collect(sample, timestamp_us=4000) + + self.assertFalse(collector.running) + self.assertIn("too many samples", stderr.getvalue()) + + # The real writer can still be finalized into a valid file that + # keeps the samples collected before the limit was hit. + collector._writer = real_writer + collector.export(None) + 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) + class TestBinaryFormatValidation(BinaryFormatTestBase): """Tests for malformed binary files.""" From d24dfd69ff2f034b5eb208cc741467e8191a5539 Mon Sep 17 00:00:00 2001 From: maurycy <5383+maurycy@users.noreply.github.com> Date: Thu, 2 Jul 2026 15:21:18 +0200 Subject: [PATCH 3/4] better test --- .../test_sampling_profiler/test_binary_format.py | 9 +++++++++ 1 file changed, 9 insertions(+) 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 433e9490c077494..ae4a87b832fd79a 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 @@ -1033,6 +1033,15 @@ def write_sample(self, stack_frames, timestamp_us): # keeps the samples collected before the limit was hit. collector._writer = real_writer collector.export(None) + + with open(filename, "rb") as f: + header = f.read(32) + magic, version = struct.unpack_from("=II", header, 0) + self.assertEqual(magic, 0x54414348) # "TACH" + self.assertEqual(version, 1) + (sample_count,) = struct.unpack_from("=I", header, 28) + self.assertEqual(sample_count, 3) + reader_collector = RawCollector() with BinaryReader(filename) as reader: self.assertEqual(reader.replay_samples(reader_collector), 3) From f2008ac97c0dfa6bc8c85338e40eb44f7f7a7627 Mon Sep 17 00:00:00 2001 From: maurycy <5383+maurycy@users.noreply.github.com> Date: Thu, 2 Jul 2026 15:26:58 +0200 Subject: [PATCH 4/4] news --- .../Library/2026-07-02-15-26-51.gh-issue-151292.nmnQlp.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-07-02-15-26-51.gh-issue-151292.nmnQlp.rst 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 new file mode 100644 index 000000000000000..eb3bda128bb2561 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-02-15-26-51.gh-issue-151292.nmnQlp.rst @@ -0,0 +1,3 @@ +Fix ``profiling.sampling --binary`` leaving unreadable profile files when +the ``_remote_debugging`` binary writer raises :exc:`OverflowError`. Patch +by Maurycy Pawłowski-Wieroński.