Skip to content

Commit 53776ad

Browse files
committed
Revert "[3.15] gh-151292: _remote_debugging: Do not corrupt the binary file when hitting OverflowError (GH-152892) (#158830)"
This reverts commit 8a7c23f.
1 parent 30df394 commit 53776ad

6 files changed

Lines changed: 14 additions & 213 deletions

File tree

‎Lib/profiling/sampling/binary_collector.py‎

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
"""Thin Python wrapper around C binary writer for profiling data."""
22

3-
import sys
43
import time
54

65
import _remote_debugging
@@ -82,7 +81,6 @@ def __init__(self, filename, sample_interval_usec, *, skip_idle=False,
8281
self.filename = filename
8382
self.sample_interval_usec = sample_interval_usec
8483
self.skip_idle = skip_idle
85-
self.running = True
8684

8785
compression_type = _resolve_compression(compression)
8886
start_time_us = int(time.monotonic() * 1_000_000)
@@ -104,19 +102,9 @@ def collect(self, stack_frames, timestamp_us=None):
104102
timestamp_us: Optional timestamp in microseconds. If not provided,
105103
uses time.monotonic() to generate one.
106104
"""
107-
if not self.running:
108-
return
109105
if timestamp_us is None:
110106
timestamp_us = int(time.monotonic() * 1_000_000)
111-
try:
112-
self._writer.write_sample(stack_frames, timestamp_us)
113-
except OverflowError as e:
114-
if not self._writer.limit_reached:
115-
raise
116-
self.running = False
117-
print(f"Warning: {e}; stopping early and keeping the data "
118-
"collected so far.",
119-
file=sys.stderr)
107+
self._writer.write_sample(stack_frames, timestamp_us)
120108

121109
def collect_failed_sample(self):
122110
"""Record a failed sample attempt (no-op for binary format)."""
@@ -155,5 +143,9 @@ def __enter__(self):
155143
return self
156144

157145
def __exit__(self, exc_type, exc_val, exc_tb):
158-
"""Finalize if the writer can still produce a valid file."""
159-
return self._writer.__exit__(exc_type, exc_val, exc_tb)
146+
"""Context manager exit - finalize unless there was an error."""
147+
if exc_type is None:
148+
self._writer.finalize()
149+
else:
150+
self._writer.close()
151+
return False

‎Lib/test/test_profiling/test_sampling_profiler/test_binary_format.py‎

Lines changed: 0 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@
99
import unittest
1010
from collections import defaultdict
1111

12-
from test.support import captured_stderr
13-
1412
try:
1513
import _remote_debugging
1614
from _remote_debugging import (
@@ -1033,142 +1031,6 @@ def test_writer_total_samples_after_close_returns_zero(self):
10331031
w.close()
10341032
self.assertEqual(w.total_samples, 0)
10351033

1036-
def test_binary_collector_stops_gracefully_on_overflow(self):
1037-
"""OverflowError from the writer stops collection via the running
1038-
protocol instead of propagating and corrupting the file.
1039-
See gh-151292."""
1040-
with tempfile.NamedTemporaryFile(suffix=".bin", delete=False) as f:
1041-
filename = f.name
1042-
self.temp_files.append(filename)
1043-
1044-
collector = BinaryCollector(filename, 1000, compression="none")
1045-
self.assertTrue(collector.running)
1046-
1047-
sample = [
1048-
make_interpreter(0, [make_thread(1, [make_frame("a.py", 1, "f")])])
1049-
]
1050-
1051-
# Collect real samples first, then hit the limit.
1052-
for i in range(3):
1053-
collector.collect(sample, timestamp_us=(i + 1) * 1000)
1054-
self.assertTrue(collector.running)
1055-
1056-
bad = [make_interpreter(2**32, sample[0].threads)]
1057-
with captured_stderr() as stderr:
1058-
collector.collect(bad, timestamp_us=4000)
1059-
collector.collect(sample, timestamp_us=5000)
1060-
1061-
self.assertFalse(collector.running)
1062-
self.assertTrue(collector._writer.limit_reached)
1063-
self.assertEqual(stderr.getvalue().count("Warning:"), 1)
1064-
self.assertIn("interpreter_id", stderr.getvalue())
1065-
1066-
collector.export(None)
1067-
1068-
self.assertEqual(collector.total_samples, 3)
1069-
1070-
reader_collector = RawCollector()
1071-
with BinaryReader(filename) as reader:
1072-
self.assertEqual(reader.replay_samples(reader_collector), 3)
1073-
1074-
def test_interpreter_id_overflow_rejected(self):
1075-
"""An interpreter_id wider than u32 raises OverflowError before any
1076-
writer state is mutated: subsequent valid samples are still accepted
1077-
and finalize produces a readable file."""
1078-
with tempfile.NamedTemporaryFile(suffix=".bin", delete=False) as f:
1079-
filename = f.name
1080-
self.temp_files.append(filename)
1081-
1082-
good = [
1083-
make_interpreter(0, [make_thread(1, [make_frame("a.py", 1, "f")])])
1084-
]
1085-
bad = [
1086-
make_interpreter(2**32, [make_thread(1, [make_frame("a.py", 1, "f")])])
1087-
]
1088-
1089-
writer = _remote_debugging.BinaryWriter(filename, 1000, 0, compression=0)
1090-
writer.write_sample(good, 1000)
1091-
with self.assertRaises(OverflowError):
1092-
writer.write_sample(bad, 2000)
1093-
writer.write_sample(good, 3000)
1094-
writer.finalize()
1095-
self.assertEqual(writer.total_samples, 2)
1096-
1097-
reader_collector = RawCollector()
1098-
with BinaryReader(filename) as reader:
1099-
self.assertEqual(reader.replay_samples(reader_collector), 2)
1100-
1101-
def test_writer_finalizes_after_format_limit(self):
1102-
for compression in (0, 1) if ZSTD_AVAILABLE else (0,):
1103-
with self.subTest(compression=compression):
1104-
with tempfile.NamedTemporaryFile(suffix=".bin", delete=False) as f:
1105-
filename = f.name
1106-
self.temp_files.append(filename)
1107-
good = [make_interpreter(0, [
1108-
make_thread(1, [make_frame("a.py", 1, "f")])
1109-
])]
1110-
bad = [make_interpreter(2**32, good[0].threads)]
1111-
writer = _remote_debugging.BinaryWriter(
1112-
filename, 1000, 0, compression=compression
1113-
)
1114-
with self.assertRaises(OverflowError):
1115-
with writer:
1116-
writer.write_sample(good, 1000)
1117-
writer.write_sample(good, 2000)
1118-
# The first interpreter is committed before the limit.
1119-
writer.write_sample(good + bad, 3000)
1120-
self.assertEqual(writer.total_samples, 3)
1121-
with BinaryReader(filename) as reader:
1122-
self.assertEqual(reader.replay_samples(RawCollector()), 3)
1123-
1124-
def test_collector_does_not_swallow_unrelated_overflow(self):
1125-
class BadStatus:
1126-
def __index__(self):
1127-
raise OverflowError("status conversion failed")
1128-
1129-
with tempfile.NamedTemporaryFile(suffix=".bin", delete=False) as f:
1130-
filename = f.name
1131-
self.temp_files.append(filename)
1132-
collector = BinaryCollector(filename, 1000, compression="none")
1133-
self.addCleanup(collector._writer.close)
1134-
sample = [make_interpreter(0, [make_thread(1, [], BadStatus())])]
1135-
with captured_stderr() as stderr:
1136-
with self.assertRaisesRegex(OverflowError, "status conversion failed"):
1137-
collector.collect(sample, timestamp_us=1000)
1138-
self.assertEqual(stderr.getvalue(), "")
1139-
self.assertFalse(collector._writer.limit_reached)
1140-
with self.assertRaisesRegex(ValueError, "broken"):
1141-
collector.export()
1142-
with self.assertRaisesRegex(ValueError, "broken"):
1143-
collector._writer.write_sample([], 2000)
1144-
# Closing a broken writer must not attempt to finalize it.
1145-
collector.__exit__(None, None, None)
1146-
1147-
def test_collector_finalizes_after_external_exception(self):
1148-
with tempfile.NamedTemporaryFile(suffix=".bin", delete=False) as f:
1149-
filename = f.name
1150-
self.temp_files.append(filename)
1151-
with self.assertRaisesRegex(RuntimeError, "sampling failed"):
1152-
with BinaryCollector(filename, 1000, compression="none") as collector:
1153-
collector.collect([make_interpreter(0, [make_thread(1, [])])])
1154-
raise RuntimeError("sampling failed")
1155-
self.assertEqual(collector.total_samples, 1)
1156-
with BinaryReader(filename) as reader:
1157-
self.assertEqual(reader.replay_samples(RawCollector()), 1)
1158-
1159-
@unittest.skipUnless(os.path.exists("/dev/full"), "requires /dev/full")
1160-
def test_finalize_failure_breaks_writer(self):
1161-
writer = _remote_debugging.BinaryWriter("/dev/full", 1000, 0)
1162-
self.addCleanup(writer.close)
1163-
writer.write_sample([make_interpreter(0, [make_thread(1, [])])], 1000)
1164-
with self.assertRaises(OSError):
1165-
writer.finalize()
1166-
self.assertFalse(writer.limit_reached)
1167-
with self.assertRaisesRegex(ValueError, "broken"):
1168-
writer.finalize()
1169-
with self.assertRaisesRegex(ValueError, "broken"):
1170-
writer.write_sample([], 2000)
1171-
11721034

11731035
class TestBinaryFormatValidation(BinaryFormatTestBase):
11741036
"""Tests for malformed binary files."""

‎Misc/NEWS.d/next/Library/2026-07-02-15-26-51.gh-issue-151292.nmnQlp.rst‎

Lines changed: 0 additions & 3 deletions
This file was deleted.

‎Modules/_remote_debugging/binary_io.h‎

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -290,18 +290,9 @@ typedef struct {
290290
size_t pending_rle_samples;
291291
} ThreadEntry;
292292

293-
/* Limit errors occur before emitting an incomplete sample. Other write
294-
* failures may leave partial records and must prevent finalization. */
295-
typedef enum {
296-
BINARY_WRITER_OPEN,
297-
BINARY_WRITER_LIMIT_REACHED,
298-
BINARY_WRITER_BROKEN,
299-
} BinaryWriterState;
300-
301293
/* Main binary writer structure */
302294
typedef struct {
303295
FILE *fp;
304-
BinaryWriterState state;
305296

306297
/* Write buffer for batched I/O */
307298
uint8_t *write_buffer;

‎Modules/_remote_debugging/binary_io_writer.c‎

Lines changed: 5 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,6 @@ writer_intern_string(BinaryWriter *writer, PyObject *string, uint32_t *index)
371371
}
372372

373373
if (writer->string_count >= UINT32_MAX) {
374-
writer->state = BINARY_WRITER_LIMIT_REACHED;
375374
PyErr_SetString(PyExc_OverflowError,
376375
"too many strings for binary format");
377376
return -1;
@@ -381,9 +380,6 @@ writer_intern_string(BinaryWriter *writer, PyObject *string, uint32_t *index)
381380
(void **)&writer->string_lengths,
382381
&writer->string_capacity,
383382
sizeof(char *), sizeof(size_t)) < 0) {
384-
if (PyErr_ExceptionMatches(PyExc_OverflowError)) {
385-
writer->state = BINARY_WRITER_LIMIT_REACHED;
386-
}
387383
return -1;
388384
}
389385
}
@@ -394,7 +390,6 @@ writer_intern_string(BinaryWriter *writer, PyObject *string, uint32_t *index)
394390
return -1;
395391
}
396392
if ((uintmax_t)str_len > UINT32_MAX) {
397-
writer->state = BINARY_WRITER_LIMIT_REACHED;
398393
PyErr_Format(PyExc_OverflowError,
399394
"string length %zd exceeds binary format maximum %u",
400395
str_len, UINT32_MAX);
@@ -443,16 +438,12 @@ writer_intern_frame(BinaryWriter *writer, const FrameEntry *entry, uint32_t *ind
443438
}
444439

445440
if (writer->frame_count >= UINT32_MAX) {
446-
writer->state = BINARY_WRITER_LIMIT_REACHED;
447441
PyErr_SetString(PyExc_OverflowError,
448442
"too many frames for binary format");
449443
return -1;
450444
}
451445
if (GROW_ARRAY(writer->frame_entries, writer->frame_count,
452446
writer->frame_capacity, FrameEntry) < 0) {
453-
if (PyErr_ExceptionMatches(PyExc_OverflowError)) {
454-
writer->state = BINARY_WRITER_LIMIT_REACHED;
455-
}
456447
return -1;
457448
}
458449

@@ -496,7 +487,6 @@ writer_get_or_create_thread_entry(BinaryWriter *writer, uint64_t thread_id,
496487
}
497488

498489
if (writer->thread_count >= UINT32_MAX) {
499-
writer->state = BINARY_WRITER_LIMIT_REACHED;
500490
PyErr_SetString(PyExc_OverflowError,
501491
"too many threads for binary format");
502492
return NULL;
@@ -506,9 +496,6 @@ writer_get_or_create_thread_entry(BinaryWriter *writer, uint64_t thread_id,
506496
&writer->thread_capacity,
507497
sizeof(ThreadEntry));
508498
if (!new_entries) {
509-
if (PyErr_ExceptionMatches(PyExc_OverflowError)) {
510-
writer->state = BINARY_WRITER_LIMIT_REACHED;
511-
}
512499
return NULL;
513500
}
514501
writer->thread_entries = new_entries;
@@ -941,12 +928,6 @@ static int
941928
process_thread_sample(BinaryWriter *writer, PyObject *thread_info,
942929
uint32_t interpreter_id, uint64_t timestamp_us)
943930
{
944-
if (writer->total_samples == UINT64_MAX) {
945-
writer->state = BINARY_WRITER_LIMIT_REACHED;
946-
PyErr_SetString(PyExc_OverflowError, "too many samples for binary format");
947-
return -1;
948-
}
949-
950931
PyObject *thread_id_obj = PyStructSequence_GET_ITEM(thread_info, 0);
951932
PyObject *status_obj = PyStructSequence_GET_ITEM(thread_info, 1);
952933
PyObject *frame_list = PyStructSequence_GET_ITEM(thread_info, 2);
@@ -969,6 +950,7 @@ process_thread_sample(BinaryWriter *writer, PyObject *thread_info,
969950

970951
/* Calculate timestamp delta */
971952
uint64_t delta = timestamp_us - entry->prev_timestamp;
953+
entry->prev_timestamp = timestamp_us;
972954

973955
/* Process frames and build current stack */
974956
uint32_t curr_stack[MAX_STACK_DEPTH];
@@ -1024,7 +1006,6 @@ process_thread_sample(BinaryWriter *writer, PyObject *thread_info,
10241006
entry->prev_stack_depth = curr_depth;
10251007
}
10261008

1027-
entry->prev_timestamp = timestamp_us;
10281009
writer->total_samples++;
10291010
return 0;
10301011
}
@@ -1044,16 +1025,15 @@ binary_writer_write_sample(BinaryWriter *writer, PyObject *stack_frames, uint64_
10441025
PyObject *interp_id_obj = PyStructSequence_GET_ITEM(interp_info, 0);
10451026
PyObject *threads = PyStructSequence_GET_ITEM(interp_info, 1);
10461027

1047-
unsigned long long interp_id_long = PyLong_AsUnsignedLongLong(interp_id_obj);
1048-
if (interp_id_long == (unsigned long long)-1 && PyErr_Occurred()) {
1028+
unsigned long interp_id_long = PyLong_AsUnsignedLong(interp_id_obj);
1029+
if (interp_id_long == (unsigned long)-1 && PyErr_Occurred()) {
10491030
return -1;
10501031
}
10511032
/* Bounds check: interpreter_id is stored as uint32_t in binary format */
10521033
if (interp_id_long > UINT32_MAX) {
1053-
writer->state = BINARY_WRITER_LIMIT_REACHED;
10541034
PyErr_Format(PyExc_OverflowError,
1055-
"interpreter_id %llu exceeds maximum value %u",
1056-
interp_id_long, UINT32_MAX);
1035+
"interpreter_id %lu exceeds maximum value %lu",
1036+
interp_id_long, (unsigned long)UINT32_MAX);
10571037
return -1;
10581038
}
10591039
uint32_t interpreter_id = (uint32_t)interp_id_long;

‎Modules/_remote_debugging/module.c‎

Lines changed: 2 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1789,15 +1789,7 @@ _remote_debugging_BinaryWriter_write_sample_impl(BinaryWriterObject *self,
17891789
return NULL;
17901790
}
17911791

1792-
if (self->writer->state == BINARY_WRITER_BROKEN) {
1793-
PyErr_SetString(PyExc_ValueError, "Writer is broken");
1794-
return NULL;
1795-
}
1796-
self->writer->state = BINARY_WRITER_OPEN;
17971792
if (binary_writer_write_sample(self->writer, stack_frames, timestamp_us) < 0) {
1798-
if (self->writer->state != BINARY_WRITER_LIMIT_REACHED) {
1799-
self->writer->state = BINARY_WRITER_BROKEN;
1800-
}
18011793
return NULL;
18021794
}
18031795

@@ -1860,12 +1852,7 @@ _remote_debugging_BinaryWriter_set_stats_impl(BinaryWriterObject *self,
18601852
static int
18611853
binary_writer_finalize_and_cache(BinaryWriterObject *self)
18621854
{
1863-
if (self->writer->state == BINARY_WRITER_BROKEN) {
1864-
PyErr_SetString(PyExc_ValueError, "Writer is broken");
1865-
return -1;
1866-
}
18671855
if (binary_writer_finalize(self->writer) < 0) {
1868-
self->writer->state = BINARY_WRITER_BROKEN;
18691856
return -1;
18701857
}
18711858
self->cached_total_samples = self->writer->total_samples;
@@ -1946,7 +1933,8 @@ _remote_debugging_BinaryWriter___exit___impl(BinaryWriterObject *self,
19461933
/*[clinic end generated code: output=61831f47c72a53c6 input=12334ce1009af37f]*/
19471934
{
19481935
if (self->writer) {
1949-
if (self->writer->state != BINARY_WRITER_BROKEN) {
1936+
/* Only finalize on normal exit (no exception) */
1937+
if (exc_type == Py_None) {
19501938
if (binary_writer_finalize_and_cache(self) < 0) {
19511939
if (self->writer) {
19521940
binary_writer_destroy(self->writer);
@@ -1995,17 +1983,8 @@ BinaryWriter_get_total_samples(PyObject *op, void *closure)
19951983
return PyLong_FromUnsignedLongLong(self->writer->total_samples);
19961984
}
19971985

1998-
static PyObject *
1999-
BinaryWriter_get_limit_reached(PyObject *op, void *closure)
2000-
{
2001-
BinaryWriter *writer = BinaryWriter_CAST(op)->writer;
2002-
return PyBool_FromLong(writer && writer->state == BINARY_WRITER_LIMIT_REACHED);
2003-
}
2004-
20051986
static PyGetSetDef BinaryWriter_getset[] = {
20061987
{"total_samples", BinaryWriter_get_total_samples, NULL, "Total samples written", NULL},
2007-
{"limit_reached", BinaryWriter_get_limit_reached, NULL,
2008-
"A format limit was reached; the collected samples can still be finalized", NULL},
20091988
{NULL}
20101989
};
20111990

0 commit comments

Comments
 (0)