Skip to content

Commit 4348bb9

Browse files
committed
gh-153400: Resolve newer libc/syscall wrappers at runtime on Linux
os functions like copy_file_range() are gated on a configure-time AC_CHECK_FUNCS probe and compiled out when the build libc lacks the symbol. A redistributable built against an old glibc (the python-build-standalone builds target glibc 2.17) therefore never exposes them, even when run on a newer glibc or a capable kernel. Add Modules/posixshims.h, which on Linux always exposes _Py_<func>(), resolves the libc symbol once at load time via dlsym(RTLD_DEFAULT) from a constructor, and falls back to the raw syscall when the running libc lacks the wrapper. Resolving in a constructor keeps dlsym(), which is not async-signal-safe, out of signal handlers and the fork()/exec() window. Off Linux the classic build-time HAVE_* direct calls are kept. Limit the shims to wrappers newer than the glibc 2.17 baseline, since anything at or below it is always present in the build libc: copy_file_range (glibc 2.27) memfd_create (glibc 2.27) pidfd_open (glibc 2.36) pidfd_getfd (glibc 2.36) pidfd_open() and pidfd_getfd() previously issued raw syscalls unconditionally; they now prefer the glibc wrapper when present.
1 parent 53b0498 commit 4348bb9

4 files changed

Lines changed: 165 additions & 20 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
On Linux, :func:`os.copy_file_range`, :func:`os.memfd_create`,
2+
:func:`os.pidfd_open` and :func:`os.pidfd_getfd` are now resolved at runtime, so
3+
they stay available on interpreters built against an older glibc when the
4+
running system provides them.

Modules/clinic/posixmodule.c.h

Lines changed: 9 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Modules/posixmodule.c

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@
4646
#include <stdio.h> // ctermid()
4747
#include <stdlib.h> // system()
4848

49+
#include "posixshims.h" // _Py_copy_file_range()
50+
4951
#ifdef HAVE_UNISTD_H
5052
# include <unistd.h> // symlink()
5153
#endif
@@ -10773,8 +10775,7 @@ os_wait_impl(PyObject *module)
1077310775

1077410776

