[3.11] gh-94938: Fix errror detection of unexpected keyword arguments (GH-94999) (GH-95353)

When keyword argument name is an instance of a str subclass with
overloaded methods __eq__ and __hash__, the former code could not find
the name of an extraneous keyword argument to report an error, and
_PyArg_UnpackKeywords() returned success without setting the
corresponding cell in the linearized arguments array. But since the number
of expected initialized cells is determined as the total number of passed
arguments, this lead to reading NULL as a keyword parameter value, that
caused SystemError or crash or other undesired behavior.
(cherry picked from commit ebad53a4dc)

Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
This commit is contained in:
Serhiy Storchaka 2022-07-28 09:51:45 +03:00 committed by GitHub
parent 547f0bb82f
commit 33efd7f7a3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 112 additions and 85 deletions

View file

@ -11,6 +11,19 @@ import gc
import contextlib
class BadStr(str):
def __eq__(self, other):
return True
def __hash__(self):
# Guaranteed different hash
return str.__hash__(self) ^ 3
def __eq__(self, other):
return False
def __hash__(self):
return str.__hash__(self)
class FunctionCalls(unittest.TestCase):
def test_kwargs_order(self):
@ -145,6 +158,18 @@ class CFunctionCallsErrorMessages(unittest.TestCase):
self.assertRaisesRegex(TypeError, msg,
print, 0, sep=1, end=2, file=3, flush=4, foo=5)
def test_varargs18_kw(self):
# _PyArg_UnpackKeywordsWithVararg()
msg = r"invalid keyword argument for print\(\)$"
with self.assertRaisesRegex(TypeError, msg):
print(0, 1, **{BadStr('foo'): ','})
def test_varargs19_kw(self):
# _PyArg_UnpackKeywords()
msg = r"invalid keyword argument for round\(\)$"
with self.assertRaisesRegex(TypeError, msg):
round(1.75, **{BadStr('foo'): 1})
def test_oldargs0_1(self):
msg = r"keys\(\) takes no arguments \(1 given\)"
self.assertRaisesRegex(TypeError, msg, {}.keys, 0)

View file

@ -747,6 +747,33 @@ class KeywordOnly_TestCase(unittest.TestCase):
"'\udc80' is an invalid keyword argument for this function"):
getargs_keyword_only(1, 2, **{'\uDC80': 10})
def test_weird_str_subclass(self):
class BadStr(str):
def __eq__(self, other):
return True
def __hash__(self):
# Guaranteed different hash
return str.__hash__(self) ^ 3
with self.assertRaisesRegex(TypeError,
"invalid keyword argument for this function"):
getargs_keyword_only(1, 2, **{BadStr("keyword_only"): 3})
with self.assertRaisesRegex(TypeError,
"invalid keyword argument for this function"):
getargs_keyword_only(1, 2, **{BadStr("monster"): 666})
def test_weird_str_subclass2(self):
class BadStr(str):
def __eq__(self, other):
return False
def __hash__(self):
return str.__hash__(self)
with self.assertRaisesRegex(TypeError,
"invalid keyword argument for this function"):
getargs_keyword_only(1, 2, **{BadStr("keyword_only"): 3})
with self.assertRaisesRegex(TypeError,
"invalid keyword argument for this function"):
getargs_keyword_only(1, 2, **{BadStr("monster"): 666})
class PositionalOnlyAndKeywords_TestCase(unittest.TestCase):
from _testcapi import getargs_positional_only_and_keywords as getargs

View file

@ -0,0 +1,3 @@
Fix error detection in some builtin functions when keyword argument name is
an instance of a str subclass with overloaded ``__eq__`` and ``__hash__``.
Previously it could cause SystemError or other undesired behavior.

View file

