Skip to content

Commit 5826743

Browse files
committed
Merge 3.15 to include the bytes.fromhex MSan prerequisite
2 parents 6e8ca50 + 4f7af46 commit 5826743

14 files changed

Lines changed: 289 additions & 30 deletions

File tree

‎Include/internal/pycore_interp_structs.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ struct gc_old_stats_buffer {
219219
struct gc_stats {
220220
struct gc_young_stats_buffer young;
221221
struct gc_old_stats_buffer old[2];
222+
uint32_t update_seq;
222223
};
223224

224225
struct _gc_runtime_state {

‎Lib/profiling/sampling/binary_collector.py‎

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

3+
import sys
34
import time
45

56
import _remote_debugging
@@ -81,6 +82,7 @@ def __init__(self, filename, sample_interval_usec, *, skip_idle=False,
8182
self.filename = filename
8283
self.sample_interval_usec = sample_interval_usec
8384
self.skip_idle = skip_idle
85+
self.running = True
8486

8587
compression_type = _resolve_compression(compression)
8688
start_time_us = int(time.monotonic() * 1_000_000)
@@ -102,9 +104,19 @@ def collect(self, stack_frames, timestamp_us=None):
102104
timestamp_us: Optional timestamp in microseconds. If not provided,
103105
uses time.monotonic() to generate one.
104106
"""
107+
if not self.running:
108+
return
105109
if timestamp_us is None:
106110
timestamp_us = int(time.monotonic() * 1_000_000)
107-
self._writer.write_sample(stack_frames, timestamp_us)
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)
108120

109121
def collect_failed_sample(self):
110122
"""Record a failed sample attempt (no-op for binary format)."""
@@ -143,9 +155,5 @@ def __enter__(self):
143155
return self
144156

145157
def __exit__(self, 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
158+
"""Finalize if the writer can still produce a valid file."""
159+
return self._writer.__exit__(exc_type, exc_val, exc_tb)

‎Lib/test/test_bytes.py‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,15 @@ def test_fromhex(self):
508508
self.type2test.fromhex(data)
509509
self.assertIn('at position %s' % pos, str(cm.exception))
510510

511+
# gh-158583: Check for out of bounds reads (uninitialized bytes).
512+
# Create an array from a list to not overallocate.
513+
a = array.array('B', list(b'1234 ')) # Py_ISSPACE() loop
514+
self.assertEqual(self.type2test.fromhex(a), b'\x12\x34')
515+
516+
a = array.array('B', list(b'12345')) # Missing second digit
517+
with self.assertRaises(ValueError):
518+
self.type2test.fromhex(a)
519+
511520
def test_hex(self):
512521
self.assertRaises(TypeError, self.type2test.hex)
513522
self.assertRaises(TypeError, self.type2test.hex, 1)

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

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

12+
from test.support import captured_stderr
13+
1214
try:
1315
import _remote_debugging
1416
from _remote_debugging import (
@@ -1031,6 +1033,142 @@ def test_writer_total_samples_after_close_returns_zero(self):
10311033
w.close()
10321034
self.assertEqual(w.total_samples, 0)
10331035

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+
10341172

10351173
class TestBinaryFormatValidation(BinaryFormatTestBase):
10361174
"""Tests for malformed binary files."""
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
:meth:`bytes.fromhex` and :meth:`bytearray.fromhex`: Fix uninitialized
2+
memory read. Patch by Victor Stinner.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix ``profiling.sampling --binary`` leaving unreadable profile files when
2+
the binary format reaches a size limit. Preserve collected samples when the
3+
writer can still finalize safely. Patch by Maurycy Pawłowski-Wieroński.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Add a sequence counter to GC statistics to prevent :mod:`!_remote_debugging`
2+
returning inconsistent snapshots caused by non-atomic reads. Patch by Maurycy
3+
Pawłowski-Wieroński.

‎Modules/_remote_debugging/binary_io.h‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,9 +290,18 @@ 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+
293301
/* Main binary writer structure */
294302
typedef struct {
295303
FILE *fp;
304+
BinaryWriterState state;
296305

297306
/* Write buffer for batched I/O */
298307
uint8_t *write_buffer;

‎Modules/_remote_debugging/binary_io_writer.c‎

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

378378
if (writer->string_count >= UINT32_MAX) {
379+
writer->state = BINARY_WRITER_LIMIT_REACHED;
379380
PyErr_SetString(PyExc_OverflowError,
380381
"too many strings for binary format");
381382
return -1;
@@ -385,6 +386,9 @@ writer_intern_string(BinaryWriter *writer, PyObject *string, uint32_t *index)
385386
(void **)&writer->string_lengths,
386387
&writer->string_capacity,
387388
sizeof(char *), sizeof(size_t)) < 0) {
389+
if (PyErr_ExceptionMatches(PyExc_OverflowError)) {
390+
writer->state = BINARY_WRITER_LIMIT_REACHED;
391+
}
388392
return -1;
389393
}
390394
}
@@ -395,6 +399,7 @@ writer_intern_string(BinaryWriter *writer, PyObject *string, uint32_t *index)
395399
return -1;
396400
}
397401
if ((uintmax_t)str_len > UINT32_MAX) {
402+
writer->state = BINARY_WRITER_LIMIT_REACHED;
398403
PyErr_Format(PyExc_OverflowError,
399404
"string length %zd exceeds binary format maximum %u",
400405
str_len, UINT32_MAX);
@@ -443,12 +448,16 @@ writer_intern_frame(BinaryWriter *writer, const FrameEntry *entry, uint32_t *ind
443448
}
444449

445450
if (writer->frame_count >= UINT32_MAX) {
451+
writer->state = BINARY_WRITER_LIMIT_REACHED;
446452
PyErr_SetString(PyExc_OverflowError,
447453
"too many frames for binary format");
448454
return -1;
449455
}
450456
if (GROW_ARRAY(writer->frame_entries, writer->frame_count,
451457
writer->frame_capacity, FrameEntry) < 0) {
458+
if (PyErr_ExceptionMatches(PyExc_OverflowError)) {
459+
writer->state = BINARY_WRITER_LIMIT_REACHED;
460+
}
452461
return -1;
453462
}
454463

@@ -492,6 +501,7 @@ writer_get_or_create_thread_entry(BinaryWriter *writer, uint64_t thread_id,
492501
}
493502

494503
if (writer->thread_count >= UINT32_MAX) {
504+
writer->state = BINARY_WRITER_LIMIT_REACHED;
495505
PyErr_SetString(PyExc_OverflowError,
496506
"too many threads for binary format");
497507
return NULL;
@@ -501,6 +511,9 @@ writer_get_or_create_thread_entry(BinaryWriter *writer, uint64_t thread_id,
501511
&writer->thread_capacity,
502512
sizeof(ThreadEntry));
503513
if (!new_entries) {
514+
if (PyErr_ExceptionMatches(PyExc_OverflowError)) {
515+
writer->state = BINARY_WRITER_LIMIT_REACHED;
516+
}
504517
return NULL;
505518
}
506519
writer->thread_entries = new_entries;
@@ -933,6 +946,12 @@ static int
933946
process_thread_sample(BinaryWriter *writer, PyObject *thread_info,
934947
uint32_t interpreter_id, uint64_t timestamp_us)
935948
{
949+
if (writer->total_samples == UINT64_MAX) {
950+
writer->state = BINARY_WRITER_LIMIT_REACHED;
951+
PyErr_SetString(PyExc_OverflowError, "too many samples for binary format");
952+
return -1;
953+
}
954+
936955
PyObject *thread_id_obj = PyStructSequence_GET_ITEM(thread_info, 0);
937956
PyObject *status_obj = PyStructSequence_GET_ITEM(thread_info, 1);
938957
PyObject *frame_list = PyStructSequence_GET_ITEM(thread_info, 2);
@@ -955,7 +974,6 @@ process_thread_sample(BinaryWriter *writer, PyObject *thread_info,
955974

956975
/* Calculate timestamp delta */
957976
uint64_t delta = timestamp_us - entry->prev_timestamp;
958-
entry->prev_timestamp = timestamp_us;
959977

960978
/* Process frames and build current stack */
961979
uint32_t curr_stack[MAX_STACK_DEPTH];
@@ -1011,6 +1029,7 @@ process_thread_sample(BinaryWriter *writer, PyObject *thread_info,
10111029
entry->prev_stack_depth = curr_depth;
10121030
}
10131031

1032+
entry->prev_timestamp = timestamp_us;
10141033
writer->total_samples++;
10151034
return 0;
10161035
}
@@ -1030,15 +1049,16 @@ binary_writer_write_sample(BinaryWriter *writer, PyObject *stack_frames, uint64_
10301049
PyObject *interp_id_obj = PyStructSequence_GET_ITEM(interp_info, 0);
10311050
PyObject *threads = PyStructSequence_GET_ITEM(interp_info, 1);
10321051

1033-
unsigned long interp_id_long = PyLong_AsUnsignedLong(interp_id_obj);
1034-
if (interp_id_long == (unsigned long)-1 && PyErr_Occurred()) {
1052+
unsigned long long interp_id_long = PyLong_AsUnsignedLongLong(interp_id_obj);
1053+
if (interp_id_long == (unsigned long long)-1 && PyErr_Occurred()) {
10351054
return -1;
10361055
}
10371056
/* Bounds check: interpreter_id is stored as uint32_t in binary format */
10381057
if (interp_id_long > UINT32_MAX) {
1058+
writer->state = BINARY_WRITER_LIMIT_REACHED;
10391059
PyErr_Format(PyExc_OverflowError,
1040-
"interpreter_id %lu exceeds maximum value %lu",
1041-
interp_id_long, (unsigned long)UINT32_MAX);
1060+
"interpreter_id %llu exceeds maximum value %u",
1061+
interp_id_long, UINT32_MAX);
10421062
return -1;
10431063
}
10441064
uint32_t interpreter_id = (uint32_t)interp_id_long;

0 commit comments

Comments
 (0)