Skip to content

Kernel-dependent features like os.pidfd_open() #193

Closed

Description

@achimnol

Recent Python versions add optional features such as os.pidfd_open() when it is available during the build time.
This is going to be the default implementation of asyncio's child process watcher as of Python 3.12, with a fallback to the thread-based legacy implementation.

It seems that the current Python 3.11 distribution in this repo does not have os.pidfd_open().
While this is not a critical regression because most libraries depending on it has a good fallback, but I'd like to be able to adopt such new kernel features.

I'm not sure how the current release process could handle this. Maybe we need to have multiple different builds like the x86_64 v2/v3/... CPU generations, by several Linux kernel versions. I'm afraid that this would incur too much burden for the build infra.

I'm just reporting this issue as a future reference, though.

Activity

  1. indygreg commented on Oct 8, 2023

    @indygreg
    Collaborator

    The way things currently work is that we build the CPython distributions against very old kernel headers and glibc to ensure maximum binary portability by targeting a very old, ~universally supported syscall + glibc API surface.

    CPython currently uses compile time checks for features like pidfd_open() support. Literally a #if defined(__linux__) && defined(__NR_pidfd_open) in C source code. So if the feature isn't present at compile time, you don't get it at run time.

    On macOS, CPython is using Mach-O weak symbols and run-time checks allow us to reference macOS SDK APIs that aren't available in all machines. If they are present at run-time, CPython can use them. If they aren't, the features depending on them aren't available. This is ideal from an end-user perspective because it doesn't penalize the common user running on modern macOS by depriving them of newer features.

    On Linux, ELF has support for weak symbols/linking. This is conceptually similar to Mach-O's similar feature. Usually you add a compiler #pragma or similar preprocessor directive to indicate a symbol is weakly linked. If the symbol resolves at run-time, the symbol/function address is non-0 and you can call it.

    Unfortunately, I don't believe CPython has any support for weak symbols on Linux/ELF. So getting run-time conditional features isn't trivially achievable. (I'd have to page this in my brain but I want to say there are some practical limitations of weak symbols on ELF that may make their use non-viable. Even if there are, there are similar features like IFUNC that could potentially be employed for dynamic run-time dispatch support.)

    That's a long way of saying that features like pidfd_open() currently require separate build variants to work. That leads to an explosion of build variants targeting various Linux + glibc feature levels. That becomes unwieldy very fast.

    It might be worth engaging upstream CPython about supporting weak symbols, IFUNC, or similar run-time dynamic feature detection on Linux/ELF like they do on macOS. This would allow pre-built CPython binaries to have better performance and features versus what is achievable today.

    @gpshead is this worth a discussion in a CPython forum? If so, which one?

  2. achimnol commented on Oct 8, 2023

    @achimnol
    Author

    Thanks for the detailed explanation!
    I don't expect this issue could be fixed in the near future, but appreciate that we could start a discussion on addition of weak symbol support in Linux.

  3. achimnol commented on Oct 9, 2023

    @achimnol
    Author

    Just a note for others: The motivation of my question/request is that when distributing Python-based apps to non-controllable "enterprise" environments (e.g., air-gapped customer-owned clusters), we need to be able to enable/disable such kernel-specific features based on the runtime availability without rebuilding everything for each customer site.

  4. HackAttack commented on Dec 16, 2024

    @HackAttack

    I have the same problem but with os.memfd_create, which requires glibc 2.27. I can work around it (in my Bazel build) by using the host python, but that sort of defeats the purpose.

  5. finn-ball commented on Dec 17, 2024

    @finn-ball

    The way things currently work is that we build the CPython distributions against very old kernel headers and glibc to ensure maximum binary portability by targeting a very old, ~universally supported syscall + glibc API surface.

    Would a short term solution for some users, not be to simply update the docker container for one with a newer glibc, such as this and build it themselves?

    diff --git a/cpython-unix/base.Dockerfile b/cpython-unix/base.Dockerfile
    index 76811a5..3844827 100644
    --- a/cpython-unix/base.Dockerfile
    +++ b/cpython-unix/base.Dockerfile
    @@ -1,5 +1,5 @@
     # Debian Jessie.
    -FROM debian@sha256:32ad5050caffb2c7e969dac873bce2c370015c2256ff984b70c1c08b3a2816a0
    +FROM debian:bookworm
     MAINTAINER Gregory Szorc <gregory.szorc@gmail.com>
     
     RUN groupadd -g 1000 build && \
    @@ -19,10 +19,10 @@ WORKDIR '/build'
     
     # Jessie's signing keys expired in late 2022. So need to add [trusted=yes] to force trust.
     # Jessie stopped publishing snapshots in March 2023.
    -RUN for s in debian_jessie debian_jessie-updates debian-security_jessie/updates; do \
    -      echo "deb [trusted=yes] http://snapshot.debian.org/archive/${s%_*}/20230322T152120Z/ ${s#*_} main"; \
    -    done > /etc/apt/sources.list && \
    -    ( echo 'quiet "true";'; \
    +# RUN for s in debian_jessie debian_jessie-updates debian-security_jessie/updates; do \
    +      # echo "deb [trusted=yes] http://snapshot.debian.org/archive/${s%_*}/20230322T152120Z/ ${s#*_} main"; \
    +    #  done > /etc/apt/sources.list && \
    +RUN ( echo 'quiet "true";'; \
           echo 'APT::Get::Assume-Yes "true";'; \
           echo 'APT::Install-Recommends "false";'; \
           echo 'Acquire::Check-Valid-Until "false";'; \
    diff --git a/cpython-unix/build.cross.Dockerfile b/cpython-unix/build.cross.Dockerfile
    index aa17d6c..5c28c7a 100644
    --- a/cpython-unix/build.cross.Dockerfile
    +++ b/cpython-unix/build.cross.Dockerfile
    @@ -1,5 +1,5 @@
     # Debian Stretch.
    -FROM debian@sha256:cebe6e1c30384958d471467e231f740e8f0fd92cbfd2a435a186e9bada3aee1c
    +FROM debian:bookworm
     MAINTAINER Gregory Szorc <gregory.szorc@gmail.com>
     
     RUN groupadd -g 1000 build && \
    @@ -20,10 +20,10 @@ WORKDIR '/build'
     # Stretch stopped publishing snapshots in April 2023. Last snapshot
     # is 20230423T032533Z. But there are package authentication issues
     # with this snapshot.
    -RUN for s in debian_stretch debian_stretch-updates debian-security_stretch/updates; do \
    -      echo "deb http://snapshot.debian.org/archive/${s%_*}/20221105T150728Z/ ${s#*_} main"; \
    -    done > /etc/apt/sources.list && \
    -    ( echo 'quiet "true";'; \
    +# RUN for s in debian_stretch debian_stretch-updates debian-security_stretch/updates; do \
    +#       echo "deb http://snapshot.debian.org/archive/${s%_*}/20221105T150728Z/ ${s#*_} main"; \
    +#     done > /etc/apt/sources.list && \
    +RUN ( echo 'quiet "true";'; \
           echo 'APT::Get::Assume-Yes "true";'; \
           echo 'APT::Install-Recommends "false";'; \
           echo 'Acquire::Check-Valid-Until "false";'; \
    diff --git a/cpython-unix/rust.Dockerfile b/cpython-unix/rust.Dockerfile
    index 82a0a92..15f0e97 100644
    --- a/cpython-unix/rust.Dockerfile
    +++ b/cpython-unix/rust.Dockerfile
    @@ -4,5 +4,5 @@ RUN apt-get install \
         curl \
         libc6-dev \
         python2.7 \
    -    python \
    +    python3 \
         tar \
    diff --git a/cpython-unix/xcb.Dockerfile b/cpython-unix/xcb.Dockerfile
    index 0480eca..acdf77c 100644
    --- a/cpython-unix/xcb.Dockerfile
    +++ b/cpython-unix/xcb.Dockerfile
    @@ -1,3 +1,3 @@
     {% include 'build.Dockerfile' %}
     RUN apt-get install \
    -    python
    +    python3
    diff --git a/cpython-unix/xcb.cross.Dockerfile b/cpython-unix/xcb.cross.Dockerfile
    index cc003ff..154a5ba 100644
    --- a/cpython-unix/xcb.cross.Dockerfile
    +++ b/cpython-unix/xcb.cross.Dockerfile
    @@ -1,3 +1,3 @@
     {% include 'build.cross.Dockerfile' %}
     RUN apt-get install \
    -    python
    +    python3
    
  6. indygreg commented on Dec 17, 2024

    @indygreg
    Collaborator

    Yes, if you want the builds to pick up newer features from newer glibc and Linux the way to do that is to modernize those versions via the container base image.

    It has been a few years and it may be worth making that change universally. It depends what CPython upstream is doing with ABI compatibility in wheels. (We want our distros to be compatible with oldest ABI seen in binary wheels in the wild.) I almost filed a standalone issue to track this a few hours ago...

  7. added
    performancePotential performance improvement
    compatibilityCompatibility with CPython and the broader ecosystem
    on Dec 18, 2024
  8. FFY00 commented on Dec 18, 2024

    @FFY00

    I looked into this and it doesn't seem overly complicated to move the compatibility checks to runtime, though I am not really convinced it's worth the trouble over just publishing a couple of different builds targetting the already defined manylinux and manymusl platform tags.

    The main issue with supporting this upstream is the complexity and maintenance burden, though it's definitely worth a discussion.

  9. indygreg commented on Dec 19, 2024

    @indygreg
    Collaborator

    Upstream already uses weak linking on macOS so they can target an older SDK but leverage features from a newer one.

    The Apple SDKs do make this easier than on Linux. (I recall there being caveats with weak linking and runtime availability checking in Linux but I can't recall specifics.) You should engage upstream about doing this on Linux!

  10. zanieb commented on Dec 19, 2024

    @zanieb
    Member

    We chatted on Discord and it sounds like the upstream is willing to accept patches for this on Linux. I intend to do so when I have the chance!

    We'll also probably add a build variant targeting one (or more) of the latest manylinux libc targets, since the added weak linking would only be available on 3.14+.

  11. achimnol commented on Dec 26, 2024

    @achimnol
    Author

    Great to hear some progress here! 👍🏼

  12. heiner commented on Mar 5, 2025

    @heiner

    Commenting to help search engines: This is also the reason for

    >>> os.memfd_create
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    AttributeError: module 'os' has no attribute 'memfd_create'
    

    For uv venv, this can be fixed by using system Python instead, e.g. uv venv --python 3.10 --python-preference=only-system.

  13. self-assigned this
    on Aug 18, 2025
  14. achimnol commented on Dec 7, 2025

    @achimnol
    Author

    Are there updates here? Just checking in to prevent lost-in-history.
    More and more usecases seem to be affected by this issue.

  15. geofft commented on Dec 7, 2025

    @geofft
    Collaborator

    It's definitely on the radar! I have an incomplete draft PR, not sure if I'll get it in shape before the holidays but I hope to have something by early January.

  16. gpshead commented on Dec 7, 2025

    @gpshead

    From a CPython point of view, runtime compatibility checks are common for this, but on low level os module APIs those should be expected to just be an EINVAL OSError form of exception. Higher level things with options of which syscalls to use such as subprocess catch invalid syscall and fallback to the next worst alternative until options are exhausted.

    But why you don't see the symbol existing at all for this issue: CPython typically requires compiling against a libc that has the function defined because that is practical (there was one case where I made a syscall directly while the world caught up due to need and prevalence of ancient glibc's - yuck, we don't like to maintain that). But what libc you compile against has little to do with which kernel version is running. A build time check saying a function is available has never strictly meant that it could actually be used. That mostly comes up with sad unsupported hardware w/o upstream kernel support (tons of mobile and embedded chipsets) that you can shoehorn a modern user space onto but must run an old only-option of a vendor kernel. Similarly a function being in libc doesn't mean it actually works as intended so we've wound up with libc version or runtime behavior checks as well (posix_spawn?) at times.

    We do not try to build a support matrix out of these, fallbacks like that are best effort bandaids. Scenarios where these messes happen tend to be well outside of PEP-11 tiered platforms and even beyond most CPython buildbots. They are not common configs anyone is happy to have around.

    I advocate for drawing a hard line in versions supported and intentionally not supporting older shapes. Force users who want nice things to have a stack that is up to it. Anything else is supporting someone's security nightmare as such old kernels and libraries are long past their rm-by EOL dates.

    ... all that said... if what @FFY00 was talking about 1y ago above would move CPython to fewer compile time checks determining which functions even show up in the os module I think it'd be great. I guess "all" we need there is dynamic detection (weak symbols or otherwise) of the libc function being available as a pattern in posixmodule.c with an exception being raised when the function isn't found. I personally wouldn't want us to be the business of circumventing libc and making syscalls directly for these (platform ABI specific using magic constants), but even that could technically be doable in places where there is a justifiable need to improve user quality of life.

  17. FFY00 commented on Dec 9, 2025

    @FFY00

    Yes, what I was referring to was converting compile time checks to runtime, using weak symbols.

    Eg. FFY00/cpython@7546dc5

  18. andrey-berenda commented on May 29, 2026

    @andrey-berenda

    Hitting the same problem with os.memfd_create on uv-managed Python in a Wolfi-based image. Confirmed on the latest PBS 3.14.5 (linux-aarch64-gnu):

    $ python -c "import sysconfig; print(sysconfig.get_config_var('HAVE_MEMFD_CREATE'))"
    0
    $ python -c "import os; print(hasattr(os, 'memfd_create'))"
    False
    

    The underlying syscall works fine via ctypes on the same image (Wolfi ships glibc 2.41+), so this is purely a configure-time decision baked into PBS by the old build sysroot.

    Aside from memfd_create, this also disables os.MFD_* constants and F_ADD_SEALS workflows for everyone using uv python install — quite a common stack now. Any update on draft PR #772 / chance of landing a newer build base?

  19. jjhelmus commented on Jun 1, 2026

    @jjhelmus
    Contributor

    Moving to a newer base image changes the compatibility requirements for pbs which is something we are not ready for yet.

    Using weak symbols and runtime detection similar to the approach in #985 for sem_clockwait and in FFY00/cpython@7546dc5 is another option. I plan on looking at these this week.

  20. jjhelmus commented on Jun 23, 2026

    @jjhelmus
    Contributor

    There are two largely independent compatibility axes here: the Linux kernel API and the glibc API. They can be addressed separately.

    For kernel interfaces that CPython invokes directly with syscall(), such as pidfd_open, compile-time availability is determined by the installed Linux UAPI headers. CPython only builds the function when the corresponding syscall number is defined. At runtime, calling the function invokes the syscall. If the running kernel does not implement it, the call returns ENOSYS, which Python exposes as an OSError.

    One option is to build against newer Linux UAPI headers. This would compile support for newer syscalls without increasing the minimum required kernel or glibc version. The tradeoff is that attributes such as os.pidfd_open would be present even when the running kernel does not implement the syscall. So, hasattr(os, "pidfd_open") indicates that support was compiled in, not that the syscall is usable at runtime.

    For glibc interfaces the existing weak linking used for statx could be expanded to cover additional functions, making them conditionally available at runtime based on the symbols the host glibc provides.

  21. jjhelmus commented on Aug 12, 2026

    @jjhelmus
    Contributor

    There has been a good amount of work on adding runtime support for various kernel and libc dependent functionality.

    In particular with #1163, the Linux distributions are now built against modern UAPI kernel headers, so functions like os.pidfd_open are available and will work provided the required syscalls are available at runtime. With #1183, os.getrandom is also available.

    #1164 and #1184 introduced weak reference so os.memfd_create and os.copy_file_range are available if the runtime libc includes support.

    How these are made available may change in future releases, #1216, but the functions will always be available.

    Closing as this covers all the kernel/libc dependent functionality that I am aware of that was not already available in the distributions. If there are other please report them and I can take a look.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

compatibilityCompatibility with CPython and the broader ecosystemperformancePotential performance improvement

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions