From 75d3dffcf30e5c081955bc98bc4bd1998bca84a1 Mon Sep 17 00:00:00 2001 From: Markus Mayer Date: Wed, 27 May 2026 17:44:37 -0700 Subject: [PATCH 1/4] build: auto-detect the presence of the openat2() syscall Let configure detect if the openat2() syscall is supported by the kernel headers we are building against. Do not attempt to use openat2() if support is not present. Users can still disable using the openat2() syscall manually if so desired. Signed-off-by: Markus Mayer --- configure.ac | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/configure.ac b/configure.ac index 4faab5fcb..2016b3a6b 100644 --- a/configure.ac +++ b/configure.ac @@ -103,10 +103,6 @@ dnl (and coverage-counted) without needing a pre-5.6 kernel. Behaviour-neutral dnl by default (the knob only REMOVES a tier when explicitly disabled). AC_ARG_ENABLE(openat2, AS_HELP_STRING([--disable-openat2],[do not use Linux openat2(RESOLVE_BENEATH); force the portable resolver (for exercising the fallback tier)])) -if test x"$enable_openat2" != x"no"; then - AC_DEFINE([HAVE_OPENAT2], 1, - [Define to use Linux openat2(RESOLVE_BENEATH) in secure_relative_open where available.]) -fi AC_MSG_CHECKING([if md2man can create manpages]) if test x"$ac_cv_path_PYTHON3" = x; then @@ -357,6 +353,19 @@ AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[ ]], [[return 0;]])], CFLAGS="$OLD_CFLAGS" AC_SUBST(NOEXECSTACK) +AC_CACHE_CHECK([for openat2],rsync_cv_HAVE_OPENAT2,[ + AC_COMPILE_IFELSE([ + AC_LANG_PROGRAM([[#include ]], [[int i = SYS_openat2]]) + ], + [rsync_cv_HAVE_OPENAT2=yes], [rsync_cv_HAVE_OPENAT2=no]) +]) +if test x"$enable_openat2" != x"no"; then + if test x"$rsync_cv_HAVE_OPENAT2" = x"yes"; then + AC_DEFINE([HAVE_OPENAT2], 1, + [Define to use Linux openat2(RESOLVE_BENEATH) in secure_relative_open where available.]) + fi +fi + # arrgh. libc in some old debian version screwed up the largefile # stuff, getting byte range locking wrong AC_CACHE_CHECK([for broken largefile support],rsync_cv_HAVE_BROKEN_LARGEFILE,[ From fafdd60d70fe2c59bc56103ec23800007b973daf Mon Sep 17 00:00:00 2001 From: Markus Mayer Date: Fri, 29 May 2026 10:15:13 -0700 Subject: [PATCH 2/4] t_chmod_secure: use HAVE_OPENAT2 to check for openat2() support To prevent using openat2() in situations where it is not supported, use #if defined(__linux__) && defined(HAVE_OPENAT2) in t_chmod_secure.c, just like it was already being done in syscall.c. Signed-off-by: Markus Mayer --- t_chmod_secure.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/t_chmod_secure.c b/t_chmod_secure.c index 7c57dbbca..130c74195 100644 --- a/t_chmod_secure.c +++ b/t_chmod_secure.c @@ -17,7 +17,7 @@ #include -#ifdef __linux__ +#if defined(__linux__) && defined(HAVE_OPENAT2) #include #include #endif @@ -45,7 +45,7 @@ static int errs = 0; static int kernel_resolve_beneath_supported(void) { int fd; -#ifdef __linux__ +#if defined(__linux__) && defined(HAVE_OPENAT2) { struct open_how how; memset(&how, 0, sizeof how); From 1dab74d83c6182e8b15c883ed7af350bb9a848ee Mon Sep 17 00:00:00 2001 From: Andrew Tridgell Date: Thu, 4 Jun 2026 08:50:49 +1000 Subject: [PATCH 3/4] configure: require , not just SYS_openat2 The openat2 secure resolver in syscall.c needs struct open_how and RESOLVE_BENEATH from , not only the SYS_openat2 syscall number. Some setups expose the syscall number via glibc without the kernel header present, so probing SYS_openat2 alone still left the build broken (#905). Exercise the header and struct in the configure check so HAVE_OPENAT2 is defined only when both are actually usable. --- configure.ac | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index 2016b3a6b..c4166c3c7 100644 --- a/configure.ac +++ b/configure.ac @@ -353,9 +353,18 @@ AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[ ]], [[return 0;]])], CFLAGS="$OLD_CFLAGS" AC_SUBST(NOEXECSTACK) +dnl We need both the SYS_openat2 syscall number and (for +dnl struct open_how / RESOLVE_BENEATH); some setups have one without the other. AC_CACHE_CHECK([for openat2],rsync_cv_HAVE_OPENAT2,[ AC_COMPILE_IFELSE([ - AC_LANG_PROGRAM([[#include ]], [[int i = SYS_openat2]]) + AC_LANG_PROGRAM([[ +#include +#include +]], [[ +struct open_how how; +how.resolve = RESOLVE_BENEATH; +return SYS_openat2 + (int)how.resolve; +]]) ], [rsync_cv_HAVE_OPENAT2=yes], [rsync_cv_HAVE_OPENAT2=no]) ]) From 713c8d27af6fa6b4c419741b325952ece8a8a47d Mon Sep 17 00:00:00 2001 From: Andrew Tridgell Date: Thu, 4 Jun 2026 09:14:52 +1000 Subject: [PATCH 4/4] android: probe openat2 usability behind a SIGSYS handler Android's seccomp sandbox traps openat2() with SECCOMP_RET_TRAP, which raises SIGSYS and kills the process instead of returning ENOSYS, so the secure resolver cannot simply try openat2() and inspect errno. Add openat2_usable() in a new android.c: it probes openat2() once behind a temporary SIGSYS handler and caches the result. Gate every SYS_openat2 call on openat2_usable(): in the resolver via an openat2_beneath() wrapper, and in t_chmod_secure's kernel probe directly, so a blocked openat2 reports ENOSYS and the caller falls back to the portable O_NOFOLLOW resolver. Only openat2 is gated -- a plain openat() (e.g. opening an operator-trusted absolute basedir) is left free. The probe body compiles only on Android -- __ANDROID__ is a Bionic target macro, so it is set for NDK cross-builds and native Termux alike and unset everywhere else, where openat2_usable() collapses to a constant 1. Link android.o into the secure-resolver test helpers too so their self-tests survive on Termux. Adapted from PR #909. --- Makefile.in | 12 +++---- android.c | 82 ++++++++++++++++++++++++++++++++++++++++++++++++ syscall.c | 17 ++++++++-- t_chmod_secure.c | 6 ++-- 4 files changed, 107 insertions(+), 10 deletions(-) create mode 100644 android.c diff --git a/Makefile.in b/Makefile.in index 60160c307..4f221d701 100644 --- a/Makefile.in +++ b/Makefile.in @@ -44,7 +44,7 @@ LIBOBJ=lib/wildmatch.o lib/compat.o lib/snprintf.o lib/mdfour.o lib/md5.o \ zlib_OBJS=zlib/deflate.o zlib/inffast.o zlib/inflate.o zlib/inftrees.o \ zlib/trees.o zlib/zutil.o zlib/adler32.o zlib/compress.o zlib/crc32.o OBJS1=flist.o rsync.o generator.o receiver.o cleanup.o sender.o exclude.o \ - util1.o util2.o main.o checksum.o match.o syscall.o log.o backup.o delete.o + util1.o util2.o main.o checksum.o match.o syscall.o android.o log.o backup.o delete.o OBJS2=options.o io.o compat.o hlink.o token.o uidlist.o socket.o hashtable.o \ usage.o fileio.o batch.o clientname.o chmod.o acls.o xattrs.o OBJS3=progress.o pipe.o @MD5_ASM@ @ROLL_SIMD@ @ROLL_ASM@ @@ -53,7 +53,7 @@ popt_OBJS= popt/popt.o popt/poptconfig.o \ popt/popthelp.o popt/poptparse.o popt/poptint.o OBJS=$(OBJS1) $(OBJS2) $(OBJS3) $(DAEMON_OBJ) $(LIBOBJ) @BUILD_ZLIB@ @BUILD_POPT@ -TLS_OBJ = tls.o syscall.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/permstring.o lib/sysxattrs.o @BUILD_POPT@ +TLS_OBJ = tls.o syscall.o android.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/permstring.o lib/sysxattrs.o @BUILD_POPT@ # Programs we must have to run the test cases CHECK_PROGS = rsync$(EXEEXT) tls$(EXEEXT) getgroups$(EXEEXT) getfsdev$(EXEEXT) \ @@ -172,19 +172,19 @@ getgroups$(EXEEXT): getgroups.o getfsdev$(EXEEXT): getfsdev.o $(CC) $(CFLAGS) $(LDFLAGS) -o $@ getfsdev.o $(LIBS) -TRIMSLASH_OBJ = trimslash.o syscall.o util2.o t_stub.o lib/compat.o lib/snprintf.o +TRIMSLASH_OBJ = trimslash.o syscall.o android.o util2.o t_stub.o lib/compat.o lib/snprintf.o trimslash$(EXEEXT): $(TRIMSLASH_OBJ) $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(TRIMSLASH_OBJ) $(LIBS) -T_UNSAFE_OBJ = t_unsafe.o syscall.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o +T_UNSAFE_OBJ = t_unsafe.o syscall.o android.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o t_unsafe$(EXEEXT): $(T_UNSAFE_OBJ) $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_UNSAFE_OBJ) $(LIBS) -T_CHMOD_SECURE_OBJ = t_chmod_secure.o syscall.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o lib/permstring.o +T_CHMOD_SECURE_OBJ = t_chmod_secure.o syscall.o android.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o lib/permstring.o t_chmod_secure$(EXEEXT): $(T_CHMOD_SECURE_OBJ) $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_CHMOD_SECURE_OBJ) $(LIBS) -T_SECURE_RELPATH_OBJ = t_secure_relpath.o syscall.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o lib/permstring.o +T_SECURE_RELPATH_OBJ = t_secure_relpath.o syscall.o android.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o lib/permstring.o t_secure_relpath$(EXEEXT): $(T_SECURE_RELPATH_OBJ) $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_SECURE_RELPATH_OBJ) $(LIBS) diff --git a/android.c b/android.c new file mode 100644 index 000000000..0094b61e5 --- /dev/null +++ b/android.c @@ -0,0 +1,82 @@ +/* + * Android-specific helpers. + * + * openat2() usability probe + * ------------------------- + * openat2(2) is invoked directly via syscall() because the C library lacked a + * wrapper for it for years. Under a seccomp filter that uses + * SECCOMP_RET_TRAP -- as the Android application sandbox does -- a disallowed + * syscall raises SIGSYS and *kills the process* rather than failing with + * ENOSYS, so inspecting errno after the call is too late. We therefore probe + * openat2() once, behind a temporary SIGSYS handler, so a trapped syscall is + * caught and secure_relative_open_linux() can fall back to the portable + * per-component O_NOFOLLOW resolver instead of the whole process dying. + * + * This is only needed on Android, so the probe body is compiled only there. + * __ANDROID__ is defined by Bionic's headers and reflects the *target*, not + * the build host: it is set both for NDK cross-compiles (from a Linux/macOS + * host) and for native Termux builds, and is unset on every other platform. + * That makes it a reliable compile-time switch for cross builds -- there is + * nothing to detect in configure. Everywhere else openat2() is never + * seccomp-trapped to SIGSYS (a missing syscall simply returns ENOSYS), so + * openat2_usable() collapses to a constant 1 with no run-time cost. + */ + +#include "rsync.h" + +#if defined(__ANDROID__) && defined(HAVE_OPENAT2) + +#include +#include +#include + +static sigjmp_buf openat2_probe_env; + +static void openat2_probe_handler(int signo) +{ + (void)signo; + siglongjmp(openat2_probe_env, 1); +} + +#endif + +int openat2_usable(void) +{ +#if defined(__ANDROID__) && defined(HAVE_OPENAT2) + static int cached = -1; + struct sigaction sa, old_sa; + + if (cached >= 0) + return cached; + + memset(&sa, 0, sizeof sa); + sa.sa_handler = openat2_probe_handler; + sigemptyset(&sa.sa_mask); + if (sigaction(SIGSYS, &sa, &old_sa) != 0) + return cached = 0; + + if (sigsetjmp(openat2_probe_env, 1) != 0) { + /* SIGSYS delivered: openat2 is blocked by a seccomp filter. */ + cached = 0; + } else { + struct open_how how; + int fd; + memset(&how, 0, sizeof how); + how.flags = O_RDONLY | O_DIRECTORY; + how.resolve = RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS; + fd = syscall(SYS_openat2, AT_FDCWD, ".", &how, sizeof how); + if (fd >= 0) + close(fd); + /* Usable only if the probe actually succeeded. Any failure -- + * ENOSYS (kernel < 5.6), a seccomp SECCOMP_RET_ERRNO denial + * (EPERM/EACCES), or EINVAL (RESOLVE_BENEATH unsupported) -- + * means we must fall back to the portable O_NOFOLLOW walk. */ + cached = fd >= 0; + } + + sigaction(SIGSYS, &old_sa, NULL); + return cached; +#else + return 1; +#endif +} diff --git a/syscall.c b/syscall.c index b402ebf8a..3f023462b 100644 --- a/syscall.c +++ b/syscall.c @@ -1706,6 +1706,19 @@ static int path_has_dotdot_component(const char *path) } #if defined(__linux__) && defined(HAVE_OPENAT2) +/* openat2(RESOLVE_BENEATH) via the raw syscall, gated on openat2_usable() so a + * seccomp filter that traps openat2 with SIGSYS (e.g. the Android sandbox) + * makes us report ENOSYS and fall back rather than killing the process. Only + * the openat2 call is gated here; a plain openat() is always safe to attempt. */ +static int openat2_beneath(int dirfd, const char *path, const struct open_how *how) +{ + if (!openat2_usable()) { + errno = ENOSYS; + return -1; + } + return syscall(SYS_openat2, dirfd, path, how, sizeof *how); +} + static int secure_relative_open_linux(const char *basedir, const char *relpath, int flags, mode_t mode) { struct open_how how; @@ -1734,12 +1747,12 @@ static int secure_relative_open_linux(const char *basedir, const char *relpath, memset(&bhow, 0, sizeof bhow); bhow.flags = O_RDONLY | O_DIRECTORY; bhow.resolve = RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS; - dirfd = syscall(SYS_openat2, AT_FDCWD, basedir, &bhow, sizeof bhow); + dirfd = openat2_beneath(AT_FDCWD, basedir, &bhow); if (dirfd == -1) return -1; } - retfd = syscall(SYS_openat2, dirfd, relpath, &how, sizeof how); + retfd = openat2_beneath(dirfd, relpath, &how); if (dirfd != AT_FDCWD) close(dirfd); diff --git a/t_chmod_secure.c b/t_chmod_secure.c index 130c74195..b99655a40 100644 --- a/t_chmod_secure.c +++ b/t_chmod_secure.c @@ -44,9 +44,11 @@ static int errs = 0; * other than the kernel rejecting the requested confinement flag. */ static int kernel_resolve_beneath_supported(void) { +#if (defined(__linux__) && defined(HAVE_OPENAT2)) || defined(O_RESOLVE_BENEATH) int fd; +#endif #if defined(__linux__) && defined(HAVE_OPENAT2) - { + if (openat2_usable()) { struct open_how how; memset(&how, 0, sizeof how); how.flags = O_RDONLY | O_DIRECTORY; @@ -56,7 +58,7 @@ static int kernel_resolve_beneath_supported(void) close(fd); return 1; } - /* ENOSYS = kernel < 5.6. Fall through to the O_RESOLVE_BENEATH + /* ENOSYS = kernel < 5.6 or openat2 seccomp-blocked. Fall through to the O_RESOLVE_BENEATH * probe in case we're a Linux build running on a kernel that * gained O_RESOLVE_BENEATH via some out-of-tree backport. */ }