https://github.com/python/cpython/commit/9f5994b94cb6b8526bcb8fa29a99656dc403e25e
commit: 9f5994b94cb6b8526bcb8fa29a99656dc403e25e
branch: main
author: Serhiy Storchaka <storch...@gmail.com>
committer: serhiy-storchaka <storch...@gmail.com>
date: 2025-04-24T19:17:11+03:00
summary:

gh-132742: Refactor fcntl.fcntl() and fcntl.ioctl()  (GH-132768)

* Support arbitrary bytes-like objects, not only bytes, in fcntl().
* The fcntl() buffer argument is now null-terminated.
* Automatically retry an ioctl() system calls failing with EINTR.
* Release the GIL for an ioctl() system call even for large bytes-like object.
* Do not silence arbitrary errors whet try to get a buffer.
* Optimize argument parsing, check the argument type before trying to get
  a buffer or convert it to integer.
* Fix some error messages.

files:
A Misc/NEWS.d/next/Library/2025-04-23-14-50-45.gh-issue-132742.PB6B7F.rst
M Doc/library/fcntl.rst
M Modules/clinic/fcntlmodule.c.h
M Modules/fcntlmodule.c

diff --git a/Doc/library/fcntl.rst b/Doc/library/fcntl.rst
index 3dcfe3b87f93d6..a9fbab3d0bbf33 100644
--- a/Doc/library/fcntl.rst
+++ b/Doc/library/fcntl.rst
@@ -89,14 +89,14 @@ The module defines the following functions:
    for *cmd* are operating system dependent, and are available as constants
    in the :mod:`fcntl` module, using the same names as used in the relevant C
    header files. The argument *arg* can either be an integer value, a
-   :class:`bytes` object, or a string.
+   :term:`bytes-like object`, or a string.
    The type and size of *arg* must match the type and size of
    the argument of the operation as specified in the relevant C documentation.
 
    When *arg* is an integer, the function returns the integer
    return value of the C :c:func:`fcntl` call.
 
-   When the argument is bytes, it represents a binary structure,
+   When the argument is bytes-like object, it represents a binary structure,
    for example, created by :func:`struct.pack`.
    A string value is encoded to binary using the UTF-8 encoding.
    The binary data is copied to a buffer whose address is
@@ -117,6 +117,10 @@ The module defines the following functions:
 
    .. audit-event:: fcntl.fcntl fd,cmd,arg fcntl.fcntl
 
+   .. versionchanged:: next
+      Add support of arbitrary :term:`bytes-like objects <bytes-like object>`,
+      not only :class:`bytes`.
+
 
 .. function:: ioctl(fd, request, arg=0, mutate_flag=True, /)
 
@@ -173,6 +177,9 @@ The module defines the following functions:
 
    .. audit-event:: fcntl.ioctl fd,request,arg fcntl.ioctl
 
+   .. versionchanged:: next
+      The GIL is always released during a system call.
+      System calls failing with EINTR are automatically retried.
 
 .. function:: flock(fd, operation, /)
 
