]> git.ipfire.org Git - thirdparty/mkosi.git/commitdiff
sandbox: Work around extra file descriptor opened by importing ctypes since python...
authorDaan De Meyer <daan.j.demeyer@gmail.com>
Wed, 25 Jun 2025 10:47:56 +0000 (12:47 +0200)
committerDaan De Meyer <daan.j.demeyer@gmail.com>
Wed, 25 Jun 2025 12:03:02 +0000 (14:03 +0200)
Since python 3.14, importing ctypes opens an extra file descriptor which is used to allocate libffi
closures which are in turn used by ctypes to pass python functions as C callback function pointers. We
don't use this functionality, yet the file descriptor is still opened and messes with the file descriptor
packing logic since the file descriptor to libffi will be passed as a packed file descriptor to the
executable we're invoking. To avoid that from happening, we close libffi's file descriptor after importing
ctypes.

See https://github.com/python/cpython/issues/135893.

mkosi/sandbox.py

index d210c8f1de1e0ccb4e74fa7efd295f073874b2be..a265e52764f767e9124d1436bd9b3f5bdd7e968e 100755 (executable)
@@ -8,7 +8,6 @@ minimum, please don't import any extra modules if it can be avoided.
 
 """
 
-import ctypes
 import os
 import sys
 import warnings  # noqa: F401 (loaded lazily by os.execvp() which happens too late)
@@ -33,7 +32,6 @@ EPERM = 1
 ENOENT = 2
 ENOSYS = 38
 F_DUPFD = 0
-F_GETFD = 1
 FS_IOC_GETFLAGS = 0x80086601
 FS_IOC_SETFLAGS = 0x40086602
 FS_NOCOW_FL = 0x00800000
@@ -66,6 +64,59 @@ SD_LISTEN_FDS_START = 3
 SIGSTOP = 19
 
 
+def is_valid_fd(fd: int) -> bool:
+    try:
+        os.close(os.dup(fd))
+    except OSError as e:
+        if e.errno != EBADF:
+            raise
+
+        return False
+
+    return True
+
+
+def open_file_descriptors() -> list[int]:
+    fds = []
+
+    with os.scandir("/proc/self/fd") as it:
+        for e in it:
+            if not e.is_symlink() and (e.is_file() or e.is_dir()):
+                continue
+
+            try:
+                fd = int(e.name)
+            except ValueError:
+                continue
+
+            fds.append(fd)
+
+    # os.scandir() either opens a file descriptor to the given path or dups the given file descriptor. Either
+    # way, there will be an extra file descriptor in the fds array that's not valid anymore now, so find out
+    # which one and drop it.
+    return sorted(fd for fd in fds if is_valid_fd(fd))
+
+
+class CloseNewFileDescriptors:
+    def __enter__(self) -> None:
+        self.fds = set(open_file_descriptors())
+
+    def __exit__(self, *args: object, **kwargs: object) -> None:
+        for fd in open_file_descriptors():
+            if fd not in self.fds:
+                os.close(fd)
+
+
+# Since python 3.14, importing ctypes opens an extra file descriptor which is used to allocate libffi
+# closures which are in turn used by ctypes to pass python functions as C callback function pointers. We
+# don't use this functionality, yet the file descriptor is still opened and messes with the file descriptor
+# packing logic since the file descriptor to libffi will be passed as a packed file descriptor to the
+# executable we're invoking. To avoid that from happening, we close libffi's file descriptor after importing
+# ctypes. See https://github.com/python/cpython/issues/135893.
+with CloseNewFileDescriptors():
+    import ctypes
+
+
 class mount_attr(ctypes.Structure):
     _fields_ = [
         ("attr_set", ctypes.c_uint64),
@@ -509,27 +560,7 @@ def is_relative_to(one: str, two: str) -> bool:
 
 
 def pack_file_descriptors() -> int:
-    fds = []
-
-    with os.scandir("/proc/self/fd") as it:
-        for e in it:
-            if not e.is_symlink() and (e.is_file() or e.is_dir()):
-                continue
-
-            try:
-                fd = int(e.name)
-            except ValueError:
-                continue
-
-            if fd < SD_LISTEN_FDS_START:
-                continue
-
-            fds.append(fd)
-
-    # os.scandir() either opens a file descriptor to the given path or dups the given file descriptor. Either
-    # way, there will be an extra file descriptor in the fds array that's not valid anymore now, so find out
-    # which one and drop it.
-    fds = sorted(fd for fd in fds if libc.fcntl(fd, F_GETFD, 0) >= 0)
+    fds = [fd for fd in open_file_descriptors() if fd >= SD_LISTEN_FDS_START]
 
     # The following is a reimplementation of pack_fds() in systemd.