Skip to content

Commit 3d8e144

Browse files
pythongh-111942: Fix crashes in TextIOWrapper.reconfigure()
* Fix crash when encoding is not string or None. * Fix crash when both line_buffering and write_through raise exception when converted ti int. * Add a number of tests for constructor and reconfigure() method with invalid arguments.
1 parent 771bd3c commit 3d8e144

3 files changed

Lines changed: 118 additions & 5 deletions

File tree

‎Lib/test/test_io.py‎

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ def _default_chunk_size():
7777
)
7878

7979

80+
class BadIndex:
81+
def __index__(self):
82+
1/0
83+
8084
class MockRawIOWithoutRead:
8185
"""A RawIO implementation without read(), so as to exercise the default
8286
RawIO.read() which calls readinto()."""
@@ -2709,8 +2713,27 @@ def test_constructor(self):
27092713
self.assertEqual(t.encoding, "utf-8")
27102714
self.assertEqual(t.line_buffering, True)
27112715
self.assertEqual("\xe9\n", t.readline())
2712-
self.assertRaises(TypeError, t.__init__, b, encoding="utf-8", newline=42)
2713-
self.assertRaises(ValueError, t.__init__, b, encoding="utf-8", newline='xyzzy')
2716+
invalid_type = TypeError if self.is_C else ValueError
2717+
with self.assertRaises(invalid_type):
2718+
t.__init__(b, encoding=42)
2719+
with self.assertRaises(UnicodeEncodeError):
2720+
t.__init__(b, encoding='\udcfe')
2721+
with self.assertRaises(ValueError):
2722+
t.__init__(b, encoding='utf-8\0')
2723+
with self.assertRaises(invalid_type):
2724+
t.__init__(b, encoding="utf-8", errors=42)
2725+
with self.assertRaises(UnicodeEncodeError):
2726+
t.__init__(b, encoding="utf-8", errors='\udcfe')
2727+
with self.assertRaises(ValueError):
2728+
t.__init__(b, encoding="utf-8", errors='replace\0')
2729+
with self.assertRaises(TypeError):
2730+
t.__init__(b, encoding="utf-8", newline=42)
2731+
with self.assertRaises(ValueError):
2732+
t.__init__(b, encoding="utf-8", newline='\udcfe')
2733+
with self.assertRaises(ValueError):
2734+
t.__init__(b, encoding="utf-8", newline='\n\0')
2735+
with self.assertRaises(ValueError):
2736+
t.__init__(b, encoding="utf-8", newline='xyzzy')
27142737

27152738
def test_uninitialized(self):
27162739
t = self.TextIOWrapper.__new__(self.TextIOWrapper)
@@ -3756,6 +3779,59 @@ def test_reconfigure_defaults(self):
37563779

37573780
self.assertEqual(txt.detach().getvalue(), b'LF\nCRLF\r\n')
37583781

3782+
def test_reconfigure_errors(self):
3783+
txt = self.TextIOWrapper(self.BytesIO(), 'ascii', 'replace', '\r')
3784+
with self.assertRaises(TypeError): # there was a crash
3785+
txt.reconfigure(encoding=42)
3786+
if self.is_C:
3787+
with self.assertRaises(UnicodeEncodeError):
3788+
txt.reconfigure(encoding='\udcfe')
3789+
with self.assertRaises(LookupError):
3790+
txt.reconfigure(encoding='locale\0')
3791+
# TODO: txt.reconfigure(encoding='utf-8\0')
3792+
# TODO: txt.reconfigure(encoding='nonexisting')
3793+
with self.assertRaises(TypeError):
3794+
txt.reconfigure(errors=42)
3795+
if self.is_C:
3796+
with self.assertRaises(UnicodeEncodeError):
3797+
txt.reconfigure(errors='\udcfe')
3798+
# TODO: txt.reconfigure(errors='ignore\0')
3799+
# TODO: txt.reconfigure(errors='nonexisting')
3800+
with self.assertRaises(TypeError):
3801+
txt.reconfigure(newline=42)
3802+
with self.assertRaises(ValueError):
3803+
txt.reconfigure(newline='\udcfe')
3804+
with self.assertRaises(ValueError):
3805+
txt.reconfigure(newline='xyz')
3806+
if not self.is_C:
3807+
# TODO: Should fail in C too.
3808+
with self.assertRaises(ValueError):
3809+
txt.reconfigure(newline='\n\0')
3810+
if self.is_C:
3811+
# TODO: Use __bool__(), not __index__().
3812+
with self.assertRaises(ZeroDivisionError):
3813+
txt.reconfigure(line_buffering=BadIndex())
3814+
with self.assertRaises(OverflowError):
3815+
txt.reconfigure(line_buffering=2**1000)
3816+
with self.assertRaises(ZeroDivisionError):
3817+
txt.reconfigure(write_through=BadIndex())
3818+
with self.assertRaises(OverflowError):
3819+
txt.reconfigure(write_through=2**1000)
3820+
with self.assertRaises(ZeroDivisionError): # there was a crash
3821+
txt.reconfigure(line_buffering=BadIndex(),
3822+
write_through=BadIndex())
3823+
self.assertEqual(txt.encoding, 'ascii')
3824+
self.assertEqual(txt.errors, 'replace')
3825+
self.assertIs(txt.line_buffering, False)
3826+
self.assertIs(txt.write_through, False)
3827+
3828+
txt.reconfigure(encoding='latin1', errors='ignore', newline='\r\n',
3829+
line_buffering=True, write_through=True)
3830+
self.assertEqual(txt.encoding, 'latin1')
3831+
self.assertEqual(txt.errors, 'ignore')
3832+
self.assertIs(txt.line_buffering, True)
3833+
self.assertIs(txt.write_through, True)
3834+
37593835
def test_reconfigure_newline(self):
37603836
raw = self.BytesIO(b'CR\rEOF')
37613837
txt = self.TextIOWrapper(raw, 'ascii', newline='\n')
@@ -4781,9 +4857,11 @@ def load_tests(loader, tests, pattern):
47814857
if test.__name__.startswith("C"):
47824858
for name, obj in c_io_ns.items():
47834859
setattr(test, name, obj)
4860+
test.is_C = True
47844861
elif test.__name__.startswith("Py"):
47854862
for name, obj in py_io_ns.items():
47864863
setattr(test, name, obj)
4864+
test.is_C = False
47874865