1077510777
// This system call always crashes on older Android versions.
10776-
#if defined(__linux__) && defined(__NR_pidfd_open) && \
10777-
!(defined(__ANDROID__) && __ANDROID_API__ < 31)
10778+
#ifdef _Py_HAVE_PIDFD_OPEN
1077810779
/*[clinic input]
1077910780
os.pidfd_open
1078010781
pid: pid_t
@@ -10790,7 +10791,7 @@ static PyObject *
1079010791
os_pidfd_open_impl(PyObject *module, pid_t pid, unsigned int flags)
1079110792
/*[clinic end generated code: output=5c7252698947dc41 input=03058b32c389f874]*/
1079210793
{
10793-
int fd = syscall(__NR_pidfd_open, pid, flags);
10794+
int fd = _Py_pidfd_open(pid, flags);
1079410795
if (fd < 0) {
1079510796
return posix_error();
1079610797
}
@@ -10799,8 +10800,7 @@ os_pidfd_open_impl(PyObject *module, pid_t pid, unsigned int flags)
1079910800
#endif
1080010801

1080110802

10802-
#if defined(__linux__) && defined(__NR_pidfd_getfd) && \
10803-
!(defined(__ANDROID__) && __ANDROID_API__ < 31)
10803+
#ifdef _Py_HAVE_PIDFD_GETFD
1080410804
/*[clinic input]
1080510805
os.pidfd_getfd
1080610806
pidfd: int
@@ -10819,7 +10819,7 @@ os_pidfd_getfd_impl(PyObject *module, int pidfd, int targetfd,
1081910819
unsigned int flags)
1082010820
/*[clinic end generated code: output=e1a1415a13c7137f input=ef6417fb10deb1cc]*/
1082110821
{
10822-
int fd = syscall(__NR_pidfd_getfd, pidfd, targetfd, flags);
10822+
int fd = _Py_pidfd_getfd(pidfd, targetfd, flags);
1082310823
if (fd < 0) {
1082410824
return posix_error();
1082510825
}
@@ -13018,7 +13018,7 @@ os_pwritev_impl(PyObject *module, int fd, PyObject *buffers, Py_off_t offset,
1301813018
}
1301913019
#endif /* HAVE_PWRITEV */
1302013020

13021-
#ifdef HAVE_COPY_FILE_RANGE
13021+
#ifdef _Py_HAVE_COPY_FILE_RANGE
1302213022
/*[clinic input]
1302313023

1302413024
os.copy_file_range
@@ -13070,7 +13070,7 @@ os_copy_file_range_impl(PyObject *module, int src, int dst, Py_ssize_t count,
1307013070

1307113071
do {
1307213072
Py_BEGIN_ALLOW_THREADS
13073-
ret = copy_file_range(src, p_offset_src, dst, p_offset_dst, count, flags);
13073+
ret = _Py_copy_file_range(src, p_offset_src, dst, p_offset_dst, count, flags);
1307413074
Py_END_ALLOW_THREADS
1307513075
} while (ret < 0 && errno == EINTR && !(async_err = PyErr_CheckSignals()));
1307613076

@@ -13080,7 +13080,7 @@ os_copy_file_range_impl(PyObject *module, int src, int dst, Py_ssize_t count,
1308013080

1308113081
return PyLong_FromSsize_t(ret);
1308213082
}
13083-
#endif /* HAVE_COPY_FILE_RANGE*/
13083+
#endif /* _Py_HAVE_COPY_FILE_RANGE */
1308413084

1308513085
#if (defined(HAVE_SPLICE) && !defined(_AIX))
1308613086
/*[clinic input]
@@ -15774,7 +15774,7 @@ os_urandom_impl(PyObject *module, Py_ssize_t size)
1577415774
return PyBytesWriter_Finish(writer);
1577515775
}
1577615776

15777-
#ifdef HAVE_MEMFD_CREATE
15777+
#ifdef _Py_HAVE_MEMFD_CREATE
1577815778
/*[clinic input]
1577915779
os.memfd_create
1578015780

@@ -15790,7 +15790,7 @@ os_memfd_create_impl(PyObject *module, PyObject *name, unsigned int flags)
1579015790
int fd;
1579115791
const char *bytes = PyBytes_AS_STRING(name);
1579215792
Py_BEGIN_ALLOW_THREADS
15793-
fd = memfd_create(bytes, flags);
15793+
fd = _Py_memfd_create(bytes, flags);
1579415794
Py_END_ALLOW_THREADS
1579515795
if (fd == -1) {
1579615796
return PyErr_SetFromErrno(PyExc_OSError);

Modules/posixshims.h

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/* posixshims.h: resolve newer libc/syscall wrappers at runtime, not build time.
2+
3+
CPython gates functions like copy_file_range() on a configure-time
4+
AC_CHECK_FUNCS probe, compiling them out when the build libc lacks the symbol.
5+
That permanently disables them in a redistributable built against an old glibc
6+
even when it later runs on a newer one. Instead, on Linux we always expose
7+
_Py_<func>(), resolve the libc symbol once at load time via dlsym(RTLD_DEFAULT)
8+
from a constructor, and fall back to the raw syscall. A constructor (vs. lazy
9+
resolution) keeps dlsym(), which is not async-signal-safe, out of signal
10+
handlers and the fork()/exec() window, and lets the cache skip synchronization.
11+
12+
Each _Py_<func>() is declared explicitly; only the shared body is a macro.
13+
Off Linux, each function keeps its classic build-time HAVE_* direct call. */
14+
15+
#ifndef Py_POSIXSHIMS_H
16+
#define Py_POSIXSHIMS_H
17+
18+
/* Needs ELF constructors, dlsym() and Linux syscall numbers. */
19+
#if defined(__linux__) && (defined(__GNUC__) || defined(__clang__)) \
20+
&& defined(HAVE_DLFCN_H) && defined(HAVE_SYS_SYSCALL_H)
21+
# define _Py_HAVE_POSIX_SHIMS
22+
#endif
23+
24+
#ifdef _Py_HAVE_POSIX_SHIMS
25+
26+
#include <dlfcn.h> // dlsym(), RTLD_DEFAULT
27+
#include <sys/syscall.h> // __NR_* syscall numbers
28+
#include <sys/types.h> // off_t, ssize_t
29+
#include <unistd.h> // syscall()
30+
31+
/* The raw-syscall fallback needs a syscall number. If a stale build header
32+
predates one, default it to an invalid number so the shim still builds and the
33+
dlsym'd libc symbol stays usable; only the syscall fallback is lost, returning
34+
-1/ENOSYS at runtime. <sys/syscall.h> above is the sole source of these
35+
numbers in this translation unit, so nothing redefines them afterwards. */
36+
#ifndef __NR_copy_file_range
37+
# define __NR_copy_file_range (-1)
38+
#endif
39+
#ifndef __NR_memfd_create
40+
# define __NR_memfd_create (-1)
41+
#endif
42+
#ifndef __NR_pidfd_open
43+
# define __NR_pidfd_open (-1)
44+
#endif
45+
#ifndef __NR_pidfd_getfd
46+
# define __NR_pidfd_getfd (-1)
47+
#endif
48+
49+
/* A per-function cache plus a constructor that resolves the libc symbol once at
50+
load time. The __asm__ barrier after dlsym() is load-bearing under LTO:
51+
without it the compiler may tail-call dlsym(), breaking glibc's
52+
return-address caller lookup and crashing at startup (glibc BZ #34156). */
53+
#define _Py_SHIM_RESOLVER(func) \
54+
static void *_Py_shim_##func; \
55+
__attribute__((constructor)) \
56+
static void _Py_shim_##func##_resolve(void) { \
57+
void *p = dlsym(RTLD_DEFAULT, #func); \
58+
__asm__ volatile("" ::: "memory"); \
59+
_Py_shim_##func = p; \
60+
}
61+
62+
/* Body of an explicitly declared _Py_<func>() wrapper: call the resolved libc
63+
symbol if present, else fall back to syscall(__NR_<func>, ...). On a kernel
64+
too old for the syscall that returns -1/ENOSYS, which posixmodule.c reports as
65+
an ordinary OSError. The cast is __typeof__ of _Py_<func> (the wrapper
66+
itself), not of <func>, which may be undeclared at build time. */
67+
#define _Py_SHIM_SYSCALL(func, ...) \
68+
if (_Py_shim_##func != NULL) { \
69+
return ((__typeof__(&_Py_##func)) _Py_shim_##func)(__VA_ARGS__); \
70+
} \
71+
return syscall(__NR_##func, __VA_ARGS__)
72+
73+
#endif /* _Py_HAVE_POSIX_SHIMS */
74+
75+
76+
/* ---- copy_file_range() (glibc 2.27) ------------------------------------- */
77+
78+
#if defined(_Py_HAVE_POSIX_SHIMS)
79+
# define _Py_HAVE_COPY_FILE_RANGE
80+
_Py_SHIM_RESOLVER(copy_file_range)
81+
/* off_t is 64-bit (_FILE_OFFSET_BITS=64), matching the kernel's loff_t. */
82+
static inline ssize_t
83+
_Py_copy_file_range(int fd_in, off_t *off_in, int fd_out, off_t *off_out,
84+
size_t len, unsigned int flags)
85+
{
86+
_Py_SHIM_SYSCALL(copy_file_range,
87+
fd_in, off_in, fd_out, off_out, len, flags);
88+
}
89+
90+
#elif defined(HAVE_COPY_FILE_RANGE)
91+
# define _Py_HAVE_COPY_FILE_RANGE
92+
# define _Py_copy_file_range copy_file_range
93+
#endif
94+
95+
96+
/* ---- memfd_create() (glibc 2.27) ---------------------------------------- */
97+
98+
#if defined(_Py_HAVE_POSIX_SHIMS)
99+
# define _Py_HAVE_MEMFD_CREATE
100+
_Py_SHIM_RESOLVER(memfd_create)
101+
static inline int
102+
_Py_memfd_create(const char *name, unsigned int flags)
103+
{
104+
_Py_SHIM_SYSCALL(memfd_create, name, flags);
105+
}
106+
107+
#elif defined(HAVE_MEMFD_CREATE)
108+
# define _Py_HAVE_MEMFD_CREATE
109+
# define _Py_memfd_create memfd_create
110+
#endif
111+
112+
113+
/* ---- pidfd_open(), pidfd_getfd() (glibc 2.36) --------------------------- *
114+
115+
Previously unconditional raw syscalls; the shim now prefers glibc's wrapper.
116+
Linux-only from the start (no non-Linux branch); the Android API floor from
117+
the original guards is preserved. */
118+
119+
#if defined(_Py_HAVE_POSIX_SHIMS) \
120+
&& !(defined(__ANDROID__) && __ANDROID_API__ < 31)
121+
# define _Py_HAVE_PIDFD_OPEN
122+
_Py_SHIM_RESOLVER(pidfd_open)
123+
static inline int
124+
_Py_pidfd_open(pid_t pid, unsigned int flags)
125+
{
126+
_Py_SHIM_SYSCALL(pidfd_open, pid, flags);
127+
}
128+
#endif
129+
130+
#if defined(_Py_HAVE_POSIX_SHIMS) \
131+
&& !(defined(__ANDROID__) && __ANDROID_API__ < 31)
132+
# define _Py_HAVE_PIDFD_GETFD
133+
_Py_SHIM_RESOLVER(pidfd_getfd)
134+
static inline int
135+
_Py_pidfd_getfd(int pidfd, int targetfd, unsigned int flags)
136+
{
137+
_Py_SHIM_SYSCALL(pidfd_getfd, pidfd, targetfd, flags);
138+
}
139+
#endif
140+
141+
#endif /* !Py_POSIXSHIMS_H */

0 commit comments

Comments
 (0)