https://github.com/python/cpython/commit/a2932e9c95640cb98828f3e78545afacf78776f5
commit: a2932e9c95640cb98828f3e78545afacf78776f5
branch: main
author: Maurycy Pawłowski-Wieroński <[email protected]>
committer: vstinner <[email protected]>
date: 2026-05-18T16:29:58+02:00
summary:

gh-149464: Add `os.pidfd_getfd(pidfd, targetfd, flags=0)` function (#149465)

Co-authored-by: Bénédikt Tran <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-05-06-20-10-44.gh-issue-149464.3mPhcr.rst
M Doc/library/os.rst
M Doc/whatsnew/3.16.rst
M Include/internal/pycore_global_objects_fini_generated.h
M Include/internal/pycore_global_strings.h
M Include/internal/pycore_runtime_init_generated.h
M Include/internal/pycore_unicodeobject_generated.h
M Lib/test/test_os/test_posix.py
M Modules/clinic/posixmodule.c.h
M Modules/posixmodule.c

diff --git a/Doc/library/os.rst b/Doc/library/os.rst
index d2534b3e974f36..f6be2dec0729f2 100644
--- a/Doc/library/os.rst
+++ b/Doc/library/os.rst
@@ -5062,6 +5062,18 @@ written in Python, such as a mail server's external 
command delivery program.
    .. availability:: Linux >= 5.10
    .. versionadded:: 3.12
 
+.. function:: pidfd_getfd(pidfd, targetfd, *, flags=0)
+
+   Duplicate *targetfd* from the process referred to by the process file
+   descriptor *pidfd*, into the calling process.  The returned file descriptor
+   is :ref:`non-inheritable <fd_inheritance>`.
+
+   *flags* is reserved, and currently must be ``0``.
+
+   See the :manpage:`pidfd_getfd(2)` man page for more details.
+
+   .. availability:: Linux >= 5.6, Android >= :func:`build-time 
<sys.getandroidapilevel>` API level 31
+   .. versionadded:: next
 
 .. function:: plock(op, /)
 
diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst
index cff0b8bbe32f0b..9383ce5665993e 100644
--- a/Doc/whatsnew/3.16.rst
+++ b/Doc/whatsnew/3.16.rst
@@ -86,10 +86,12 @@ New modules
 Improved modules
 ================
 
-module_name
------------
+os
+--
 
-* TODO
+* Add :func:`os.pidfd_getfd` for duplicating a file descriptor from another
+  process via a pidfd.  Available on Linux 5.6+.
+  (Contributed by Maurycy Pawłowski-Wieroński in :gh:`149464`.)
 
 .. Add improved modules above alphabetically, not here at the end.
 
diff --git a/Include/internal/pycore_global_objects_fini_generated.h 
b/Include/internal/pycore_global_objects_fini_generated.h
index f7d3dcd440aaf1..7e6107e54a0ab6 100644
--- a/Include/internal/pycore_global_objects_fini_generated.h
+++ b/Include/internal/pycore_global_objects_fini_generated.h
@@ -1990,6 +1990,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) {
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(person));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(pi_factory));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(pid));
+    _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(pidfd));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(pointer_bits));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(policy));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(pos));
@@ -2108,6 +2109,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) {
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(take_bytes));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(target));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(target_is_directory));
+    _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(targetfd));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(task));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(tb_frame));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(tb_lasti));
diff --git a/Include/internal/pycore_global_strings.h 
b/Include/internal/pycore_global_strings.h
index 22494b1798cc53..e0b05dde8fa377 100644
--- a/Include/internal/pycore_global_strings.h
+++ b/Include/internal/pycore_global_strings.h
@@ -713,6 +713,7 @@ struct _Py_global_strings {
         STRUCT_FOR_ID(person)
         STRUCT_FOR_ID(pi_factory)
         STRUCT_FOR_ID(pid)
+        STRUCT_FOR_ID(pidfd)
         STRUCT_FOR_ID(pointer_bits)
         STRUCT_FOR_ID(policy)
         STRUCT_FOR_ID(pos)
@@ -831,6 +832,7 @@ struct _Py_global_strings {
         STRUCT_FOR_ID(take_bytes)
         STRUCT_FOR_ID(target)
         STRUCT_FOR_ID(target_is_directory)
+        STRUCT_FOR_ID(targetfd)
         STRUCT_FOR_ID(task)
         STRUCT_FOR_ID(tb_frame)
         STRUCT_FOR_ID(tb_lasti)
diff --git a/Include/internal/pycore_runtime_init_generated.h 
b/Include/internal/pycore_runtime_init_generated.h
index 892c3cdd9623a2..47c6226a7d2216 100644
--- a/Include/internal/pycore_runtime_init_generated.h
+++ b/Include/internal/pycore_runtime_init_generated.h
@@ -1988,6 +1988,7 @@ extern "C" {
     INIT_ID(person), \
     INIT_ID(pi_factory), \
     INIT_ID(pid), \
+    INIT_ID(pidfd), \
     INIT_ID(pointer_bits), \
     INIT_ID(policy), \
     INIT_ID(pos), \
@@ -2106,6 +2107,7 @@ extern "C" {
     INIT_ID(take_bytes), \
     INIT_ID(target), \
     INIT_ID(target_is_directory), \
+    INIT_ID(targetfd), \
     INIT_ID(task), \
     INIT_ID(tb_frame), \
     INIT_ID(tb_lasti), \
diff --git a/Include/internal/pycore_unicodeobject_generated.h 
b/Include/internal/pycore_unicodeobject_generated.h
index f0fc3c4f5b0900..acad87c5045338 100644
--- a/Include/internal/pycore_unicodeobject_generated.h
+++ b/Include/internal/pycore_unicodeobject_generated.h
@@ -2632,6 +2632,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) 
{
     _PyUnicode_InternStatic(interp, &string);
     assert(_PyUnicode_CheckConsistency(string, 1));
     assert(PyUnicode_GET_LENGTH(string) != 1);
+    string = &_Py_ID(pidfd);
+    _PyUnicode_InternStatic(interp, &string);
+    assert(_PyUnicode_CheckConsistency(string, 1));
+    assert(PyUnicode_GET_LENGTH(string) != 1);
     string = &_Py_ID(pointer_bits);
     _PyUnicode_InternStatic(interp, &string);
     assert(_PyUnicode_CheckConsistency(string, 1));
@@ -3104,6 +3108,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) 
{
     _PyUnicode_InternStatic(interp, &string);
     assert(_PyUnicode_CheckConsistency(string, 1));
     assert(PyUnicode_GET_LENGTH(string) != 1);
+    string = &_Py_ID(targetfd);
+    _PyUnicode_InternStatic(interp, &string);
+    assert(_PyUnicode_CheckConsistency(string, 1));
+    assert(PyUnicode_GET_LENGTH(string) != 1);
     string = &_Py_ID(task);
     _PyUnicode_InternStatic(interp, &string);
     assert(_PyUnicode_CheckConsistency(string, 1));
diff --git a/Lib/test/test_os/test_posix.py b/Lib/test/test_os/test_posix.py
index d0a662a091829e..1395156539a163 100644
--- a/Lib/test/test_os/test_posix.py
+++ b/Lib/test/test_os/test_posix.py
@@ -1604,6 +1604,34 @@ def test_pidfd_open(self):
         self.assertEqual(cm.exception.errno, errno.EINVAL)
         os.close(os.pidfd_open(os.getpid(), 0))
 
+    @unittest.skipUnless(hasattr(os, "pidfd_getfd"), "pidfd_getfd unavailable")
+    def test_pidfd_getfd(self):
+        fd = os.open(__file__, os.O_RDONLY)
+        self.addCleanup(os.close, fd)
+        pidfd = os.pidfd_open(os.getpid(), 0)
+        self.addCleanup(os.close, pidfd)
+        try:
+            dupfd = os.pidfd_getfd(pidfd, fd)
+        except OSError as exc:
+            if exc.errno == errno.ENOSYS:
+                self.skipTest("system does not support pidfd_getfd")
+            if isinstance(exc, PermissionError):
+                self.skipTest(f"pidfd_getfd syscall blocked: {exc!r}")
+            raise
+        self.addCleanup(os.close, dupfd)
+
+        self.assertFalse(os.get_inheritable(dupfd))     # PEP 446
+        self.assertEqual(os.fstat(fd), os.fstat(dupfd))
+
+        with self.assertRaises(OSError) as cm:
+            os.pidfd_getfd(-1, 0)
+        self.assertEqual(cm.exception.errno, errno.EBADF)
+
+        with self.assertRaises(OSError) as cm:
+            bad_fd = os_helper.make_bad_fd()
+            os.pidfd_getfd(pidfd, bad_fd)
+        self.assertEqual(cm.exception.errno, errno.EBADF)
+
     @os_helper.skip_unless_hardlink
     @os_helper.skip_unless_symlink
     def test_link_follow_symlinks(self):
diff --git 
a/Misc/NEWS.d/next/Library/2026-05-06-20-10-44.gh-issue-149464.3mPhcr.rst 
b/Misc/NEWS.d/next/Library/2026-05-06-20-10-44.gh-issue-149464.3mPhcr.rst
new file mode 100644
index 00000000000000..9f987d2a662d8f
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-05-06-20-10-44.gh-issue-149464.3mPhcr.rst
@@ -0,0 +1,3 @@
+Add :func:`os.pidfd_getfd` for duplicating a file descriptor from another
+process via a pidfd.  Available on Linux 5.6+. Patch by Maurycy
+Pawłowski-Wieroński.
diff --git a/Modules/clinic/posixmodule.c.h b/Modules/clinic/posixmodule.c.h
index ad4c5dd1c9bc08..5403b875b04a32 100644
--- a/Modules/clinic/posixmodule.c.h
+++ b/Modules/clinic/posixmodule.c.h
@@ -6393,6 +6393,93 @@ os_pidfd_open(PyObject *module, PyObject *const *args, 
Py_ssize_t nargs, PyObjec
 
 #endif /* (defined(__linux__) && defined(__NR_pidfd_open) && 
!(defined(__ANDROID__) && __ANDROID_API__ < 31)) */
 
+#if (defined(__linux__) && defined(__NR_pidfd_getfd) && !(defined(__ANDROID__) 
&& __ANDROID_API__ < 31))
+
+PyDoc_STRVAR(os_pidfd_getfd__doc__,
+"pidfd_getfd($module, /, pidfd, targetfd, *, flags=0)\n"
+"--\n"
+"\n"
+"Duplicate a file descriptor from the process referred to by *pidfd*.\n"
+"\n"
+"  pidfd\n"
+"    A process file descriptor.\n"
+"  targetfd\n"
+"    The file descriptor to duplicate from the target process.\n"
+"  flags\n"
+"    Reserved, must be 0.");
+
+#define OS_PIDFD_GETFD_METHODDEF    \
+    {"pidfd_getfd", _PyCFunction_CAST(os_pidfd_getfd), 
METH_FASTCALL|METH_KEYWORDS, os_pidfd_getfd__doc__},
+
+static PyObject *
+os_pidfd_getfd_impl(PyObject *module, int pidfd, int targetfd,
+                    unsigned int flags);
+
+static PyObject *
+os_pidfd_getfd(PyObject *module, PyObject *const *args, Py_ssize_t nargs, 
PyObject *kwnames)
+{
+    PyObject *return_value = NULL;
+    #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)
+
+    #define NUM_KEYWORDS 3
+    static struct {
+        PyGC_Head _this_is_not_used;
+        PyObject_VAR_HEAD
+        Py_hash_t ob_hash;
+        PyObject *ob_item[NUM_KEYWORDS];
+    } _kwtuple = {
+        .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS)
+        .ob_hash = -1,
+        .ob_item = { &_Py_ID(pidfd), &_Py_ID(targetfd), &_Py_ID(flags), },
+    };
+    #undef NUM_KEYWORDS
+    #define KWTUPLE (&_kwtuple.ob_base.ob_base)
+
+    #else  // !Py_BUILD_CORE
+    #  define KWTUPLE NULL
+    #endif  // !Py_BUILD_CORE
+
+    static const char * const _keywords[] = {"pidfd", "targetfd", "flags", 
NULL};
+    static _PyArg_Parser _parser = {
+        .keywords = _keywords,
+        .fname = "pidfd_getfd",
+        .kwtuple = KWTUPLE,
+    };
+    #undef KWTUPLE
+    PyObject *argsbuf[3];
+    Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 
2;
+    int pidfd;
+    int targetfd;
+    unsigned int flags = 0;
+
+    args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser,
+            /*minpos*/ 2, /*maxpos*/ 2, /*minkw*/ 0, /*varpos*/ 0, argsbuf);
+    if (!args) {
+        goto exit;
+    }
+    pidfd = PyLong_AsInt(args[0]);
+    if (pidfd == -1 && PyErr_Occurred()) {
+        goto exit;
+    }
+    targetfd = PyLong_AsInt(args[1]);
+    if (targetfd == -1 && PyErr_Occurred()) {
+        goto exit;
+    }
+    if (!noptargs) {
+        goto skip_optional_kwonly;
+    }
+    if (!_PyLong_UnsignedInt_Converter(args[2], &flags)) {
+        goto exit;
+    }
+skip_optional_kwonly:
+    return_value = os_pidfd_getfd_impl(module, pidfd, targetfd, flags);
+
+exit:
+    return return_value;
+}
+
+#endif /* (defined(__linux__) && defined(__NR_pidfd_getfd) && 
!(defined(__ANDROID__) && __ANDROID_API__ < 31)) */
+
 #if defined(HAVE_SETNS)
 
 PyDoc_STRVAR(os_setns__doc__,
@@ -13296,6 +13383,10 @@ os__emscripten_log(PyObject *module, PyObject *const 
*args, Py_ssize_t nargs, Py
     #define OS_PIDFD_OPEN_METHODDEF
 #endif /* !defined(OS_PIDFD_OPEN_METHODDEF) */
 
+#ifndef OS_PIDFD_GETFD_METHODDEF
+    #define OS_PIDFD_GETFD_METHODDEF
+#endif /* !defined(OS_PIDFD_GETFD_METHODDEF) */
+
 #ifndef OS_SETNS_METHODDEF
     #define OS_SETNS_METHODDEF
 #endif /* !defined(OS_SETNS_METHODDEF) */
@@ -13611,4 +13702,4 @@ os__emscripten_log(PyObject *module, PyObject *const 
*args, Py_ssize_t nargs, Py
 #ifndef OS__EMSCRIPTEN_LOG_METHODDEF
     #define OS__EMSCRIPTEN_LOG_METHODDEF
 #endif /* !defined(OS__EMSCRIPTEN_LOG_METHODDEF) */
-/*[clinic end generated code: output=e709b8b783fbc261 input=a9049054013a1b77]*/
+/*[clinic end generated code: output=c4cf19262e42e352 input=a9049054013a1b77]*/
diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c
index 5bd53c2146a822..7552cd150f0c2a 100644
--- a/Modules/posixmodule.c
+++ b/Modules/posixmodule.c
@@ -10777,6 +10777,35 @@ os_pidfd_open_impl(PyObject *module, pid_t pid, 
unsigned int flags)
 #endif
 
 
+#if defined(__linux__) && defined(__NR_pidfd_getfd) && \
+    !(defined(__ANDROID__) && __ANDROID_API__ < 31)
+/*[clinic input]
+os.pidfd_getfd
+  pidfd: int
+    A process file descriptor.
+  targetfd: int
+    The file descriptor to duplicate from the target process.
+  *
+  flags: unsigned_int = 0
+    Reserved, must be 0.
+
+Duplicate a file descriptor from the process referred to by *pidfd*.
+[clinic start generated code]*/
+
+static PyObject *
+os_pidfd_getfd_impl(PyObject *module, int pidfd, int targetfd,
+                    unsigned int flags)
+/*[clinic end generated code: output=e1a1415a13c7137f input=ef6417fb10deb1cc]*/
+{
+    int fd = syscall(__NR_pidfd_getfd, pidfd, targetfd, flags);
+    if (fd < 0) {
+        return posix_error();
+    }
+    return PyLong_FromLong(fd);
+}
+#endif
+
+
 #ifdef HAVE_SETNS
 /*[clinic input]
 os.setns
@@ -17606,6 +17635,7 @@ static PyMethodDef posix_methods[] = {
     OS_WAITID_METHODDEF
     OS_WAITPID_METHODDEF
     OS_PIDFD_OPEN_METHODDEF
+    OS_PIDFD_GETFD_METHODDEF
     OS_GETSID_METHODDEF
     OS_SETSID_METHODDEF
     OS_SETPGID_METHODDEF

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to