Skip to content

Commit 8dbe08e

Browse files
authored
pythongh-99240: Fix double-free bug in Argument Clinic str_converter generated code (pythonGH-99241)
Fix double-free bug mentioned at python#99240, by moving memory clean up out of "exit" label. Automerge-Triggered-By: GH:erlend-aasland
1 parent 69f6cc7 commit 8dbe08e

File tree

6 files changed

+201
-25
lines changed

6 files changed

+201
-25
lines changed

Lib/test/clinic.test

+11-22
Original file line numberDiff line numberDiff line change
@@ -1740,37 +1740,26 @@ test_str_converter_encoding(PyObject *module, PyObject *const *args, Py_ssize_t
17401740
goto exit;
17411741
}
17421742
return_value = test_str_converter_encoding_impl(module, a, b, c, d, d_length, e, e_length);
1743+
/* Post parse cleanup for a */
1744+
PyMem_FREE(a);
1745+
/* Post parse cleanup for b */
1746+
PyMem_FREE(b);
1747+
/* Post parse cleanup for c */
1748+
PyMem_FREE(c);
1749+
/* Post parse cleanup for d */
1750+
PyMem_FREE(d);
1751+
/* Post parse cleanup for e */
1752+
PyMem_FREE(e);
17431753

17441754
exit:
1745-
/* Cleanup for a */
1746-
if (a) {
1747-
PyMem_FREE(a);
1748-
}
1749-
/* Cleanup for b */
1750-
if (b) {
1751-
PyMem_FREE(b);
1752-
}
1753-
/* Cleanup for c */
1754-
if (c) {
1755-
PyMem_FREE(c);
1756-
}
1757-
/* Cleanup for d */
1758-
if (d) {
1759-
PyMem_FREE(d);
1760-
}
1761-
/* Cleanup for e */
1762-
if (e) {
1763-
PyMem_FREE(e);
1764-
}
1765-
17661755
return return_value;
17671756
}
17681757

17691758
static PyObject *
17701759
test_str_converter_encoding_impl(PyObject *module, char *a, char *b, char *c,
17711760
char *d, Py_ssize_t d_length, char *e,
17721761
Py_ssize_t e_length)
1773-
/*[clinic end generated code: output=8acb886a3843f3bc input=eb4c38e1f898f402]*/
1762+
/*[clinic end generated code: output=999c1deecfa15b0a input=eb4c38e1f898f402]*/
17741763

17751764

