You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Kernel-dependent features like os.pidfd_open() #193
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.
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?
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.
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.
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.
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
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...
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.
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!
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+.
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.
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.
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?
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.
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.
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.
Activity
indygreg commentedon Oct 8, 2023
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?
achimnol commentedon Oct 8, 2023
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.
achimnol commentedon Oct 9, 2023
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.
HackAttack commentedon Dec 16, 2024
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.finn-ball commentedon Dec 17, 2024
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?indygreg commentedon Dec 17, 2024
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...
FFY00 commentedon Dec 18, 2024
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.
indygreg commentedon Dec 19, 2024
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!
zanieb commentedon Dec 19, 2024
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+.
achimnol commentedon Dec 26, 2024
Great to hear some progress here! 👍🏼
heiner commentedon Mar 5, 2025
Commenting to help search engines: This is also the reason for
For
uv venv, this can be fixed by using system Python instead, e.g.uv venv --python 3.10 --python-preference=only-system.achimnol commentedon Dec 7, 2025
Are there updates here? Just checking in to prevent lost-in-history.
More and more usecases seem to be affected by this issue.
geofft commentedon Dec 7, 2025
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.
gpshead commentedon Dec 7, 2025
From a CPython point of view, runtime compatibility checks are common for this, but on low level
osmodule 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
osmodule 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.FFY00 commentedon Dec 9, 2025
Yes, what I was referring to was converting compile time checks to runtime, using weak symbols.
Eg. FFY00/cpython@7546dc5
andrey-berenda commentedon May 29, 2026
Hitting the same problem with
os.memfd_createonuv-managed Python in a Wolfi-based image. Confirmed on the latest PBS 3.14.5 (linux-aarch64-gnu):The underlying syscall works fine via
ctypeson 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 disablesos.MFD_*constants andF_ADD_SEALSworkflows for everyone usinguv python install— quite a common stack now. Any update on draft PR #772 / chance of landing a newer build base?jjhelmus commentedon Jun 1, 2026
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_clockwaitand in FFY00/cpython@7546dc5 is another option. I plan on looking at these this week.jjhelmus commentedon Jun 23, 2026
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 aspidfd_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 returnsENOSYS, which Python exposes as anOSError.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_openwould 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
statxcould be expanded to cover additional functions, making them conditionally available at runtime based on the symbols the host glibc provides.Build CPython against modern kernel UAPI headers on *-linux-gnu targe…
Enable os.getrandom with modern Linux UAPI headers (#1183)
jjhelmus commentedon Aug 12, 2026
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_openare available and will work provided the required syscalls are available at runtime. With #1183,os.getrandomis also available.#1164 and #1184 introduced weak reference so
os.memfd_createandos.copy_file_rangeare 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.