Skip to content

Commit 4a3297f

Browse files
Merge remote-tracking branch 'upstream/3.13' into backport-156106-3.13
2 parents afcc22e + e178a13 commit 4a3297f

6 files changed

Lines changed: 77 additions & 13 deletions

File tree

Lib/test/test_capi/test_structmembers.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,10 @@ def _test_warn(self, name, value, expected=None):
6060

6161
def _test_overflow(self, name, value):
6262
ts = self.ts
63+
oldvalue = getattr(ts, name)
6364
self.assertRaises(OverflowError, setattr, ts, name, value)
65+
# a failed assignment does not change the value
66+
self.assertEqual(getattr(ts, name), oldvalue)
6467

6568
def _test_int_range(self, name, minval, maxval, *, hardlimit=None,
6669
indexlimit=None):
@@ -154,8 +157,11 @@ def test_bad_assignments(self):
154157
# issue8014: this produced 'bad argument to internal function'
155158
# internal error
156159
for nonint in None, 3.2j, "full of eels", {}, []:
157-
for attr in integer_attributes:
160+
for attr in integer_attributes + ['T_FLOAT', 'T_DOUBLE']:
161+
oldvalue = getattr(ts, attr)
158162
self.assertRaises(TypeError, setattr, ts, attr, nonint)
163+
# a failed assignment does not change the value
164+
self.assertEqual(getattr(ts, attr), oldvalue)
159165

160166
def test_inplace_string(self):
161167
ts = self.ts

Lib/test/test_io.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4088,6 +4088,49 @@ def test_chunk_size(self):
40884088
# a failed assignment does not change the value
40894089
self.assertEqual(t._CHUNK_SIZE, 1024)
40904090

4091+
def test_reentrant_seek_during_tell(self):
4092+
# gh-153539: reading short of _CHUNK_SIZE leaves residual bytes in the
4093+
# snapshot, so tell() re-decodes and calls the decoder's getstate(); a
4094+
# reentrant seek() there must not free the snapshot tell() still uses.
4095+
# C-only: _pyio binds next_input as a strong local and cannot crash.
4096+
wrapper = None
4097+
armed = False
4098+
4099+
class ReentrantDecoder(codecs.IncrementalDecoder):
4100+
def decode(self, input, final=False):
4101+
return bytes(input).decode("latin-1")
4102+
def getstate(self):
4103+
nonlocal armed
4104+
if wrapper is not None and armed:
4105+
armed = False
4106+
wrapper.seek(0)
4107+
return (b"", 0)
4108+
def setstate(self, state):
4109+
pass
4110+
4111+
def search(name):
4112+
if name != "reentrant_tell_test":
4113+
return None
4114+
return codecs.CodecInfo(
4115+
name=name,
4116+
encode=lambda s, e='strict': (s.encode("latin-1"), len(s)),
4117+
decode=lambda b, e='strict': (bytes(b).decode("latin-1"), len(b)),
4118+
incrementaldecoder=ReentrantDecoder)
4119+
4120+
codecs.register(search)
4121+
self.addCleanup(codecs.unregister, search)
4122+
raw = self.BytesIO(b"abcdefghijklmnop" * 8)
4123+
wrapper = self.TextIOWrapper(self.BufferedReader(raw),
4124+
encoding="reentrant_tell_test", newline="")
4125+
wrapper._CHUNK_SIZE = 8
4126+
wrapper.read(5)
4127+
armed = True
4128+
self.assertIsInstance(wrapper.tell(), int)
4129+
# tell() at the snapshot boundary takes the early return that owns and
4130+
# must release next_input; exercise it too (leak-checked under -R).
4131+
wrapper.seek(0)
4132+
self.assertIsInstance(wrapper.tell(), int)
4133+
40914134
def test_initialization(self):
40924135
r = self.BytesIO(b"\xc3\xa9\n\n")
40934136
b = self.BufferedReader(r, 1000)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
A failed assignment to an attribute defined with :c:type:`PyMemberDef` of
2+
type ``Py_T_LONG``, ``Py_T_LONGLONG``, ``Py_T_PYSSIZET`` or ``Py_T_DOUBLE``
3+
no longer changes its value.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix a crash in the C implementation of :meth:`io.TextIOWrapper.tell` when the
2+
decoder's ``getstate`` method triggers a reentrant seek, or when another thread
3+
seeks the same stream concurrently. Patch by tonghuaroot.

