From a05fe6684ae0af8128c20e2054d52d8393c2ac61 Mon Sep 17 00:00:00 2001 From: Luca Boccassi Date: Sat, 23 May 2026 16:18:03 +0100 Subject: [PATCH] libc: dlsym the time64 alias for epoll_pwait2() on 32-bit _TIME_BITS=64 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 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 --- src/libc/epoll.c | 14 ++++++++++++++ src/libc/libc-shim.h | 22 ++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/src/libc/epoll.c b/src/libc/epoll.c index 0845cf38a8..a7d4075f37 100644 --- a/src/libc/epoll.c +++ b/src/libc/epoll.c @@ -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 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 diff --git a/src/libc/libc-shim.h b/src/libc/libc-shim.h index 022ee5ad15..4377b713f0 100644 --- a/src/libc/libc-shim.h +++ b/src/libc/libc-shim.h @@ -102,3 +102,25 @@ 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; \ + }