@ -1554,6 +1554,50 @@ _PyArg_VaParseTupleAndKeywordsFast_SizeT(PyObject *args, PyObject *keywords,
return retval;
}
static void
error_unexpected_keyword_arg(PyObject *kwargs, PyObject *kwnames, PyObject *kwtuple, const char *fname)
{
/* make sure there are no extraneous keyword arguments */
Py_ssize_t j = 0;
while (1) {
PyObject *keyword;
if (kwargs != NULL) {
if (!PyDict_Next(kwargs, &j, &keyword, NULL))
break;
}
else {
if (j >= PyTuple_GET_SIZE(kwnames))
break;
keyword = PyTuple_GET_ITEM(kwnames, j);
j++;
}
if (!PyUnicode_Check(keyword)) {
PyErr_SetString(PyExc_TypeError,
"keywords must be strings");
return;
}
int match = PySequence_Contains(kwtuple, keyword);
if (match <= 0) {
if (!match) {
PyErr_Format(PyExc_TypeError,
"'%S' is an invalid keyword "
"argument for %.200s%s",
keyword,
(fname == NULL) ? "this function" : fname,
(fname == NULL) ? "" : "()");
}
return;
}
}
/* Something wrong happened. There are extraneous keyword arguments,
* but we don't know what. And we don't bother. */
PyErr_Format(PyExc_TypeError,
"invalid keyword argument for %.200s%s",
(fname == NULL) ? "this function" : fname,
(fname == NULL) ? "" : "()");
}
int
PyArg_ValidateKeywordArguments(PyObject *kwargs)
{
@ -1842,6 +1886,13 @@ vgetargskeywords(PyObject *args, PyObject *kwargs, const char *format,
return cleanreturn(0, &freelist);
}
}
/* Something wrong happened. There are extraneous keyword arguments,
* but we don't know what. And we don't bother. */
PyErr_Format(PyExc_TypeError,
"invalid keyword argument for %.200s%s",
(fname == NULL) ? "this function" : fname,
(fname == NULL) ? "" : "()");
return cleanreturn(0, &freelist);
}
return cleanreturn(1, &freelist);
@ -2184,7 +2235,6 @@ vgetargskeywordsfast_impl(PyObject *const *args, Py_ssize_t nargs,
assert(IS_END_OF_FORMAT(*format) || (*format == '|') || (*format == '$'));
if (nkwargs > 0) {
Py_ssize_t j;
/* make sure there are no arguments given by name and position */
for (i = pos; i < nargs; i++) {
keyword = PyTuple_GET_ITEM(kwtuple, i - pos);
@ -2208,34 +2258,9 @@ vgetargskeywordsfast_impl(PyObject *const *args, Py_ssize_t nargs,
return cleanreturn(0, &freelist);
}
}
/* make sure there are no extraneous keyword arguments */
j = 0;
while (1) {
int match;
if (kwargs != NULL) {
if (!PyDict_Next(kwargs, &j, &keyword, NULL))
break;
}
else {
if (j >= PyTuple_GET_SIZE(kwnames))
break;
keyword = PyTuple_GET_ITEM(kwnames, j);
j++;
}
match = PySequence_Contains(kwtuple, keyword);
if (match <= 0) {
if (!match) {
PyErr_Format(PyExc_TypeError,
"'%S' is an invalid keyword "
"argument for %.200s%s",
keyword,
(parser->fname == NULL) ? "this function" : parser->fname,
(parser->fname == NULL) ? "" : "()");
}
return cleanreturn(0, &freelist);
}
}
error_unexpected_keyword_arg(kwargs, kwnames, kwtuple, parser->fname);
return cleanreturn(0, &freelist);
}
return cleanreturn(1, &freelist);
@ -2409,7 +2434,6 @@ _PyArg_UnpackKeywords(PyObject *const *args, Py_ssize_t nargs,
}
if (nkwargs > 0) {
Py_ssize_t j;
/* make sure there are no arguments given by name and position */
for (i = posonly; i < nargs; i++) {
keyword = PyTuple_GET_ITEM(kwtuple, i - posonly);
@ -2433,34 +2457,9 @@ _PyArg_UnpackKeywords(PyObject *const *args, Py_ssize_t nargs,
return NULL;
}
}
/* make sure there are no extraneous keyword arguments */
j = 0;
while (1) {
int match;
if (kwargs != NULL) {
if (!PyDict_Next(kwargs, &j, &keyword, NULL))
break;
}
else {
if (j >= PyTuple_GET_SIZE(kwnames))
break;
keyword = PyTuple_GET_ITEM(kwnames, j);
j++;
}
match = PySequence_Contains(kwtuple, keyword);
if (match <= 0) {
if (!match) {
PyErr_Format(PyExc_TypeError,
"'%S' is an invalid keyword "
"argument for %.200s%s",
keyword,
(parser->fname == NULL) ? "this function" : parser->fname,
(parser->fname == NULL) ? "" : "()");
}
return NULL;
}
}
error_unexpected_keyword_arg(kwargs, kwnames, kwtuple, parser->fname);
return NULL;
}
return buf;
@ -2589,35 +2588,8 @@ _PyArg_UnpackKeywordsWithVararg(PyObject *const *args, Py_ssize_t nargs,
}
if (nkwargs > 0) {
Py_ssize_t j;
/* make sure there are no extraneous keyword arguments */
j = 0;
while (1) {
int match;
if (kwargs != NULL) {
if (!PyDict_Next(kwargs, &j, &keyword, NULL))
break;
}
else {
if (j >= PyTuple_GET_SIZE(kwnames))
break;
keyword = PyTuple_GET_ITEM(kwnames, j);
j++;
}
match = PySequence_Contains(kwtuple, keyword);
if (match <= 0) {
if (!match) {
PyErr_Format(PyExc_TypeError,
"'%S' is an invalid keyword "
"argument for %.200s%s",
keyword,
(parser->fname == NULL) ? "this function" : parser->fname,
(parser->fname == NULL) ? "" : "()");
}
goto exit;
}
}
error_unexpected_keyword_arg(kwargs, kwnames, kwtuple, parser->fname);
goto exit;
}
return buf;