Modules/_io/textio.c

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2726,7 +2726,7 @@ _io_TextIOWrapper_tell_impl(textio *self)
27262726
PyObject *res;
27272727
PyObject *posobj = NULL;
27282728
cookie_type cookie = {0,0,0,0,0};
2729-
PyObject *next_input;
2729+
PyObject *next_input = NULL;
27302730
Py_ssize_t chars_to_skip, chars_decoded;
27312731
Py_ssize_t skip_bytes, skip_back;
27322732
PyObject *saved_state = NULL;
@@ -2778,11 +2778,15 @@ _io_TextIOWrapper_tell_impl(textio *self)
27782778

27792779
assert (PyBytes_Check(next_input));
27802780

2781+
/* Own next_input: a reentrant or concurrent seek can drop the snapshot. */
2782+
Py_INCREF(next_input);
2783+
27812784
cookie.start_pos -= PyBytes_GET_SIZE(next_input);
27822785

27832786
/* How many decoded characters have been used up since the snapshot? */
27842787
if (self->decoded_chars_used == 0) {
27852788
/* We haven't moved from the snapshot point. */
2789+
Py_DECREF(next_input);
27862790
return textiowrapper_build_cookie(&cookie);
27872791
}
27882792

@@ -2923,6 +2927,7 @@ _io_TextIOWrapper_tell_impl(textio *self)
29232927
}
29242928

29252929
finally:
2930+
Py_XDECREF(next_input);
29262931
res = PyObject_CallMethodOneArg(
29272932
self->decoder, &_Py_ID(setstate), saved_state);
29282933
Py_DECREF(saved_state);
@@ -2935,6 +2940,7 @@ _io_TextIOWrapper_tell_impl(textio *self)
29352940
return textiowrapper_build_cookie(&cookie);
29362941

29372942
fail:
2943+
Py_XDECREF(next_input);
29382944
if (saved_state) {
29392945
PyObject *exc = PyErr_GetRaisedException();
29402946
res = PyObject_CallMethodOneArg(

Python/structmember.c

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -251,9 +251,10 @@ PyMember_SetOne(char *addr, PyMemberDef *l, PyObject *v)
251251
break;
252252
}
253253
case Py_T_LONG:{
254-
*(long*)addr = PyLong_AsLong(v);
255-
if ((*(long*)addr == -1) && PyErr_Occurred())
254+
long long_val = PyLong_AsLong(v);
255+
if ((long_val == -1) && PyErr_Occurred())
256256
return -1;
257+
*(long*)addr = long_val;
257258
break;
258259
}
259260
case Py_T_ULONG: {
@@ -283,10 +284,10 @@ PyMember_SetOne(char *addr, PyMemberDef *l, PyObject *v)
283284
break;
284285
}
285286
case Py_T_PYSSIZET:{
286-
*(Py_ssize_t*)addr = PyLong_AsSsize_t(v);
287-
if ((*(Py_ssize_t*)addr == (Py_ssize_t)-1)
288-
&& PyErr_Occurred())
289-
return -1;
287+
Py_ssize_t ssize_val = PyLong_AsSsize_t(v);
288+
if ((ssize_val == (Py_ssize_t)-1) && PyErr_Occurred())
289+
return -1;
290+
*(Py_ssize_t*)addr = ssize_val;
290291
break;
291292
}
292293
case Py_T_FLOAT:{
@@ -296,11 +297,13 @@ PyMember_SetOne(char *addr, PyMemberDef *l, PyObject *v)
296297
*(float*)addr = (float)double_val;
297298
break;
298299
}
299-
case Py_T_DOUBLE:
300-
*(double*)addr = PyFloat_AsDouble(v);
301-
if ((*(double*)addr == -1) && PyErr_Occurred())
300+
case Py_T_DOUBLE:{
301+
double double_val = PyFloat_AsDouble(v);
302+
if ((double_val == -1) && PyErr_Occurred())
302303
return -1;
304+
*(double*)addr = double_val;
303305
break;
306+
}
304307
case _Py_T_OBJECT:
305308
case Py_T_OBJECT_EX:
306309
Py_BEGIN_CRITICAL_SECTION(obj);
@@ -326,10 +329,10 @@ PyMember_SetOne(char *addr, PyMemberDef *l, PyObject *v)
326329
PyErr_SetString(PyExc_TypeError, "readonly attribute");
327330
return -1;
328331
case Py_T_LONGLONG:{
329-
long long value;
330-
*(long long*)addr = value = PyLong_AsLongLong(v);
332+
long long value = PyLong_AsLongLong(v);
331333
if ((value == -1) && PyErr_Occurred())
332334
return -1;
335+
*(long long*)addr = value;
333336
break;
334337
}
335338
case Py_T_ULONGLONG: {

0 commit comments

Comments
 (0)