Skip to content

Commit c063bd1

Browse files
committed
gh-144777: Fix data races in IncrementalNewlineDecoder
Add critical sections to methods of IncrementalNewlineDecoder to prevent concurrent access to shared bitfields (pendingcr, seennl).
1 parent 3e2f5c1 commit c063bd1

File tree

4 files changed

+83
-7
lines changed

4 files changed

+83
-7
lines changed

Lib/test/test_free_threading/test_io.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1+
import codecs
12
import io
23
import _pyio as pyio
34
import threading
45
from unittest import TestCase
56
from test.support import threading_helper
7+
from test.support.threading_helper import run_concurrently
68
from random import randint
79
from sys import getsizeof
810

11+
threading_helper.requires_working_threading(module=True)
12+
913

1014
class ThreadSafetyMixin:
1115
# Test pretty much everything that can break under free-threading.
@@ -115,3 +119,54 @@ class CBytesIOTest(ThreadSafetyMixin, TestCase):
115119

116120
class PyBytesIOTest(ThreadSafetyMixin, TestCase):
117121
ioclass = pyio.BytesIO
122+
123+
124+
class IncrementalNewlineDecoderTest(TestCase):
125+
def make_decoder(self):
126+
utf8_decoder = codecs.getincrementaldecoder('utf-8')()
127+
return io.IncrementalNewlineDecoder(utf8_decoder, translate=True)
128+
129+
def test_concurrent_reset(self):
130+
decoder = self.make_decoder()
131+
132+
def worker():
133+
for _ in range(100):
134+
decoder.reset()
135+
136+
run_concurrently(worker_func=worker, nthreads=2)
137+
138+
def test_concurrent_decode(self):
139+
decoder = self.make_decoder()
140+
141+
def worker():
142+
for _ in range(100):
143+
decoder.decode(b"line\r\n", final=False)
144+
145+
run_concurrently(worker_func=worker, nthreads=2)
146+
147+
def test_concurrent_getstate_setstate(self):
148+
decoder = self.make_decoder()
149+
state = decoder.getstate()
150+
151+
def getstate_worker():
152+
for _ in range(100):
153+
decoder.getstate()
154+
155+
def setstate_worker():
156+
for _ in range(100):
157+
decoder.setstate(state)
158+
159+
run_concurrently([getstate_worker] * 2 + [setstate_worker] * 2)
160+
161+
def test_concurrent_decode_and_reset(self):
162+
decoder = self.make_decoder()
163+
164+
def decode_worker():
165+
for _ in range(100):
166+
decoder.decode(b"line\r\n", final=False)
167+
168+
def reset_worker():
169+
for _ in range(100):
170+
decoder.reset()
171+
172+
run_concurrently([decode_worker] * 2 + [reset_worker] * 2)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix data races in :class:`io.IncrementalNewlineDecoder` in the :term:`free-threaded build`.

Modules/_io/clinic/textio.c.h

Lines changed: 19 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Modules/_io/textio.c

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,7 @@ _PyIncrementalNewlineDecoder_decode(PyObject *myself,
519519
}
520520

521521
/*[clinic input]
522+
@critical_section
522523
_io.IncrementalNewlineDecoder.decode
523524
input: object
524525
final: bool = False
@@ -527,18 +528,19 @@ _io.IncrementalNewlineDecoder.decode
527528
static PyObject *
528529
_io_IncrementalNewlineDecoder_decode_impl(nldecoder_object *self,
529530
PyObject *input, int final)
530-
/*[clinic end generated code: output=0d486755bb37a66e input=90e223c70322c5cd]*/
531+
/*[clinic end generated code: output=0d486755bb37a66e input=9475d16a73168504]*/
531532
{
532533
return _PyIncrementalNewlineDecoder_decode((PyObject *) self, input, final);
533534
}
534535

535536
/*[clinic input]
537+
@critical_section
536538
_io.IncrementalNewlineDecoder.getstate
537539
[clinic start generated code]*/
538540

539541
static PyObject *
540542
_io_IncrementalNewlineDecoder_getstate_impl(nldecoder_object *self)
541-
/*[clinic end generated code: output=f0d2c9c136f4e0d0 input=f8ff101825e32e7f]*/
543+
/*[clinic end generated code: output=f0d2c9c136f4e0d0 input=dc3e1f27aa850f12]*/
542544
{
543545
PyObject *buffer;
544546
unsigned long long flag;
@@ -576,6 +578,7 @@ _io_IncrementalNewlineDecoder_getstate_impl(nldecoder_object *self)
576578
}
577579

578580
/*[clinic input]
581+
@critical_section
579582
_io.IncrementalNewlineDecoder.setstate
580583
state: object
581584
/
@@ -584,7 +587,7 @@ _io.IncrementalNewlineDecoder.setstate
584587
static PyObject *
585588
_io_IncrementalNewlineDecoder_setstate_impl(nldecoder_object *self,
586589
PyObject *state)
587-
/*[clinic end generated code: output=09135cb6e78a1dc8 input=c53fb505a76dbbe2]*/
590+
/*[clinic end generated code: output=09135cb6e78a1dc8 input=275fd3982d2b08cb]*/
588591
{
589592
PyObject *buffer;
590593
unsigned long long flag;
@@ -614,12 +617,13 @@ _io_IncrementalNewlineDecoder_setstate_impl(nldecoder_object *self,
614617
}
615618

616619
/*[clinic input]
620+
@critical_section
617621
_io.IncrementalNewlineDecoder.reset
618622
[clinic start generated code]*/
619623

620624
static PyObject *
621625
_io_IncrementalNewlineDecoder_reset_impl(nldecoder_object *self)
622-
/*[clinic end generated code: output=32fa40c7462aa8ff input=728678ddaea776df]*/
626+
/*[clinic end generated code: output=32fa40c7462aa8ff input=31bd8ae4e36cec83]*/
623627
{
624628
CHECK_INITIALIZED_DECODER(self);
625629

0 commit comments

Comments
 (0)