]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
[3.14] gh-139283: correctly handle `size` limit in `cursor.fetchmany()` (GH-139296...
authorMiss Islington (bot) <31488909+miss-islington@users.noreply.github.com>
Tue, 7 Oct 2025 19:23:08 +0000 (21:23 +0200)
committerGitHub <noreply@github.com>
Tue, 7 Oct 2025 19:23:08 +0000 (21:23 +0200)
Passing a negative or zero size to `cursor.fetchmany()` made it fetch all rows
instead of none.

While this could be considered a security vulnerability, it was decided to treat
this issue as a regular bug as passing a non-sanitized *size* value in the first
place is not recommended.
(cherry picked from commit bc172ee8307431caf4c89612e9e454081635191f)

Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com>
Co-authored-by: Petr Viktorin <encukou@gmail.com>
Doc/library/sqlite3.rst
Lib/test/test_sqlite3/test_dbapi.py
Misc/NEWS.d/next/Security/2025-09-24-13-39-56.gh-issue-139283.jODz_q.rst [new file with mode: 0644]
Modules/_sqlite/clinic/cursor.c.h
Modules/_sqlite/cursor.c
Modules/_sqlite/cursor.h

index 0f00f7e93c1810dc64f600542c820c98edfe01d1..55a97bc07885767f0e1b1bbff2fce62eeacc70bb 100644 (file)
@@ -1612,6 +1612,9 @@ Cursor objects
       If the *size* parameter is used, then it is best for it to retain the same
       value from one :meth:`fetchmany` call to the next.
 
+      .. versionchanged:: next
+         Negative *size* values are rejected by raising :exc:`ValueError`.
+
    .. method:: fetchall()
 
       Return all (remaining) rows of a query result as a :class:`list`.
@@ -1639,6 +1642,9 @@ Cursor objects
       Read/write attribute that controls the number of rows returned by :meth:`fetchmany`.
       The default value is 1 which means a single row would be fetched per call.
 
+      .. versionchanged:: next
+         Negative values are rejected by raising :exc:`ValueError`.
+
    .. attribute:: connection
 
       Read-only attribute that provides the SQLite database :class:`Connection`
index 0ef05ff1929b24ccf97a898712d1d4bcec9ac929..0284f2d10b052bc1b8ee5975f27374f9bdf51e26 100644 (file)
@@ -21,6 +21,7 @@
 # 3. This notice may not be removed or altered from any source distribution.
 
 import contextlib
+import functools
 import os
 import sqlite3 as sqlite
 import subprocess
@@ -1060,7 +1061,7 @@ class CursorTests(unittest.TestCase):
         # now set to 2
         self.cu.arraysize = 2
 
-        # now make the query return 3 rows
+        # now make the query return 2 rows from a table of 3 rows
         self.cu.execute("delete from test")
         self.cu.execute("insert into test(name) values ('A')")
         self.cu.execute("insert into test(name) values ('B')")
@@ -1070,13 +1071,50 @@ class CursorTests(unittest.TestCase):
 
         self.assertEqual(len(res), 2)
 
+    def test_invalid_array_size(self):
+        UINT32_MAX = (1 << 32) - 1
+        setter = functools.partial(setattr, self.cu, 'arraysize')
+
+        self.assertRaises(TypeError, setter, 1.0)
+        self.assertRaises(ValueError, setter, -3)
+        self.assertRaises(OverflowError, setter, UINT32_MAX + 1)
+
     def test_fetchmany(self):
+        # no active SQL statement
+        res = self.cu.fetchmany()
+        self.assertEqual(res, [])
+        res = self.cu.fetchmany(1000)
+        self.assertEqual(res, [])
+
+        # test default parameter
+        self.cu.execute("select name from test")
+        res = self.cu.fetchmany()
+        self.assertEqual(len(res), 1)
+
+        # test when the number of requested rows exceeds the actual count
         self.cu.execute("select name from test")
         res = self.cu.fetchmany(100)
         self.assertEqual(len(res), 1)
         res = self.cu.fetchmany(100)
         self.assertEqual(res, [])
 
+        # test when size = 0
+        self.cu.execute("select name from test")
+        res = self.cu.fetchmany(0)
+        self.assertEqual(res, [])
+        res = self.cu.fetchmany(100)
+        self.assertEqual(len(res), 1)
+        res = self.cu.fetchmany(100)
+        self.assertEqual(res, [])
+
+    def test_invalid_fetchmany(self):
+        UINT32_MAX = (1 << 32) - 1
+        fetchmany = self.cu.fetchmany
+
+        self.assertRaises(TypeError, fetchmany, 1.0)
+        self.assertRaises(ValueError, fetchmany, -3)
+        self.assertRaises(OverflowError, fetchmany, UINT32_MAX + 1)
+
     def test_fetchmany_kw_arg(self):
         """Checks if fetchmany works with keyword arguments"""
         self.cu.execute("select name from test")
diff --git a/Misc/NEWS.d/next/Security/2025-09-24-13-39-56.gh-issue-139283.jODz_q.rst b/Misc/NEWS.d/next/Security/2025-09-24-13-39-56.gh-issue-139283.jODz_q.rst
new file mode 100644 (file)
index 0000000..a8fd83b
--- /dev/null
@@ -0,0 +1,4 @@
+:mod:`sqlite3`: correctly handle maximum number of rows to fetch in
+:meth:`Cursor.fetchmany <sqlite3.Cursor.fetchmany>` and reject negative
+values for :attr:`Cursor.arraysize <sqlite3.Cursor.arraysize>`. Patch by
+Bénédikt Tran.
index 350577f488df4b457e81d2b5a898282e6a3176c7..3cad9f3aef5ecd51059f1cee6dbf779a19f35801 100644 (file)
@@ -6,6 +6,7 @@ preserve
 #  include "pycore_gc.h"          // PyGC_Head
 #  include "pycore_runtime.h"     // _Py_ID()
 #endif
+#include "pycore_long.h"          // _PyLong_UInt32_Converter()
 #include "pycore_modsupport.h"    // _PyArg_CheckPositional()
 
 static int
@@ -181,7 +182,7 @@ PyDoc_STRVAR(pysqlite_cursor_fetchmany__doc__,
     {"fetchmany", _PyCFunction_CAST(pysqlite_cursor_fetchmany), METH_FASTCALL|METH_KEYWORDS, pysqlite_cursor_fetchmany__doc__},
 
 static PyObject *
-pysqlite_cursor_fetchmany_impl(pysqlite_Cursor *self, int maxrows);
+pysqlite_cursor_fetchmany_impl(pysqlite_Cursor *self, uint32_t maxrows);
 
 static PyObject *
 pysqlite_cursor_fetchmany(PyObject *self, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
@@ -216,7 +217,7 @@ pysqlite_cursor_fetchmany(PyObject *self, PyObject *const *args, Py_ssize_t narg
     #undef KWTUPLE
     PyObject *argsbuf[1];
     Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 0;
-    int maxrows = ((pysqlite_Cursor *)self)->arraysize;
+    uint32_t maxrows = ((pysqlite_Cursor *)self)->arraysize;
 
     args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser,
             /*minpos*/ 0, /*maxpos*/ 1, /*minkw*/ 0, /*varpos*/ 0, argsbuf);
@@ -226,8 +227,7 @@ pysqlite_cursor_fetchmany(PyObject *self, PyObject *const *args, Py_ssize_t narg
     if (!noptargs) {
         goto skip_optional_pos;
     }
-    maxrows = PyLong_AsInt(args[0]);
-    if (maxrows == -1 && PyErr_Occurred()) {
+    if (!_PyLong_UInt32_Converter(args[0], &maxrows)) {
         goto exit;
     }
 skip_optional_pos:
@@ -329,4 +329,46 @@ pysqlite_cursor_close(PyObject *self, PyObject *Py_UNUSED(ignored))
 {
     return pysqlite_cursor_close_impl((pysqlite_Cursor *)self);
 }
-/*[clinic end generated code: output=d05c7cbbc8bcab26 input=a9049054013a1b77]*/
+
+#if !defined(_sqlite3_Cursor_arraysize_DOCSTR)
+#  define _sqlite3_Cursor_arraysize_DOCSTR NULL
+#endif
+#if defined(_SQLITE3_CURSOR_ARRAYSIZE_GETSETDEF)
+#  undef _SQLITE3_CURSOR_ARRAYSIZE_GETSETDEF
+#  define _SQLITE3_CURSOR_ARRAYSIZE_GETSETDEF {"arraysize", (getter)_sqlite3_Cursor_arraysize_get, (setter)_sqlite3_Cursor_arraysize_set, _sqlite3_Cursor_arraysize_DOCSTR},
+#else
+#  define _SQLITE3_CURSOR_ARRAYSIZE_GETSETDEF {"arraysize", (getter)_sqlite3_Cursor_arraysize_get, NULL, _sqlite3_Cursor_arraysize_DOCSTR},
+#endif
+
+static PyObject *
+_sqlite3_Cursor_arraysize_get_impl(pysqlite_Cursor *self);
+
+static PyObject *
+_sqlite3_Cursor_arraysize_get(PyObject *self, void *Py_UNUSED(context))
+{
+    return _sqlite3_Cursor_arraysize_get_impl((pysqlite_Cursor *)self);
+}
+
+#if !defined(_sqlite3_Cursor_arraysize_DOCSTR)
+#  define _sqlite3_Cursor_arraysize_DOCSTR NULL
+#endif
+#if defined(_SQLITE3_CURSOR_ARRAYSIZE_GETSETDEF)
+#  undef _SQLITE3_CURSOR_ARRAYSIZE_GETSETDEF
+#  define _SQLITE3_CURSOR_ARRAYSIZE_GETSETDEF {"arraysize", (getter)_sqlite3_Cursor_arraysize_get, (setter)_sqlite3_Cursor_arraysize_set, _sqlite3_Cursor_arraysize_DOCSTR},
+#else
+#  define _SQLITE3_CURSOR_ARRAYSIZE_GETSETDEF {"arraysize", NULL, (setter)_sqlite3_Cursor_arraysize_set, NULL},
+#endif
+
+static int
+_sqlite3_Cursor_arraysize_set_impl(pysqlite_Cursor *self, PyObject *value);
+
+static int
+_sqlite3_Cursor_arraysize_set(PyObject *self, PyObject *value, void *Py_UNUSED(context))
+{
+    int return_value;
+
+    return_value = _sqlite3_Cursor_arraysize_set_impl((pysqlite_Cursor *)self, value);
+
+    return return_value;
+}
+/*[clinic end generated code: output=a0e3ebba9e4d0ece input=a9049054013a1b77]*/
index a38bf534d1b334e1972f4a9b1cded5c40ae161b0..cb0f9adcc45a96bf610386d4d0ab54688cd93707 100644 (file)
@@ -1162,35 +1162,31 @@ pysqlite_cursor_fetchone_impl(pysqlite_Cursor *self)
 /*[clinic input]
 _sqlite3.Cursor.fetchmany as pysqlite_cursor_fetchmany
 
-    size as maxrows: int(c_default='((pysqlite_Cursor *)self)->arraysize') = 1
+    size as maxrows: uint32(c_default='((pysqlite_Cursor *)self)->arraysize') = 1
         The default value is set by the Cursor.arraysize attribute.
 
 Fetches several rows from the resultset.
 [clinic start generated code]*/
 
 static PyObject *
-pysqlite_cursor_fetchmany_impl(pysqlite_Cursor *self, int maxrows)
-/*[clinic end generated code: output=a8ef31fea64d0906 input=035dbe44a1005bf2]*/
+pysqlite_cursor_fetchmany_impl(pysqlite_Cursor *self, uint32_t maxrows)
+/*[clinic end generated code: output=3325f2b477c71baf input=a509c412aa70b27e]*/
 {
     PyObject* row;
     PyObject* list;
-    int counter = 0;
 
     list = PyList_New(0);
     if (!list) {
         return NULL;
     }
 
-    while ((row = pysqlite_cursor_iternext((PyObject *)self))) {
-        if (PyList_Append(list, row) < 0) {
-            Py_DECREF(row);
-            break;
-        }
+    while (maxrows > 0 && (row = pysqlite_cursor_iternext((PyObject *)self))) {
+        int rc = PyList_Append(list, row);
         Py_DECREF(row);
-
-        if (++counter == maxrows) {
+        if (rc < 0) {
             break;
         }
+        maxrows--;
     }
 
     if (PyErr_Occurred()) {
@@ -1304,6 +1300,30 @@ pysqlite_cursor_close_impl(pysqlite_Cursor *self)
     Py_RETURN_NONE;
 }
 
+/*[clinic input]
+@getter
+_sqlite3.Cursor.arraysize
+[clinic start generated code]*/
+
+static PyObject *
+_sqlite3_Cursor_arraysize_get_impl(pysqlite_Cursor *self)
+/*[clinic end generated code: output=e0919d97175e6c50 input=3278f8d3ecbd90e3]*/
+{
+    return PyLong_FromUInt32(self->arraysize);
+}
+
+/*[clinic input]
+@setter
+_sqlite3.Cursor.arraysize
+[clinic start generated code]*/
+
+static int
+_sqlite3_Cursor_arraysize_set_impl(pysqlite_Cursor *self, PyObject *value)
+/*[clinic end generated code: output=af59a6b09f8cce6e input=ace48cb114e26060]*/
+{
+    return PyLong_AsUInt32(value, &self->arraysize);
+}
+
 static PyMethodDef cursor_methods[] = {
     PYSQLITE_CURSOR_CLOSE_METHODDEF
     PYSQLITE_CURSOR_EXECUTEMANY_METHODDEF
@@ -1321,7 +1341,6 @@ static struct PyMemberDef cursor_members[] =
 {
     {"connection", _Py_T_OBJECT, offsetof(pysqlite_Cursor, connection), Py_READONLY},
     {"description", _Py_T_OBJECT, offsetof(pysqlite_Cursor, description), Py_READONLY},
-    {"arraysize", Py_T_INT, offsetof(pysqlite_Cursor, arraysize), 0},
     {"lastrowid", _Py_T_OBJECT, offsetof(pysqlite_Cursor, lastrowid), Py_READONLY},
     {"rowcount", Py_T_LONG, offsetof(pysqlite_Cursor, rowcount), Py_READONLY},
     {"row_factory", _Py_T_OBJECT, offsetof(pysqlite_Cursor, row_factory), 0},
@@ -1329,6 +1348,11 @@ static struct PyMemberDef cursor_members[] =
     {NULL}
 };
 
+static struct PyGetSetDef cursor_getsets[] = {
+    _SQLITE3_CURSOR_ARRAYSIZE_GETSETDEF
+    {NULL},
+};
+
 static const char cursor_doc[] =
 PyDoc_STR("SQLite database cursor class.");
 
@@ -1339,6 +1363,7 @@ static PyType_Slot cursor_slots[] = {
     {Py_tp_iternext, pysqlite_cursor_iternext},
     {Py_tp_methods, cursor_methods},
     {Py_tp_members, cursor_members},
+    {Py_tp_getset, cursor_getsets},
     {Py_tp_init, pysqlite_cursor_init},
     {Py_tp_traverse, cursor_traverse},
     {Py_tp_clear, cursor_clear},
index 42f817af7c54ad4cfefc4dafac7810a84f53e6f8..c840a3d7ed0d156ddbfac032d7f21d72c33638b9 100644 (file)
@@ -35,7 +35,7 @@ typedef struct
     pysqlite_Connection* connection;
     PyObject* description;
     PyObject* row_cast_map;
-    int arraysize;
+    uint32_t arraysize;
     PyObject* lastrowid;
     long rowcount;
     PyObject* row_factory;