17761765
/*[clinic input]

Lib/test/test_clinic.py

+15
Original file line numberDiff line numberDiff line change
@@ -1045,6 +1045,17 @@ def test_str_converter(self):
10451045
self.assertEqual(ac_tester.str_converter('a', b'b', b'c'), ('a', 'b', 'c'))
10461046
self.assertEqual(ac_tester.str_converter('a', b'b', 'c\0c'), ('a', 'b', 'c\0c'))
10471047

1048+
def test_str_converter_encoding(self):
1049+
with self.assertRaises(TypeError):
1050+
ac_tester.str_converter_encoding(1)
1051+
self.assertEqual(ac_tester.str_converter_encoding('a', 'b', 'c'), ('a', 'b', 'c'))
1052+
with self.assertRaises(TypeError):
1053+
ac_tester.str_converter_encoding('a', b'b\0b', 'c')
1054+
self.assertEqual(ac_tester.str_converter_encoding('a', b'b', bytearray([ord('c')])), ('a', 'b', 'c'))
1055+
self.assertEqual(ac_tester.str_converter_encoding('a', b'b', bytearray([ord('c'), 0, ord('c')])),
1056+
('a', 'b', 'c\x00c'))
1057+
self.assertEqual(ac_tester.str_converter_encoding('a', b'b', b'c\x00c'), ('a', 'b', 'c\x00c'))
1058+
10481059
def test_py_buffer_converter(self):
10491060
with self.assertRaises(TypeError):
10501061
ac_tester.py_buffer_converter('a', 'b')
@@ -1225,6 +1236,10 @@ def test_gh_99233_refcount(self):
12251236
arg_refcount_after = sys.getrefcount(arg)
12261237
self.assertEqual(arg_refcount_origin, arg_refcount_after)
12271238

1239+
def test_gh_99240_double_free(self):
1240+
expected_error = r'gh_99240_double_free\(\) argument 2 must be encoded string without null bytes, not str'
1241+
with self.assertRaisesRegex(TypeError, expected_error):
1242+
ac_tester.gh_99240_double_free('a', '\0b')
12281243

12291244
if __name__ == "__main__":
12301245
unittest.main()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix double-free bug in Argument Clinic ``str_converter`` by
2+
extracting memory clean up to a new ``post_parsing`` section.

Modules/_testclinic.c

+79
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,64 @@ str_converter_impl(PyObject *module, const char *a, const char *b,
551551
}
552552

553553

554+
/*[clinic input]
555+
str_converter_encoding
556+
557+
a: str(encoding="idna")
558+
b: str(encoding="idna", accept={bytes, bytearray, str})
559+
c: str(encoding="idna", accept={bytes, bytearray, str}, zeroes=True)
560+
/
561+
562+
[clinic start generated code]*/
563+
564+
static PyObject *
565+
str_converter_encoding_impl(PyObject *module, char *a, char *b, char *c,
566+
Py_ssize_t c_length)
567+
/*[clinic end generated code: output=af68766049248a1c input=0c5cf5159d0e870d]*/
568+
{
569+
assert(!PyErr_Occurred());
570+
PyObject *out[3] = {NULL,};
571+
int i = 0;
572+
PyObject *arg;
573+
574+
arg = PyUnicode_FromString(a);
575+
assert(arg || PyErr_Occurred());
576+
if (!arg) {
577+
goto error;
578+
}
579+
out[i++] = arg;
580+
581+
arg = PyUnicode_FromString(b);
582+
assert(arg || PyErr_Occurred());
583+
if (!arg) {
584+
goto error;
585+
}
586+
out[i++] = arg;
587+
588+
arg = PyUnicode_FromStringAndSize(c, c_length);
589+
assert(arg || PyErr_Occurred());
590+
if (!arg) {
591+
goto error;
592+
}
593+
out[i++] = arg;
594+
595+
PyObject *tuple = PyTuple_New(3);
596+
if (!tuple) {
597+
goto error;
598+
}
599+
for (int j = 0; j < 3; j++) {
600+
PyTuple_SET_ITEM(tuple, j, out[j]);
601+
}
602+
return tuple;
603+
604+
error:
605+
for (int j = 0; j < i; j++) {
606+
Py_DECREF(out[j]);
607+
}
608+
return NULL;
609+
}
610+
611+
554612
static PyObject *
555613
bytes_from_buffer(Py_buffer *buf)
556614
{
@@ -927,6 +985,25 @@ gh_99233_refcount_impl(PyObject *module, PyObject *args)
927985
}
928986

929987

