]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
libc: dlsym the time64 alias for epoll_pwait2() on 32-bit _TIME_BITS=64
authorLuca Boccassi <luca.boccassi@gmail.com>
Sat, 23 May 2026 15:18:03 +0000 (16:18 +0100)
committerDaan De Meyer <daan.j.demeyer@gmail.com>
Sun, 24 May 2026 13:00:37 +0000 (15:00 +0200)
Commit f795d54591 ("libc,shared: detect newer library symbols at
runtime") replaced the build-time HAVE_EPOLL_PWAIT2 gate with a runtime
shim that resolves the libc symbol via dlsym(RTLD_DEFAULT, "epoll_pwait2").
That breaks on 32-bit architectures built with _TIME_BITS=64 (e.g. Debian
armhf since the time64 transition).

When _TIME_BITS=64 is in effect on a 32-bit target, glibc's <sys/epoll.h>
asm-renames the C identifier `epoll_pwait2` to the matching 64-bit-time_t
ABI symbol `__epoll_pwait2_time64`, so a normal C call resolves to the
variant whose `struct timespec` layout (16 bytes: s64 tv_sec, long tv_nsec,
long pad) matches what the compiler is using:

    /* /usr/include/sys/epoll.h */
    #ifndef __USE_TIME64_REDIRECTS
    extern int epoll_pwait2 (int __epfd, struct epoll_event *__events,
                             int __maxevents,
                             const struct timespec *__timeout,
                             const __sigset_t *__ss) ...;
    #else
    # ifdef __REDIRECT
    extern int __REDIRECT (epoll_pwait2, (int __epfd, ...,
                                          const struct timespec *__timeout,
                                          const __sigset_t *__ss),
                           __epoll_pwait2_time64) ...;
    # else
    #  define epoll_pwait2 __epoll_pwait2_time64
    # endif
    #endif

dlsym() doesn't see that header-level rename and returns the legacy
32-bit-time_t `epoll_pwait2` symbol, which interprets only the first 8
bytes of the 16-byte timespec we hand it. With a little-endian layout
that means the legacy callee reads tv_sec = (low 32 bits of real tv_sec)
and tv_nsec = (high 32 bits of real tv_sec, normally 0). Sub-second
timeouts therefore become {0,0} and return immediately, while multi-
second timeouts get truncated to whole seconds.

That matches the test failure reported on armv7:

    /* test_simple_timeout */
    src/libsystemd/sd-event/test-event.c:724: Assertion failed:
      Expected "t >= usec_add(f, some_time)",
      but 571153514285 < 571154403965

(the 889680 us shortfall is consistent with a sub-second `some_time` that
got rounded down to zero by the truncated wait).

Fix it by adding a DEFINE_LIBC_ERRNO_SHIM_NAMED() variant of the macro
that takes an explicit dlsym symbol name string. Both macros keep their
own copy of the body so every `func` reference stays behind `#` or `##`,
otherwise the override-header `#define epoll_pwait2 epoll_pwait2_shim`
would rewrite the token before the inner macro could paste it (yielding
`epoll_pwait2_shim_shim`). src/libc/epoll.c then picks
"__epoll_pwait2_time64" under __USE_TIME_BITS64 so the cached function
pointer matches the timespec ABI the rest of the code is built with.

Follow-up for f795d5459151ad84acf77557cf47dddddb3b4bce

Co-developed-by: Claude Opus 4.7 <noreply@anthropic.com>
src/libc/epoll.c
src/libc/libc-shim.h

index 0845cf38a875a82328a88fa77117b3625643ca1c..a7d4075f37a35e9385ae1029b42f64b92968993f 100644 (file)
@@ -4,9 +4,23 @@
 
 #include "libc-shim.h"
 
+/* On 32-bit architectures built with _TIME_BITS=64, glibc renames epoll_pwait2() to
+ * __epoll_pwait2_time64 via __asm__() in <sys/epoll.h> so the linker picks the variant whose
+ * struct timespec ABI matches our 64-bit time_t. dlsym() can't see that header-level rename, so
+ * we have to spell out the right name here, otherwise we'd silently dispatch a 64-bit timespec to
+ * the legacy 32-bit-time_t entry point. */
+#ifdef __USE_TIME_BITS64
+DEFINE_LIBC_ERRNO_SHIM_NAMED(epoll_pwait2, "__epoll_pwait2_time64", int,
+                             int, fd,
+                             struct epoll_event *, events,
+                             int, maxevents,
+                             const struct timespec *, timeout,
+                             const sigset_t *, sigmask)
+#else
 DEFINE_LIBC_ERRNO_SHIM(epoll_pwait2, int,
                        int, fd,
                        struct epoll_event *, events,
                        int, maxevents,
                        const struct timespec *, timeout,
                        const sigset_t *, sigmask)
+#endif
index 022ee5ad157bd9241d5b31d6b21c40cbde1ad8ef..4377b713f0ab6814bb5799bc7628094312a9efee 100644 (file)
                 errno = ENOSYS;                                                                      \
                 return -1;                                                                           \
         }
+
+/* Like DEFINE_LIBC_ERRNO_SHIM but with an explicit string for the libc symbol name to dlsym. This is
+ * needed for functions whose libc symbol is renamed via __asm__() in the header (e.g. when glibc
+ * redirects time-related calls to their __*_time64 aliases on 32-bit systems built with
+ * _TIME_BITS=64) since dlsym() doesn't see those header-level renames, so the caller has to spell out
+ * the actual ABI symbol name that matches the struct layout the compiler picked. We can't forward to
+ * DEFINE_LIBC_ERRNO_SHIM since passing `func` as a regular argument would let the override-header
+ * #define rewrite the token (e.g. `epoll_pwait2` to `epoll_pwait2_shim`) before the inner macro
+ * could paste it, so we duplicate the body and keep every `func` reference behind `#` or `##`. */
+#define DEFINE_LIBC_ERRNO_SHIM_NAMED(func, sym_name, ret, ...)                                       \
+        static typeof(&func##_shim) func##_shim_cache;                                               \
+        __attribute__((constructor)) static void func##_shim_init(void) {                            \
+                void *p = dlsym(RTLD_DEFAULT, sym_name);                                             \
+                __asm__ volatile("" ::: "memory");                                                   \
+                func##_shim_cache = (typeof(&func##_shim)) p;                                        \
+        }                                                                                            \
+        ret func##_shim(_SHIM_DECL(__VA_ARGS__)) {                                                   \
+                if (func##_shim_cache)                                                               \
+                        return func##_shim_cache(_SHIM_NAME(__VA_ARGS__));                           \
+                errno = ENOSYS;                                                                      \
+                return -1;                                                                           \
+        }