diff --git 
a/Misc/NEWS.d/next/Library/2025-04-23-14-50-45.gh-issue-132742.PB6B7F.rst 
b/Misc/NEWS.d/next/Library/2025-04-23-14-50-45.gh-issue-132742.PB6B7F.rst
new file mode 100644
index 00000000000000..db2905bb3bcd13
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-04-23-14-50-45.gh-issue-132742.PB6B7F.rst
@@ -0,0 +1,4 @@
+:func:`fcntl.fcntl` now supports arbitrary :term:`bytes-like objects
+<bytes-like object>`, not only :class:`bytes`. :func:`fcntl.ioctl` now
+automatically retries system calls failing with EINTR and releases the GIL
+during a system call even for large bytes-like object.
diff --git a/Modules/clinic/fcntlmodule.c.h b/Modules/clinic/fcntlmodule.c.h
index 53b139e09afdf1..d8721e9ea99dd9 100644
--- a/Modules/clinic/fcntlmodule.c.h
+++ b/Modules/clinic/fcntlmodule.c.h
@@ -96,8 +96,8 @@ PyDoc_STRVAR(fcntl_ioctl__doc__,
     {"ioctl", (PyCFunction)(void(*)(void))fcntl_ioctl, METH_FASTCALL, 
fcntl_ioctl__doc__},
 
 static PyObject *
-fcntl_ioctl_impl(PyObject *module, int fd, unsigned long code,
-                 PyObject *ob_arg, int mutate_arg);
+fcntl_ioctl_impl(PyObject *module, int fd, unsigned long code, PyObject *arg,
+                 int mutate_arg);
 
 static PyObject *
 fcntl_ioctl(PyObject *module, PyObject *const *args, Py_ssize_t nargs)
@@ -105,7 +105,7 @@ fcntl_ioctl(PyObject *module, PyObject *const *args, 
Py_ssize_t nargs)
     PyObject *return_value = NULL;
     int fd;
     unsigned long code;
-    PyObject *ob_arg = NULL;
+    PyObject *arg = NULL;
     int mutate_arg = 1;
 
     if (nargs < 2) {
@@ -128,7 +128,7 @@ fcntl_ioctl(PyObject *module, PyObject *const *args, 
Py_ssize_t nargs)
     if (nargs < 3) {
         goto skip_optional;
     }
-    ob_arg = args[2];
+    arg = args[2];
     if (nargs < 4) {
         goto skip_optional;
     }
@@ -137,7 +137,7 @@ fcntl_ioctl(PyObject *module, PyObject *const *args, 
Py_ssize_t nargs)
         goto exit;
     }
 skip_optional:
-    return_value = fcntl_ioctl_impl(module, fd, code, ob_arg, mutate_arg);
+    return_value = fcntl_ioctl_impl(module, fd, code, arg, mutate_arg);
 
 exit:
     return return_value;
@@ -264,4 +264,4 @@ fcntl_lockf(PyObject *module, PyObject *const *args, 
Py_ssize_t nargs)
 exit:
     return return_value;
 }
