From 0b0b270dd6bca6241c861859986a33b2ad5c2802 Mon Sep 17 00:00:00 2001 From: Samuel Henrique Date: Tue, 18 Aug 2026 20:32:48 -0700 Subject: [PATCH] testsuite: interpose the large-file spellings of open/openat/fstatat Which symbol names this test's LD_PRELOAD hook exports is decided by the compiler that builds it, and which names rsync imports is decided by configure -- and the two do not have to agree. Where off_t is not already 64 bits, configure's AC_SYS_LARGEFILE adds -D_FILE_OFFSET_BITS=64 (i386 and alpha in Debian), so glibc redirects every open()/openat()/fstatat() call in rsync to open64(), openat64() and fstatat64(). The hook is compiled by a bare "cc", so on those architectures it defines only the unsuffixed names: the receiver's opens never reach it, no EACCES is injected, the marker the positive control looks for is never written, and the test fails with positive control failed: receiver did not open the existing partial file with O_CREAT (rc=0, output='') It happens to work on Debian's 64-bit time_t ports (armhf, hppa, powerpc, ...) only by luck: their gcc predefines -D_FILE_OFFSET_BITS=64 -D_TIME_BITS=64, so glibc's __REDIRECT renames the hook's own DEFINITIONS as well and it ends up exporting exactly the *64 names rsync imports. Define both spellings explicitly so the hook interposes whichever set the rsync under test was linked against, and #undef the two macros at the top of the hook so that renaming cannot happen -- otherwise, on precisely those ports whose compiler predefines them, open() would be emitted as open64() and collide with the explicit wrapper ("symbol `open64' is already defined"), leaving the hook unbuildable and the test skipped. The new pointers are resolved through hook_resolve(), so the existing nested-dlsym recursion guard covers them as well. Failing build logs: i386 https://buildd.debian.org/status/fetch.php?pkg=rsync&arch=i386&ver=3.5.0%2Bds1-1&stamp=1786938290&raw=0 alpha https://buildd.debian.org/status/fetch.php?pkg=rsync&arch=alpha&ver=3.5.0%2Bds1-1&stamp=1786973432&raw=0 --- ...tial-protected-regular-retry-linux_test.py | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) diff --git a/testsuite/partial-protected-regular-retry-linux_test.py b/testsuite/partial-protected-regular-retry-linux_test.py index 9a4420fa9..0cb80c2fd 100644 --- a/testsuite/partial-protected-regular-retry-linux_test.py +++ b/testsuite/partial-protected-regular-retry-linux_test.py @@ -37,6 +37,17 @@ hook_code = r''' #define _GNU_SOURCE +/* Build the hook itself WITHOUT large-file redirection, whatever the compiler + * defaults to. Debian's armhf/hppa/powerpc gcc predefines + * -D_FILE_OFFSET_BITS=64 -D_TIME_BITS=64 (check with "gcc -v -E -"), and under + * those macros glibc's __REDIRECT renames the DEFINITIONS below -- open() + * becomes open64(), fstatat() becomes __fstatat64_time64() -- which then + * collide with the explicit large-file wrappers further down ("symbol `open64' + * is already defined"). Undefining them here keeps each name declared exactly + * once, so the hook always exports both spellings and interposes whichever set + * the rsync under test was linked against. */ +#undef _FILE_OFFSET_BITS +#undef _TIME_BITS #include #include #include @@ -56,6 +67,9 @@ static int (*real_openat)(int, const char *, int, ...); static int (*real_fstatat)(int, const char *, struct stat *, int); static int (*real_fxstatat)(int, int, const char *, struct stat *, int); +static int (*real_fstatat64)(int, const char *, struct stat64 *, int); +static int (*real_fxstatat64)(int, int, const char *, struct stat64 *, int); +static int (*real_fstatat64_time64)(int, const char *, struct stat64 *, int); /* Resolve on demand rather than trusting our constructor to have run. A * preloaded open() interposes for the whole process the moment the loader maps @@ -77,6 +91,10 @@ if (!real_openat) real_openat = dlsym(RTLD_NEXT, "openat"); if (!real_fstatat) real_fstatat = dlsym(RTLD_NEXT, "fstatat"); if (!real_fxstatat) real_fxstatat = dlsym(RTLD_NEXT, "__fxstatat"); + if (!real_fstatat64) real_fstatat64 = dlsym(RTLD_NEXT, "fstatat64"); + if (!real_fxstatat64) real_fxstatat64 = dlsym(RTLD_NEXT, "__fxstatat64"); + if (!real_fstatat64_time64) + real_fstatat64_time64 = dlsym(RTLD_NEXT, "__fstatat64_time64"); hook_resolving = 0; } @@ -151,6 +169,10 @@ # define HOOK_TAKES_MODE(f) ((f) & O_CREAT) #endif +#ifndef O_LARGEFILE +# define O_LARGEFILE 0 +#endif + static int is_victim_write(const char *path, int flags) { return path && strcmp(path, "victim") == 0 @@ -231,6 +253,49 @@ return fd; } +/* --- large-file spellings ------------------------------------------------- + * Which names the RECEIVER calls is settled by its own build: where off_t is + * not already 64 bits, configure's AC_SYS_LARGEFILE adds -D_FILE_OFFSET_BITS=64 + * (i386, alpha, ...), and distro CPPFLAGS add it -- along with -D_TIME_BITS=64 + * -- on the 64-bit time_t ports, so glibc redirects each open()/openat()/ + * fstatat() call to open64()/openat64()/fstatat64()/__fstatat64_time64(). + * "objdump -T rsync | grep UND" says which set a given build imports. + * + * Which names THIS HOOK exports is a different question with a different + * answer, settled by whatever the "cc" below it defaults to -- see the #undef + * at the top. Nothing keeps the two in step, so define every spelling and let + * the loader match them up. With only the unsuffixed ones the receiver's opens + * sail straight past the hook, no EACCES is ever injected, and the test reports + * "positive control failed" having exercised nothing at all. + * + * O_LARGEFILE is the only thing open64() adds over open(), so the wrappers + * below can hand the call to the unsuffixed interposer above. */ +int open64(const char *path, int flags, ...) +{ + mode_t mode = 0; + + if (HOOK_TAKES_MODE(flags)) { + va_list ap; + va_start(ap, flags); + mode = (mode_t)va_arg(ap, int); + va_end(ap); + } + return open(path, flags | O_LARGEFILE, mode); +} + +int openat64(int dfd, const char *path, int flags, ...) +{ + mode_t mode = 0; + + if (HOOK_TAKES_MODE(flags)) { + va_list ap; + va_start(ap, flags); + mode = (mode_t)va_arg(ap, int); + va_end(ap); + } + return openat(dfd, path, flags | O_LARGEFILE, mode); +} + /* ona_open() decides via fstatat(..., AT_SYMLINK_NOFOLLOW) and refuses a * component owned by neither root nor the euid. Model the attacker as a * different uid so a retry that kept the ownership walk refuses the swap. */ @@ -276,6 +341,68 @@ return rc; } +/* The stat family's large-file spellings. st_mode and st_uid sit ahead of the + * timestamps in every glibc struct stat layout, so the time32/time64 variants + * of the buffer are interchangeable for the two fields touched here. */ +static void model_foreign_owner64(int rc, const char *path, struct stat64 *st) +{ + if (rc == 0 && swapped && path && strcmp(path, "pdir") == 0 + && S_ISLNK(st->st_mode)) { + st->st_uid = geteuid() + 1; + mark(getenv("RSYNC_PARTIAL_RETRY_FOREIGN_MARKER")); + } +} + +int fstatat64(int dfd, const char *path, struct stat64 *st, int flags) +{ + int rc, saved_errno; + + hook_resolve(); + if (!real_fstatat64) { + errno = ENOSYS; + return -1; + } + rc = real_fstatat64(dfd, path, st, flags); + saved_errno = errno; + model_foreign_owner64(rc, path, st); + errno = saved_errno; + return rc; +} + +int __fxstatat64(int ver, int dfd, const char *path, struct stat64 *st, int flags) +{ + int rc, saved_errno; + + hook_resolve(); + if (!real_fxstatat64) { + errno = ENOSYS; + return -1; + } + rc = real_fxstatat64(ver, dfd, path, st, flags); + saved_errno = errno; + model_foreign_owner64(rc, path, st); + errno = saved_errno; + return rc; +} + +/* A 32-bit port built with -D_TIME_BITS=64 (Debian's armhf/armel/hppa/powerpc, + * ...) reaches fstatat() under this third name. */ +int __fstatat64_time64(int dfd, const char *path, struct stat64 *st, int flags) +{ + int rc, saved_errno; + + hook_resolve(); + if (!real_fstatat64_time64) { + errno = ENOSYS; + return -1; + } + rc = real_fstatat64_time64(dfd, path, st, flags); + saved_errno = errno; + model_foreign_owner64(rc, path, st); + errno = saved_errno; + return rc; +} + __attribute__((constructor)) static void hook_loaded(void) { hook_resolve();