47884866
suite = loader.suiteClass()
47894867
for test in tests:
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix crashes in :meth:`io.TextIOWrapper.reconfigure` when pass invalid
2+
arguments, e.g. non-string encoding.

‎Modules/_io/textio.c‎

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1284,30 +1284,40 @@ textiowrapper_change_encoding(textio *self, PyObject *encoding,
12841284
errors = &_Py_ID(strict);
12851285
}
12861286
}
1287+
Py_INCREF(errors);
12871288

1289+
const char *c_encoding = PyUnicode_AsUTF8(encoding);
1290+
if (c_encoding == NULL) {
1291+
Py_DECREF(encoding);
1292+
Py_DECREF(errors);
1293+
return -1;
1294+
}
12881295
const char *c_errors = PyUnicode_AsUTF8(errors);
12891296
if (c_errors == NULL) {
12901297
Py_DECREF(encoding);
1298+
Py_DECREF(errors);
12911299
return -1;
12921300
}
12931301

12941302
// Create new encoder & decoder
12951303
PyObject *codec_info = _PyCodec_LookupTextEncoding(
1296-
PyUnicode_AsUTF8(encoding), "codecs.open()");
1304+
c_encoding, "codecs.open()");
12971305
if (codec_info == NULL) {
12981306
Py_DECREF(encoding);
1307+
Py_DECREF(errors);
12991308
return -1;
13001309
}
13011310
if (_textiowrapper_set_decoder(self, codec_info, c_errors) != 0 ||
13021311
_textiowrapper_set_encoder(self, codec_info, c_errors) != 0) {
13031312
Py_DECREF(codec_info);
13041313
Py_DECREF(encoding);
1314+
Py_DECREF(errors);
13051315
return -1;
13061316
}
13071317
Py_DECREF(codec_info);
13081318

13091319
Py_SETREF(self->encoding, encoding);
1310-
Py_SETREF(self->errors, Py_NewRef(errors));
1320+
Py_SETREF(self->errors, errors);
13111321

13121322
return _textiowrapper_fix_encoder_state(self);
13131323
}
@@ -1338,6 +1348,26 @@ _io_TextIOWrapper_reconfigure_impl(textio *self, PyObject *encoding,
13381348
int write_through;
13391349
const char *newline = NULL;
13401350

1351+
if (encoding != Py_None && !PyUnicode_Check(encoding)) {
1352+
PyErr_Format(PyExc_TypeError,
1353+
"reconfigure() argument 'encoding' must be str or None, not %s",
1354+
Py_TYPE(encoding)->tp_name);
1355+
return NULL;
1356+
}
1357+
if (errors != Py_None && !PyUnicode_Check(errors)) {
1358+
PyErr_Format(PyExc_TypeError,
1359+
"reconfigure() argument 'errors' must be str or None, not %s",
1360+
Py_TYPE(errors)->tp_name);
1361+
return NULL;
1362+
}
1363+
if (newline_obj != NULL && newline_obj != Py_None &&
1364+
!PyUnicode_Check(newline_obj))
1365+
{
1366+
PyErr_Format(PyExc_TypeError,
1367+
"reconfigure() argument 'newline' must be str or None, not %s",
1368+
Py_TYPE(newline_obj)->tp_name);
1369+
return NULL;
1370+
}
13411371
/* Check if something is in the read buffer */
13421372
if (self->decoded_chars != NULL) {
13431373
if (encoding != Py_None || errors != Py_None || newline_obj != NULL) {
@@ -1357,9 +1387,12 @@ _io_TextIOWrapper_reconfigure_impl(textio *self, PyObject *encoding,
13571387

13581388
line_buffering = convert_optional_bool(line_buffering_obj,
13591389
self->line_buffering);
1390+
if (line_buffering < 0) {
1391+
return NULL;
1392+
}
13601393
write_through = convert_optional_bool(write_through_obj,
13611394
self->write_through);
1362-
if (line_buffering < 0 || write_through < 0) {
1395+
if (write_through < 0) {
13631396
return NULL;
13641397
}
13651398

0 commit comments

Comments
 (0)