-/*[clinic end generated code: output=45a56f53fd17ff3c input=a9049054013a1b77]*/
+/*[clinic end generated code: output=30c01b46bfa840c1 input=a9049054013a1b77]*/
diff --git a/Modules/fcntlmodule.c b/Modules/fcntlmodule.c
index 90ebfd7e99a777..81a1e1c22c8a27 100644
--- a/Modules/fcntlmodule.c
+++ b/Modules/fcntlmodule.c
@@ -54,57 +54,66 @@ static PyObject *
 fcntl_fcntl_impl(PyObject *module, int fd, int code, PyObject *arg)
 /*[clinic end generated code: output=888fc93b51c295bd input=7955340198e5f334]*/
 {
-    unsigned int int_arg = 0;
     int ret;
-    char *str;
-    Py_ssize_t len;
-    char buf[1024];
     int async_err = 0;
 
     if (PySys_Audit("fcntl.fcntl", "iiO", fd, code, arg ? arg : Py_None) < 0) {
         return NULL;
     }
 
-    if (arg != NULL) {
-        int parse_result;
-
-        if (PyArg_Parse(arg, "s#", &str, &len)) {
-            if ((size_t)len > sizeof buf) {
-                PyErr_SetString(PyExc_ValueError,
-                                "fcntl string arg too long");
+    if (arg == NULL || PyIndex_Check(arg)) {
+        unsigned int int_arg = 0;
+        if (arg != NULL) {
+            if (!PyArg_Parse(arg, "I", &int_arg)) {
                 return NULL;
             }
-            memcpy(buf, str, len);
-            do {
-                Py_BEGIN_ALLOW_THREADS
-                ret = fcntl(fd, code, buf);
-                Py_END_ALLOW_THREADS
-            } while (ret == -1 && errno == EINTR && !(async_err = 
PyErr_CheckSignals()));
-            if (ret < 0) {
-                return !async_err ? PyErr_SetFromErrno(PyExc_OSError) : NULL;
-            }
-            return PyBytes_FromStringAndSize(buf, len);
         }
 
-        PyErr_Clear();
-        parse_result = PyArg_Parse(arg,
-            "I;fcntl requires a file or file descriptor,"
-            " an integer and optionally a third integer or a string",
-            &int_arg);
-        if (!parse_result) {
-          return NULL;
+        do {
+            Py_BEGIN_ALLOW_THREADS
+            ret = fcntl(fd, code, (int)int_arg);
+            Py_END_ALLOW_THREADS
+        } while (ret == -1 && errno == EINTR && !(async_err = 
PyErr_CheckSignals()));
+        if (ret < 0) {
+            return !async_err ? PyErr_SetFromErrno(PyExc_OSError) : NULL;
         }
+        return PyLong_FromLong(ret);
     }
+    if (PyUnicode_Check(arg) || PyObject_CheckBuffer(arg)) {
+#define FCNTL_BUFSZ 1024
+        Py_buffer view;
+        char buf[FCNTL_BUFSZ+1];  /* argument plus NUL byte */
 
-    do {
-        Py_BEGIN_ALLOW_THREADS
-        ret = fcntl(fd, code, (int)int_arg);
-        Py_END_ALLOW_THREADS
-    } while (ret == -1 && errno == EINTR && !(async_err = 
PyErr_CheckSignals()));
-    if (ret < 0) {
-        return !async_err ? PyErr_SetFromErrno(PyExc_OSError) : NULL;
+        if (!PyArg_Parse(arg, "s*", &view)) {
+            return NULL;
+        }
+        Py_ssize_t len = view.len;
+        if (len > FCNTL_BUFSZ) {
+            PyErr_SetString(PyExc_ValueError,
+                            "fcntl argument 3 is too long");
+            PyBuffer_Release(&view);
+            return NULL;
+        }
+        memcpy(buf, view.buf, len);
+        buf[len] = '\0';
+        PyBuffer_Release(&view);
+
+        do {
+            Py_BEGIN_ALLOW_THREADS
+            ret = fcntl(fd, code, buf);
+            Py_END_ALLOW_THREADS
+        } while (ret == -1 && errno == EINTR && !(async_err = 
PyErr_CheckSignals()));
+        if (ret < 0) {
+            return !async_err ? PyErr_SetFromErrno(PyExc_OSError) : NULL;
+        }
+        return PyBytes_FromStringAndSize(buf, len);
+#undef FCNTL_BUFSZ
     }
-    return PyLong_FromLong((long)ret);
+    PyErr_Format(PyExc_TypeError,
+                 "fcntl() argument 3 must be an integer, "
+                 "a bytes-like object, or a string, not %T",
+                 arg);
+    return NULL;
 }
 
 
@@ -113,7 +122,7 @@ fcntl.ioctl
 
     fd: fildes
     request as code: unsigned_long(bitwise=True)
-    arg as ob_arg: object(c_default='NULL') = 0
+    arg: object(c_default='NULL') = 0
     mutate_flag as mutate_arg: bool = True
     /
 
@@ -148,11 +157,10 @@ code.
 [clinic start generated code]*/
 
 static PyObject *
-fcntl_ioctl_impl(PyObject *module, int fd, unsigned long code,
-                 PyObject *ob_arg, int mutate_arg)
-/*[clinic end generated code: output=3d8eb6828666cea1 input=cee70f6a27311e58]*/
+fcntl_ioctl_impl(PyObject *module, int fd, unsigned long code, PyObject *arg,
+                 int mutate_arg)
+/*[clinic end generated code: output=f72baba2454d7a62 input=9c6cca5e2c339622]*/
 {
-#define IOCTL_BUFSZ 1024
     /* We use the unsigned non-checked 'I' format for the 'code' parameter
        because the system expects it to be a 32bit bit field value
        regardless of it being passed as an int or unsigned long on
@@ -163,114 +171,100 @@ fcntl_ioctl_impl(PyObject *module, int fd, unsigned 
long code,
        in their unsigned long ioctl codes this will break and need
        special casing based on the platform being built on.
      */
-    int arg = 0;
     int ret;
-    Py_buffer pstr;
-    char *str;
-    Py_ssize_t len;
-    char buf[IOCTL_BUFSZ+1];  /* argument plus NUL byte */
+    int async_err = 0;
 
-    if (PySys_Audit("fcntl.ioctl", "ikO", fd, code,
-                    ob_arg ? ob_arg : Py_None) < 0) {
+    if (PySys_Audit("fcntl.ioctl", "ikO", fd, code, arg ? arg : Py_None) < 0) {
         return NULL;
     }
 
-    if (ob_arg != NULL) {
-        if (PyArg_Parse(ob_arg, "w*:ioctl", &pstr)) {
-            char *arg;
-            str = pstr.buf;
-            len = pstr.len;
-
-            if (mutate_arg) {
-                if (len <= IOCTL_BUFSZ) {
-                    memcpy(buf, str, len);
-                    buf[len] = '\0';
-                    arg = buf;
+    if (arg == NULL || PyIndex_Check(arg)) {
+        int int_arg = 0;
+        if (arg != NULL) {
+            if (!PyArg_Parse(arg, "i", &int_arg)) {
+                return NULL;
+            }
+        }
+
+        do {
+            Py_BEGIN_ALLOW_THREADS
+            ret = ioctl(fd, code, int_arg);
+            Py_END_ALLOW_THREADS
+        } while (ret == -1 && errno == EINTR && !(async_err = 
PyErr_CheckSignals()));
+        if (ret < 0) {
+            return !async_err ? PyErr_SetFromErrno(PyExc_OSError) : NULL;
+        }
+        return PyLong_FromLong(ret);
+    }
+    if (PyUnicode_Check(arg) || PyObject_CheckBuffer(arg)) {
+        Py_buffer view;
+#define IOCTL_BUFSZ 1024
+        char buf[IOCTL_BUFSZ+1];  /* argument plus NUL byte */
+        if (mutate_arg && !PyBytes_Check(arg) && !PyUnicode_Check(arg)) {
+            if (PyObject_GetBuffer(arg, &view, PyBUF_WRITABLE) == 0) {
+                if (view.len <= IOCTL_BUFSZ) {
+                    memcpy(buf, view.buf, view.len);
+                    buf[view.len] = '\0';
+                    do {
+                        Py_BEGIN_ALLOW_THREADS
+                        ret = ioctl(fd, code, buf);
+                        Py_END_ALLOW_THREADS
+                    } while (ret == -1 && errno == EINTR && !(async_err = 
PyErr_CheckSignals()));
+                    memcpy(view.buf, buf, view.len);
                 }
                 else {
-                    arg = str;
+                    do {
+                        Py_BEGIN_ALLOW_THREADS
+                        ret = ioctl(fd, code, view.buf);
+                        Py_END_ALLOW_THREADS
+                    } while (ret == -1 && errno == EINTR && !(async_err = 
PyErr_CheckSignals()));
                 }
-            }
-            else {
-                if (len > IOCTL_BUFSZ) {
-                    PyBuffer_Release(&pstr);
-                    PyErr_SetString(PyExc_ValueError,
-                        "ioctl string arg too long");
+                if (ret < 0) {
+                    if (!async_err) {
+                        PyErr_SetFromErrno(PyExc_OSError);
+                    }
+                    PyBuffer_Release(&view);
                     return NULL;
                 }
-                else {
-                    memcpy(buf, str, len);
-                    buf[len] = '\0';
-                    arg = buf;
-                }
-            }
-            if (buf == arg) {
-                Py_BEGIN_ALLOW_THREADS /* think array.resize() */
-                ret = ioctl(fd, code, arg);
-                Py_END_ALLOW_THREADS
-            }
-            else {
-                ret = ioctl(fd, code, arg);
-            }
-            if (mutate_arg && (len <= IOCTL_BUFSZ)) {
-                memcpy(str, buf, len);
-            }
-            if (ret < 0) {
-                PyErr_SetFromErrno(PyExc_OSError);
-                PyBuffer_Release(&pstr);
-                return NULL;
-            }
-            PyBuffer_Release(&pstr);
-            if (mutate_arg) {
+                PyBuffer_Release(&view);
                 return PyLong_FromLong(ret);
             }
-            else {
-                return PyBytes_FromStringAndSize(buf, len);
+            if (!PyErr_ExceptionMatches(PyExc_BufferError)) {
+                return NULL;
             }
+            PyErr_Clear();
         }
 
-        PyErr_Clear();
-        if (PyArg_Parse(ob_arg, "s*:ioctl", &pstr)) {
-            str = pstr.buf;
-            len = pstr.len;
-            if (len > IOCTL_BUFSZ) {
-                PyBuffer_Release(&pstr);
-                PyErr_SetString(PyExc_ValueError,
-                                "ioctl string arg too long");
-                return NULL;
-            }
-            memcpy(buf, str, len);
-            buf[len] = '\0';
+        if (!PyArg_Parse(arg, "s*", &view)) {
+            return NULL;
+        }
+        Py_ssize_t len = view.len;
+        if (len > IOCTL_BUFSZ) {
+            PyErr_SetString(PyExc_ValueError,
+                            "ioctl argument 3 is too long");
+            PyBuffer_Release(&view);
+            return NULL;
+        }
+        memcpy(buf, view.buf, len);
+        buf[len] = '\0';
+        PyBuffer_Release(&view);
+
+        do {
             Py_BEGIN_ALLOW_THREADS
             ret = ioctl(fd, code, buf);
             Py_END_ALLOW_THREADS
-            if (ret < 0) {
-                PyErr_SetFromErrno(PyExc_OSError);
-                PyBuffer_Release(&pstr);
-                return NULL;
-            }
-            PyBuffer_Release(&pstr);
-            return PyBytes_FromStringAndSize(buf, len);
-        }
-
-        PyErr_Clear();
-        if (!PyArg_Parse(ob_arg,
-             "i;ioctl requires a file or file descriptor,"
-             " an integer and optionally an integer or buffer argument",
-             &arg)) {
-          return NULL;
+        } while (ret == -1 && errno == EINTR && !(async_err = 
PyErr_CheckSignals()));
+        if (ret < 0) {
+            return !async_err ? PyErr_SetFromErrno(PyExc_OSError) : NULL;
         }
-        // Fall-through to outside the 'if' statement.
-    }
-    Py_BEGIN_ALLOW_THREADS
-    ret = ioctl(fd, code, arg);
-    Py_END_ALLOW_THREADS
-    if (ret < 0) {
-        PyErr_SetFromErrno(PyExc_OSError);
-        return NULL;
-    }
-    return PyLong_FromLong((long)ret);
+        return PyBytes_FromStringAndSize(buf, len);
 #undef IOCTL_BUFSZ
+    }
+    PyErr_Format(PyExc_TypeError,
+                 "ioctl() argument 3 must be an integer, "
+                 "a bytes-like object, or a string, not %T",
+                 arg);
+    return NULL;
 }
 
 /*[clinic input]

_______________________________________________
Python-checkins mailing list -- python-checkins@python.org
To unsubscribe send an email to python-checkins-le...@python.org
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: arch...@mail-archive.com

Reply via email to