]> git.ipfire.org Git - thirdparty/rsync.git/commitdiff
android: probe openat2 usability behind a SIGSYS handler
authorAndrew Tridgell <andrew@tridgell.net>
Wed, 3 Jun 2026 23:14:52 +0000 (09:14 +1000)
committerAndrew Tridgell <andrew@tridgell.net>
Thu, 4 Jun 2026 03:41:07 +0000 (13:41 +1000)
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
android.c [new file with mode: 0644]
syscall.c
t_chmod_secure.c

index 60160c30753c12d2712e59272241df3901856952..4f221d701b523727cd0d729bf9a72339cc28ac73 100644 (file)
@@ -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 (file)
index 0000000..0094b61
--- /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 <setjmp.h>
+#include <sys/syscall.h>
+#include <linux/openat2.h>
+
+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
+}
index b402ebf8a8168ccfe68de5f757c827470d10ca05..3f023462b93cd2ad56635a6a8ae70291d90d7542 100644 (file)
--- 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);
index 130c74195f82321656cdcc7741a13475df139873..b99655a40340443121065f82ab4b6494668a3325 100644 (file)
@@ -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. */
        }