988+
/*[clinic input]
989+
gh_99240_double_free
990+
991+
a: str(encoding="idna")
992+
b: str(encoding="idna")
993+
/
994+
995+
Proof-of-concept of GH-99240 double-free bug.
996+
997+
[clinic start generated code]*/
998+
999+
static PyObject *
1000+
gh_99240_double_free_impl(PyObject *module, char *a, char *b)
1001+
/*[clinic end generated code: output=586dc714992fe2ed input=23db44aa91870fc7]*/
1002+
{
1003+
Py_RETURN_NONE;
1004+
}
1005+
1006+
9301007
static PyMethodDef tester_methods[] = {
9311008
TEST_EMPTY_FUNCTION_METHODDEF
9321009
OBJECTS_CONVERTER_METHODDEF
@@ -951,6 +1028,7 @@ static PyMethodDef tester_methods[] = {
9511028
DOUBLE_CONVERTER_METHODDEF
9521029
PY_COMPLEX_CONVERTER_METHODDEF
9531030
STR_CONVERTER_METHODDEF
1031+
STR_CONVERTER_ENCODING_METHODDEF
9541032
PY_BUFFER_CONVERTER_METHODDEF
9551033
KEYWORDS_METHODDEF
9561034
KEYWORDS_KWONLY_METHODDEF
@@ -970,6 +1048,7 @@ static PyMethodDef tester_methods[] = {
9701048
KEYWORD_ONLY_PARAMETER_METHODDEF
9711049
VARARG_AND_POSONLY_METHODDEF
9721050
GH_99233_REFCOUNT_METHODDEF
1051+
GH_99240_DOUBLE_FREE_METHODDEF
9731052
{NULL, NULL}
9741053
};
9751054

Modules/clinic/_testclinic.c.h

+71-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Tools/clinic/clinic.py

+23-2
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,12 @@ def __init__(self):
348348
# "goto exit" if there are any.
349349
self.return_conversion = []
350350

351+
# The C statements required to do some operations
352+
# after the end of parsing but before cleaning up.
353+
# These operations may be, for example, memory deallocations which
354+
# can only be done without any error happening during argument parsing.
355+
self.post_parsing = []
356+
351357
# The C statements required to clean up after the impl call.
352358
self.cleanup = []
353359

@@ -820,6 +826,7 @@ def parser_body(prototype, *fields, declarations=''):
820826
{modifications}
821827
{return_value} = {c_basename}_impl({impl_arguments});
822828
{return_conversion}
829+
{post_parsing}
823830
824831
{exit_label}
825832
{cleanup}
@@ -1460,6 +1467,7 @@ def render_function(self, clinic, f):
14601467
template_dict['impl_parameters'] = ", ".join(data.impl_parameters)
14611468
template_dict['impl_arguments'] = ", ".join(data.impl_arguments)
14621469
template_dict['return_conversion'] = format_escape("".join(data.return_conversion).rstrip())
1470+
template_dict['post_parsing'] = format_escape("".join(data.post_parsing).rstrip())
14631471
template_dict['cleanup'] = format_escape("".join(data.cleanup))
14641472
template_dict['return_value'] = data.return_value
14651473

@@ -1484,6 +1492,7 @@ def render_function(self, clinic, f):
14841492
return_conversion=template_dict['return_conversion'],
14851493
initializers=template_dict['initializers'],
14861494
modifications=template_dict['modifications'],
1495+
post_parsing=template_dict['post_parsing'],
14871496
cleanup=template_dict['cleanup'],
14881497
)
14891498

@@ -2725,6 +2734,10 @@ def _render_non_self(self, parameter, data):
27252734
# parse_arguments
27262735
self.parse_argument(data.parse_arguments)
27272736

2737+
# post_parsing
2738+
if post_parsing := self.post_parsing():
2739+
data.post_parsing.append('/* Post parse cleanup for ' + name + ' */\n' + post_parsing.rstrip() + '\n')
2740+
27282741
# cleanup
27292742
cleanup = self.cleanup()
27302743
if cleanup:
@@ -2820,6 +2833,14 @@ def modify(self):
28202833
"""
28212834
return ""
28222835

2836+
def post_parsing(self):
2837+
"""
2838+
The C statements required to do some operations after the end of parsing but before cleaning up.
2839+
Return a string containing this code indented at column 0.
2840+
If no operation is necessary, return an empty string.
2841+
"""
2842+
return ""
2843+
28232844
def cleanup(self):
28242845
"""
28252846
The C statements required to clean up after this variable.
@@ -3416,10 +3437,10 @@ def converter_init(self, *, accept={str}, encoding=None, zeroes=False):
34163437
if NoneType in accept and self.c_default == "Py_None":
34173438
self.c_default = "NULL"
34183439

3419-
def cleanup(self):
3440+
def post_parsing(self):
34203441
if self.encoding:
34213442
name = self.name
3422-
return "".join(["if (", name, ") {\n PyMem_FREE(", name, ");\n}\n"])
3443+
return f"PyMem_FREE({name});\n"
34233444

34243445
def parse_arg(self, argname, displayname):
34253446
if self.format_unit == 's':

0 commit comments

Comments
 (0)